Selaa lähdekoodia

Improve Gmail desktop widget shortcuts and URL handling.

Expand Gmail widget variants with triage-focused actions (unread, search,
categories, attachments, contacts, settings). Resolve path and fragment
separately so Gmail hash URLs open correctly; substitute {{u}} for the
primary account slot. Open mailto links in the default mail client.

Raise the large widget panel and preview height for Gmail’s expanded grid,
and cap extra actions to Gmail-only so other apps stay unchanged.

Made-with: Cursor
huzaifahayat12 3 kuukautta sitten
vanhempi
sitoutus
d8b1bc60ce

+ 30 - 9
google_apps/Widgets/DesktopWidgetView.swift

@@ -1,9 +1,12 @@
 import SwiftUI
+import AppKit
 
 struct DesktopWidgetView: View {
     let app: LauncherApp
     let variant: WidgetVariant
     var isPreview: Bool = false
+    /// Gmail: account slot for `/mail/u/{{u}}/…` in URLs. Ignored for other apps.
+    var gmailAccountSlot: Int = 0
 
     private var actions: [WidgetAction] { variant.actions }
     private var layoutMode: WidgetLayoutMode { variant.layoutMode }
@@ -94,7 +97,9 @@ struct DesktopWidgetView: View {
         switch size {
         case .small: return 3
         case .medium: return 4
-        case .large: return 8
+        case .large:
+            if WidgetAppProfile.from(app: app) == .gmail { return 15 }
+            return 8
         }
     }
 
@@ -184,21 +189,37 @@ struct DesktopWidgetView: View {
 
     private func open(action: WidgetAction) {
         guard !isPreview, let base = app.webURL else { return }
-        let target = resolvedURL(base: base, actionPath: action.urlPath)
-        InAppBrowserWindowManager.shared.open(url: target, title: app.name)
+        let target = resolvedURL(base: base, actionPath: action.urlPath, accountSlot: gmailAccountSlot)
+        if target.scheme?.lowercased() == "mailto" {
+            NSWorkspace.shared.open(target)
+        } else {
+            InAppBrowserWindowManager.shared.open(url: target, title: app.name)
+        }
     }
 
-    private func resolvedURL(base: URL, actionPath: String?) -> URL {
+    private func resolvedURL(base: URL, actionPath: String?, accountSlot: Int) -> URL {
         guard let actionPath, !actionPath.isEmpty else { return base }
-        if actionPath.hasPrefix("http://") || actionPath.hasPrefix("https://") {
-            return URL(string: actionPath) ?? base
+        let substituted = actionPath.replacingOccurrences(of: "{{u}}", with: "\(accountSlot)")
+        if substituted.hasPrefix("http://") || substituted.hasPrefix("https://") {
+            return URL(string: substituted) ?? base
         }
-        if actionPath.hasPrefix("/") {
+        if substituted.lowercased().hasPrefix("mailto:") {
+            return URL(string: substituted) ?? base
+        }
+        if substituted.hasPrefix("/") {
             var c = URLComponents(url: base, resolvingAgainstBaseURL: false)
-            c?.path = actionPath
+            if let hashIdx = substituted.firstIndex(of: "#") {
+                let pathPart = String(substituted[..<hashIdx])
+                let fragPart = String(substituted[substituted.index(after: hashIdx)...])
+                c?.path = pathPart
+                c?.fragment = fragPart.isEmpty ? nil : String(fragPart)
+            } else {
+                c?.path = substituted
+                c?.fragment = nil
+            }
             return c?.url ?? base
         }
-        return URL(string: actionPath, relativeTo: base)?.absoluteURL ?? base
+        return URL(string: substituted, relativeTo: base)?.absoluteURL ?? base
     }
 
     private func isPrimary(_ action: WidgetAction) -> Bool {

+ 1 - 3
google_apps/Widgets/DesktopWidgetWindowManager.swift

@@ -127,7 +127,6 @@ final class DesktopWidgetWindowManager: ObservableObject {
         let root = DesktopWidgetHostView(
             app: app,
             variant: variant,
-            instanceID: instance.id,
             onRemove: { self.close(instanceID: instance.id) }
         )
 
@@ -168,7 +167,7 @@ final class DesktopWidgetWindowManager: ObservableObject {
         case .medium:
             return CGSize(width: 390, height: 210)
         case .large:
-            return CGSize(width: 390, height: 300)
+            return CGSize(width: 390, height: 400)
         }
     }
 
@@ -336,7 +335,6 @@ private final class DesktopWidgetPanel: NSPanel {
 private struct DesktopWidgetHostView: View {
     let app: LauncherApp
     let variant: WidgetVariant
-    let instanceID: UUID
     let onRemove: () -> Void
 
     var body: some View {

+ 1 - 1
google_apps/Widgets/WidgetPreviewCard.swift

@@ -21,7 +21,7 @@ struct WidgetPreviewCard: View {
         case .medium:
             return CGSize(width: 340, height: 190)
         case .large:
-            return CGSize(width: 340, height: 260)
+            return CGSize(width: 340, height: 360)
         }
     }
 }

+ 30 - 21
google_apps/Widgets/WidgetTemplates.swift

@@ -54,27 +54,36 @@ enum WidgetTemplates {
                 ]),
             ]
         case .gmail:
-            return [
-                v("gmail_small", "Inbox Quick", .small, false, .actionsRow, [
-                    a("compose", "Compose", "square.and.pencil", "/mail/u/0/#inbox?compose=new"),
-                    a("inbox", "Inbox", "tray", "/mail/u/0/#inbox"),
-                    a("starred", "Starred", "star", "/mail/u/0/#starred"),
-                ]),
-                v("gmail_medium", "Mailbox", .medium, true, .actionsGrid(columns: 2), [
-                    a("inbox", "Inbox", "tray", "/mail/u/0/#inbox"),
-                    a("important", "Important", "exclamationmark.circle", "/mail/u/0/#imp"),
-                    a("sent", "Sent", "paperplane", "/mail/u/0/#sent"),
-                    a("drafts", "Drafts", "doc.plaintext", "/mail/u/0/#drafts"),
-                ]),
-                v("gmail_large", "Gmail Actions", .large, true, .actionsGrid(columns: 2), [
-                    a("compose", "Compose", "square.and.pencil", "/mail/u/0/#inbox?compose=new"),
-                    a("inbox", "Inbox", "tray", "/mail/u/0/#inbox"),
-                    a("starred", "Starred", "star", "/mail/u/0/#starred"),
-                    a("sent", "Sent", "paperplane", "/mail/u/0/#sent"),
-                    a("drafts", "Drafts", "doc.plaintext", "/mail/u/0/#drafts"),
-                    a("snoozed", "Snoozed", "clock", "/mail/u/0/#snoozed"),
-                    a("important", "Important", "exclamationmark.circle", "/mail/u/0/#imp"),
-                    a("trash", "Trash", "trash", "/mail/u/0/#trash"),
+            // Paths use `{{u}}` for the signed-in account slot (0…3); resolved on the desktop widget.
+            func gm(_ path: String) -> String { path }
+            return [
+                v("gmail_small", "Quick triage", .small, false, .actionsRow, [
+                    a("compose", "Compose", "square.and.pencil", gm("/mail/u/{{u}}/#inbox?compose=new")),
+                    a("unread", "Unread", "envelope.badge", gm("/mail/u/{{u}}/#search/is:unread")),
+                    a("search", "Search", "magnifyingglass", gm("/mail/u/{{u}}/#advanced-search")),
+                ]),
+                v("gmail_medium", "Inbox & search", .medium, true, .actionsGrid(columns: 2), [
+                    a("inbox", "Inbox", "tray", gm("/mail/u/{{u}}/#inbox")),
+                    a("unread", "Unread", "envelope.badge", gm("/mail/u/{{u}}/#search/is:unread")),
+                    a("search", "Search", "magnifyingglass", gm("/mail/u/{{u}}/#search")),
+                    a("compose", "Compose", "square.and.pencil", gm("/mail/u/{{u}}/#inbox?compose=new")),
+                ]),
+                v("gmail_large", "Gmail hub", .large, true, .actionsGrid(columns: 2), [
+                    a("compose", "Compose", "square.and.pencil", gm("/mail/u/{{u}}/#inbox?compose=new")),
+                    a("inbox", "Inbox", "tray", gm("/mail/u/{{u}}/#inbox")),
+                    a("unread", "Unread", "envelope.badge", gm("/mail/u/{{u}}/#search/is:unread")),
+                    a("search", "Search", "magnifyingglass", gm("/mail/u/{{u}}/#search")),
+                    a("starred", "Starred", "star", gm("/mail/u/{{u}}/#starred")),
+                    a("drafts", "Drafts", "doc.plaintext", gm("/mail/u/{{u}}/#drafts")),
+                    a("sent", "Sent", "paperplane", gm("/mail/u/{{u}}/#sent")),
+                    a("spam", "Spam", "exclamationmark.triangle", gm("/mail/u/{{u}}/#spam")),
+                    a("allmail", "All Mail", "archivebox", gm("/mail/u/{{u}}/#all")),
+                    a("scheduled", "Scheduled", "calendar.badge.clock", gm("/mail/u/{{u}}/#scheduled")),
+                    a("attachments", "Attachments", "paperclip", gm("/mail/u/{{u}}/#search/has:attachment")),
+                    a("contacts", "Contacts", "person.crop.circle", "https://contacts.google.com/"),
+                    a("promos", "Promotions", "tag", gm("/mail/u/{{u}}/#category/promotions")),
+                    a("settings", "Settings", "gearshape", gm("/mail/u/{{u}}/#settings/general")),
+                    a("trash", "Trash", "trash", gm("/mail/u/{{u}}/#trash")),
                 ]),
             ]
         case .drive: