Browse Source

Stabilize desktop widgets interactions and notifications

Fixes widget window interaction/crash behavior, restores drag and context-menu removal, refines desktop widget styling, and adds a compact success toast when widgets are added to desktop.

Made-with: Cursor
huzaifahayat12 3 tháng trước cách đây
mục cha
commit
1246d8a730

+ 31 - 13
google_apps/Widgets/DesktopWidgetView.swift

@@ -13,11 +13,17 @@ struct DesktopWidgetView: View {
     var body: some View {
         ZStack {
             RoundedRectangle(cornerRadius: 22, style: .continuous)
-                .fill(Color.white)
-                .shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 7)
+                .fill(
+                    LinearGradient(
+                        colors: [Color(red: 0.18, green: 0.24, blue: 0.31), Color(red: 0.18, green: 0.47, blue: 0.46)],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+                .shadow(color: .black.opacity(0.25), radius: 14, x: 0, y: 8)
                 .overlay(
                     RoundedRectangle(cornerRadius: 22, style: .continuous)
-                        .stroke(Color.black.opacity(0.10), lineWidth: 1)
+                        .stroke(Color.white.opacity(0.12), lineWidth: 1)
                 )
 
             VStack(alignment: .leading, spacing: 12) {
@@ -49,7 +55,7 @@ struct DesktopWidgetView: View {
             VStack(alignment: .leading, spacing: 2) {
                 Text(app.name)
                     .font(.system(size: 16, weight: .bold))
-                    .foregroundStyle(.black.opacity(0.88))
+                    .foregroundStyle(.white.opacity(0.92))
                     .lineLimit(1)
             }
             Spacer(minLength: 0)
@@ -70,7 +76,7 @@ struct DesktopWidgetView: View {
             AppIconView(app: app, size: 44, showAppBackground: false, iconPaddingFactor: 0.05)
             Text(title)
                 .font(.system(size: fontSize, weight: .bold))
-                .foregroundStyle(.black.opacity(0.88))
+                .foregroundStyle(.white.opacity(0.92))
                 .lineLimit(1)
         }
 
@@ -115,21 +121,21 @@ struct DesktopWidgetView: View {
             HStack(spacing: 8) {
                 Image(systemName: action.systemImage)
                     .font(.system(size: 12, weight: .semibold))
-                    .foregroundStyle(.black.opacity(0.62))
+                    .foregroundStyle(foregroundColor(for: action))
                 Text(action.title)
                     .font(.system(size: 12, weight: .bold))
-                    .foregroundStyle(.black.opacity(0.86))
+                    .foregroundStyle(foregroundColor(for: action))
                     .lineLimit(1)
             }
             .padding(.horizontal, 12)
             .padding(.vertical, 9)
             .background(
                 RoundedRectangle(cornerRadius: 999, style: .continuous)
-                    .fill(Color.white)
+                    .fill(backgroundColor(for: action))
             )
             .overlay(
                 RoundedRectangle(cornerRadius: 999, style: .continuous)
-                    .stroke(Color.black.opacity(0.12), lineWidth: 1)
+                    .stroke(Color.white.opacity(isPrimary(action) ? 0.22 : 0.12), lineWidth: 1)
             )
         }
         .buttonStyle(.plain)
@@ -142,10 +148,10 @@ struct DesktopWidgetView: View {
             HStack(spacing: 10) {
                 Image(systemName: action.systemImage)
                     .font(.system(size: 12, weight: .semibold))
-                    .foregroundStyle(.black.opacity(0.62))
+                    .foregroundStyle(foregroundColor(for: action))
                 Text(action.title)
                     .font(.system(size: 12, weight: .bold))
-                    .foregroundStyle(.black.opacity(0.86))
+                    .foregroundStyle(foregroundColor(for: action))
                     .lineLimit(1)
                 Spacer(minLength: 0)
             }
@@ -153,11 +159,11 @@ struct DesktopWidgetView: View {
             .padding(.vertical, 10)
             .background(
                 RoundedRectangle(cornerRadius: 14, style: .continuous)
-                    .fill(Color.white)
+                    .fill(backgroundColor(for: action))
             )
             .overlay(
                 RoundedRectangle(cornerRadius: 14, style: .continuous)
-                    .stroke(Color.black.opacity(0.12), lineWidth: 1)
+                    .stroke(Color.white.opacity(isPrimary(action) ? 0.22 : 0.12), lineWidth: 1)
             )
         }
         .buttonStyle(.plain)
@@ -188,5 +194,17 @@ struct DesktopWidgetView: View {
         }
         return URL(string: actionPath, relativeTo: base)?.absoluteURL ?? base
     }
+
+    private func isPrimary(_ action: WidgetAction) -> Bool {
+        actions.first?.id == action.id
+    }
+
+    private func backgroundColor(for action: WidgetAction) -> Color {
+        isPrimary(action) ? Color.white.opacity(0.38) : Color.black.opacity(0.66)
+    }
+
+    private func foregroundColor(for action: WidgetAction) -> Color {
+        isPrimary(action) ? Color.white.opacity(0.95) : Color.white.opacity(0.88)
+    }
 }
 

+ 20 - 37
google_apps/Widgets/DesktopWidgetWindowManager.swift

@@ -28,7 +28,7 @@ final class DesktopWidgetWindowManager {
             panel.center()
         }
 
-        panel.makeKeyAndOrderFront(nil)
+        panel.orderFront(nil)
     }
 
     func restore(appProvider: (UUID) -> LauncherApp?) {
@@ -59,28 +59,31 @@ final class DesktopWidgetWindowManager {
         let size = panelSize(for: variant.size)
         let panel = NSPanel(
             contentRect: NSRect(x: 0, y: 0, width: size.width, height: size.height),
-            styleMask: [.nonactivatingPanel, .titled, .fullSizeContentView],
+            styleMask: [.nonactivatingPanel, .borderless],
             backing: .buffered,
             defer: false
         )
 
-        panel.isFloatingPanel = true
-        panel.level = .floating
-        panel.titleVisibility = .hidden
-        panel.titlebarAppearsTransparent = true
+        panel.isReleasedWhenClosed = false
+        panel.isFloatingPanel = false
+        panel.becomesKeyOnlyIfNeeded = true
+        panel.isMovable = true
+        panel.level = .normal
+        panel.hasShadow = true
         panel.isOpaque = false
         panel.backgroundColor = .clear
         panel.isMovableByWindowBackground = true
         panel.hidesOnDeactivate = false
         panel.collectionBehavior = [
             .canJoinAllSpaces,
-            .fullScreenAuxiliary,
-            .transient,
         ]
 
-        let root = DesktopWidgetHostView(app: app, variant: variant, instanceID: instance.id) {
-            self.close(instanceID: instance.id)
-        }
+        let root = DesktopWidgetHostView(
+            app: app,
+            variant: variant,
+            instanceID: instance.id,
+            onRemove: { self.close(instanceID: instance.id) }
+        )
 
         let hosting = NSHostingView(rootView: root)
         hosting.translatesAutoresizingMaskIntoConstraints = false
@@ -197,35 +200,15 @@ private struct DesktopWidgetHostView: View {
     let app: LauncherApp
     let variant: WidgetVariant
     let instanceID: UUID
-    let onClose: () -> Void
-
-    @State private var hovering = false
+    let onRemove: () -> Void
 
     var body: some View {
-        ZStack(alignment: .topTrailing) {
-            DesktopWidgetView(app: app, variant: variant, isPreview: false)
-
-            Button(action: onClose) {
-                Image(systemName: "xmark")
-                    .font(.system(size: 11, weight: .bold))
-                    .foregroundStyle(.white.opacity(0.9))
-                    .frame(width: 26, height: 26)
-                    .background(
-                        Circle()
-                            .fill(Color.black.opacity(hovering ? 0.35 : 0.2))
-                    )
-                    .overlay(
-                        Circle()
-                            .stroke(Color.white.opacity(hovering ? 0.16 : 0.10), lineWidth: 1)
-                    )
-                    .shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 6)
+        DesktopWidgetView(app: app, variant: variant, isPreview: false)
+            .contextMenu {
+                Button("Remove Widget", role: .destructive) {
+                    onRemove()
+                }
             }
-            .buttonStyle(.plain)
-            .padding(8)
-            .opacity(hovering ? 1 : 0.0)
-            .animation(.easeOut(duration: 0.12), value: hovering)
-        }
-        .onHover { hovering = $0 }
     }
 }
 

+ 3 - 3
google_apps/Widgets/WidgetTemplates.swift

@@ -124,11 +124,11 @@ enum WidgetTemplates {
                 ]),
                 v("youtube_large", "Library", .large, true, .actionsGrid(columns: 2), [
                     a("home", "Home", "house", "/"),
+                    a("shorts", "Shorts", "play.square", "/shorts"),
                     a("subscriptions", "Subscriptions", "play.rectangle", "/feed/subscriptions"),
-                    a("library", "Library", "books.vertical", "/feed/library"),
                     a("history", "History", "clock", "/feed/history"),
-                    a("watchLater", "Watch Later", "bookmark", "/playlist?list=WL"),
-                    a("shorts", "Shorts", "bolt", "/shorts"),
+                    a("playlists", "Playlists", "list.bullet.rectangle", "/feed/playlists"),
+                    a("liked", "Liked videos", "hand.thumbsup", "/playlist?list=LL"),
                 ]),
             ]
         case .translate:

+ 52 - 0
google_apps/Widgets/WidgetsRootView.swift

@@ -11,6 +11,8 @@ struct WidgetsRootView: View {
     @Binding var widgetLibraryIDsData: String
 
     @State private var query = ""
+    @State private var toastMessage: String?
+    @State private var toastTask: Task<Void, Never>?
 
     private var filteredApps: [LauncherApp] {
         let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -41,6 +43,14 @@ struct WidgetsRootView: View {
         }
         .padding(.horizontal, 10)
         .padding(.bottom, 12)
+        .overlay(alignment: .top) {
+            if let toastMessage {
+                AddedToDesktopToast(message: toastMessage)
+                    .padding(.top, 6)
+                    .transition(.move(edge: .top).combined(with: .opacity))
+            }
+        }
+        .animation(.spring(response: 0.28, dampingFraction: 0.9), value: toastMessage != nil)
     }
 
     private var sidebar: some View {
@@ -182,6 +192,17 @@ struct WidgetsRootView: View {
         DesktopWidgetWindowManager.shared.show(instance: instance, appProvider: { id in
             apps.first { $0.id == id }
         })
+        showAddedToast(for: app)
+    }
+
+    private func showAddedToast(for app: LauncherApp) {
+        toastTask?.cancel()
+        toastMessage = "The \"\(app.name)\" 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
+        }
     }
 }
 
@@ -251,6 +272,37 @@ private struct WidgetPreviewAddCard: View {
     }
 }
 
+private struct AddedToDesktopToast: View {
+    let message: String
+
+    var body: some View {
+        HStack(spacing: 12) {
+            Image(systemName: "checkmark.circle")
+                .font(.system(size: 18, weight: .semibold))
+                .foregroundStyle(Color.green.opacity(0.92))
+
+            Text(message)
+                .font(.system(size: 14, weight: .semibold))
+                .foregroundStyle(.white.opacity(0.95))
+                .lineLimit(1)
+                .multilineTextAlignment(.leading)
+
+            Spacer(minLength: 0)
+        }
+        .padding(.horizontal, 14)
+        .padding(.vertical, 10)
+        .frame(maxWidth: 560)
+        .background(
+            RoundedRectangle(cornerRadius: 16, style: .continuous)
+                .fill(Color.black.opacity(0.82))
+        )
+        .overlay(
+            RoundedRectangle(cornerRadius: 16, style: .continuous)
+                .stroke(Color.white.opacity(0.08), lineWidth: 1)
+        )
+    }
+}
+
 private func decodeUUIDSet(from dataString: String) -> Set<UUID> {
     guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] }
     do {