|
|
@@ -13,6 +13,11 @@ struct WidgetsRootView: View {
|
|
|
@ObservedObject private var premiumStore = PremiumStore.shared
|
|
|
@State private var query = ""
|
|
|
@State private var toastMessage: String?
|
|
|
+ enum ToastKind {
|
|
|
+ case added
|
|
|
+ case removed
|
|
|
+ }
|
|
|
+ @State private var toastKind: ToastKind?
|
|
|
@State private var toastTask: Task<Void, Never>?
|
|
|
@State private var showingPremiumScreen = false
|
|
|
|
|
|
@@ -46,8 +51,8 @@ struct WidgetsRootView: View {
|
|
|
.padding(.horizontal, 10)
|
|
|
.padding(.bottom, 12)
|
|
|
.overlay(alignment: .top) {
|
|
|
- if let toastMessage {
|
|
|
- AddedToDesktopToast(message: toastMessage)
|
|
|
+ if let toastMessage, let toastKind {
|
|
|
+ DesktopWidgetToast(message: toastMessage, kind: toastKind)
|
|
|
.padding(.top, 6)
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
}
|
|
|
@@ -128,8 +133,9 @@ struct WidgetsRootView: View {
|
|
|
WidgetPreviewAddCard(
|
|
|
app: selectedApp,
|
|
|
variant: variant,
|
|
|
+ isAdded: isWidgetAdded(appID: selectedApp.id, variantID: variant.id),
|
|
|
isPremiumUnlocked: premiumStore.isPremiumUnlocked,
|
|
|
- onAdd: { handleAddWidgetTap(app: selectedApp, variant: variant) }
|
|
|
+ onToggle: { handleWidgetTap(app: selectedApp, variant: variant) }
|
|
|
)
|
|
|
}
|
|
|
Spacer(minLength: 0)
|
|
|
@@ -160,7 +166,7 @@ struct WidgetsRootView: View {
|
|
|
Text(app.name)
|
|
|
.font(.system(size: 20, weight: .bold))
|
|
|
.foregroundStyle(.white.opacity(0.94))
|
|
|
- Text("Click the green + to add widgets to your desktop.")
|
|
|
+ Text("Click a widget card to add it. Click again to remove it.")
|
|
|
.font(.system(size: 12.5, weight: .medium))
|
|
|
.foregroundStyle(.white.opacity(0.65))
|
|
|
}
|
|
|
@@ -190,6 +196,9 @@ struct WidgetsRootView: View {
|
|
|
}
|
|
|
|
|
|
private func addToDesktop(app: LauncherApp, variant: WidgetVariant) {
|
|
|
+ guard !DesktopWidgetWindowManager.shared.hasInstance(appID: app.id, variantID: variant.id) else {
|
|
|
+ return
|
|
|
+ }
|
|
|
ensureInWidgetLibrary(appID: app.id)
|
|
|
let instance = WidgetInstance(
|
|
|
appID: app.id,
|
|
|
@@ -200,24 +209,52 @@ struct WidgetsRootView: View {
|
|
|
DesktopWidgetWindowManager.shared.show(instance: instance, appProvider: { id in
|
|
|
apps.first { $0.id == id }
|
|
|
})
|
|
|
- showAddedToast(for: app)
|
|
|
+ showAddedToast(for: app, variant: variant)
|
|
|
}
|
|
|
|
|
|
- private func handleAddWidgetTap(app: LauncherApp, variant: WidgetVariant) {
|
|
|
- guard premiumStore.isPremiumUnlocked else {
|
|
|
- showingPremiumScreen = true
|
|
|
- return
|
|
|
+ private func isWidgetAdded(appID: UUID, variantID: String) -> Bool {
|
|
|
+ DesktopWidgetWindowManager.shared.hasInstance(appID: appID, variantID: variantID)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func handleWidgetTap(app: LauncherApp, variant: WidgetVariant) {
|
|
|
+ if isWidgetAdded(appID: app.id, variantID: variant.id) {
|
|
|
+ removeFromDesktop(app: app, variant: variant)
|
|
|
+ } else {
|
|
|
+ guard premiumStore.isPremiumUnlocked else {
|
|
|
+ showingPremiumScreen = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ addToDesktop(app: app, variant: variant)
|
|
|
}
|
|
|
- addToDesktop(app: app, variant: variant)
|
|
|
}
|
|
|
|
|
|
- private func showAddedToast(for app: LauncherApp) {
|
|
|
+ private func removeFromDesktop(app: LauncherApp, variant: WidgetVariant) {
|
|
|
+ let removedCount = DesktopWidgetWindowManager.shared.removeInstances(appID: app.id, variantID: variant.id)
|
|
|
+ guard removedCount > 0 else { return }
|
|
|
+ showRemovedToast(for: app)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func showAddedToast(for app: LauncherApp, variant: WidgetVariant) {
|
|
|
toastTask?.cancel()
|
|
|
- toastMessage = "The \"\(app.name)\" widget has been added to the desktop."
|
|
|
+ toastKind = .added
|
|
|
+ toastMessage = "The \"\(app.name)\" \"\(variant.title)\" widget has been added to the desktop."
|
|
|
toastTask = Task { @MainActor in
|
|
|
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
|
|
guard !Task.isCancelled else { return }
|
|
|
toastMessage = nil
|
|
|
+ toastKind = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func showRemovedToast(for app: LauncherApp) {
|
|
|
+ toastTask?.cancel()
|
|
|
+ toastKind = .removed
|
|
|
+ toastMessage = "The \"\(app.name)\" widget has been removed from the desktop."
|
|
|
+ toastTask = Task { @MainActor in
|
|
|
+ try? await Task.sleep(nanoseconds: 2_000_000_000)
|
|
|
+ guard !Task.isCancelled else { return }
|
|
|
+ toastMessage = nil
|
|
|
+ toastKind = nil
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -264,42 +301,68 @@ private struct WidgetSidebarRow: View {
|
|
|
private struct WidgetPreviewAddCard: View {
|
|
|
let app: LauncherApp
|
|
|
let variant: WidgetVariant
|
|
|
+ let isAdded: Bool
|
|
|
let isPremiumUnlocked: Bool
|
|
|
- let onAdd: () -> Void
|
|
|
+ let onToggle: () -> Void
|
|
|
|
|
|
@State private var hovering = false
|
|
|
|
|
|
var body: some View {
|
|
|
- WidgetPreviewCard(app: app, variant: variant)
|
|
|
- .overlay(alignment: .topTrailing) {
|
|
|
- Button(action: onAdd) {
|
|
|
- Image(systemName: "plus")
|
|
|
- .font(.system(size: 12, weight: .bold))
|
|
|
- .foregroundStyle(isPremiumUnlocked ? .black.opacity(0.85) : .white.opacity(0.82))
|
|
|
- .frame(width: 26, height: 26)
|
|
|
- .background(
|
|
|
- Circle()
|
|
|
- .fill(isPremiumUnlocked ? Color.green.opacity(0.95) : Color.gray.opacity(0.7))
|
|
|
- )
|
|
|
- .shadow(color: .black.opacity(0.28), radius: 10, x: 0, y: 6)
|
|
|
+ Button(action: onToggle) {
|
|
|
+ WidgetPreviewCard(app: app, variant: variant)
|
|
|
+ .contentShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
|
|
|
+ .overlay(alignment: .topTrailing) {
|
|
|
+ ZStack {
|
|
|
+ Image(systemName: isAdded ? "minus" : "plus")
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
+ .foregroundStyle(iconForegroundStyle)
|
|
|
+ .frame(width: 26, height: 26)
|
|
|
+ .background(
|
|
|
+ Circle()
|
|
|
+ .fill(iconBackgroundFill)
|
|
|
+ )
|
|
|
+ .shadow(color: .black.opacity(0.28), radius: 10, x: 0, y: 6)
|
|
|
+ }
|
|
|
+ .offset(x: 10, y: -10)
|
|
|
+ .allowsHitTesting(false)
|
|
|
+ .opacity(isAdded ? 1 : (hovering ? 1 : 0))
|
|
|
+ .animation(.easeOut(duration: 0.12), value: hovering)
|
|
|
}
|
|
|
- .buttonStyle(.plain)
|
|
|
- .offset(x: 10, y: -10)
|
|
|
- .opacity(hovering ? 1 : 0)
|
|
|
- .animation(.easeOut(duration: 0.12), value: hovering)
|
|
|
- }
|
|
|
- .onHover { hovering = $0 }
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .onHover { hovering = $0 }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var iconForegroundStyle: some ShapeStyle {
|
|
|
+ if isAdded { return .white.opacity(0.92) }
|
|
|
+ return isPremiumUnlocked ? .black.opacity(0.85) : .white.opacity(0.82)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var iconBackgroundFill: Color {
|
|
|
+ if isAdded { return Color.red.opacity(0.92) }
|
|
|
+ return isPremiumUnlocked ? Color.green.opacity(0.95) : Color.gray.opacity(0.7)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-private struct AddedToDesktopToast: View {
|
|
|
+private struct DesktopWidgetToast: View {
|
|
|
let message: String
|
|
|
+ let kind: WidgetsRootView.ToastKind
|
|
|
+
|
|
|
+ private var iconAndTint: (String, Color) {
|
|
|
+ switch kind {
|
|
|
+ case .added:
|
|
|
+ return ("checkmark.circle", Color.green.opacity(0.92))
|
|
|
+ case .removed:
|
|
|
+ return ("minus.circle", Color.red.opacity(0.92))
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
var body: some View {
|
|
|
+ let (systemImage, tint) = iconAndTint
|
|
|
HStack(spacing: 12) {
|
|
|
- Image(systemName: "checkmark.circle")
|
|
|
+ Image(systemName: systemImage)
|
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
- .foregroundStyle(Color.green.opacity(0.92))
|
|
|
+ .foregroundStyle(tint)
|
|
|
|
|
|
Text(message)
|
|
|
.font(.system(size: 14, weight: .semibold))
|