Преглед изворни кода

Redesign Gmail large widget as native dashboard.

Replace the embedded webview with a polished action-first layout, keep Gmail deep-link functionality through in-app browser actions, and tune large widget sizing for a cleaner desktop presentation.

Made-with: Cursor
huzaifahayat12 пре 3 месеци
родитељ
комит
2f64f1101c
2 измењених фајлова са 162 додато и 180 уклоњено
  1. 160 178
      google_apps/Widgets/GmailDesktopWidgetView.swift
  2. 2 2
      google_apps/Widgets/WidgetTemplates.swift

+ 160 - 178
google_apps/Widgets/GmailDesktopWidgetView.swift

@@ -1,6 +1,5 @@
 import AppKit
 import SwiftUI
-import WebKit
 
 // MARK: - Public SwiftUI view
 
@@ -85,20 +84,6 @@ struct GmailDesktopWidgetView: View {
     // MARK: - Large (interactive)
 
     @State private var searchText = ""
-    @State private var webURL: URL
-
-    init(app: LauncherApp, mode: GmailWidgetMode, gmailAccountSlot: Int = 0, isPreview: Bool = false) {
-        self.app = app
-        self.mode = mode
-        self.gmailAccountSlot = gmailAccountSlot
-        self.isPreview = isPreview
-        let base = app.webURL ?? URL(string: "https://mail.google.com")!
-        _webURL = State(initialValue: Self.defaultInboxURL(base: base, slot: gmailAccountSlot))
-    }
-
-    private static func defaultInboxURL(base: URL, slot: Int) -> URL {
-        WidgetDeepLinkURL.resolved(base: base, actionPath: "/mail/u/{{u}}/#inbox", accountSlot: slot)
-    }
 
     private var interactiveBody: some View {
         Group {
@@ -120,12 +105,12 @@ struct GmailDesktopWidgetView: View {
                         endPoint: .bottomTrailing
                     )
                 )
-            VStack(spacing: 12) {
+            VStack(spacing: 10) {
                 AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
-                Text("Gmail inbox")
+                Text("Gmail command center")
                     .font(.system(size: 15, weight: .bold))
                     .foregroundStyle(.white.opacity(0.92))
-                Text("Full inbox, search, and folder shortcuts load on your desktop.")
+                Text("Fast actions, inbox shortcuts, and clean navigation built for desktop.")
                     .font(.system(size: 11, weight: .medium))
                     .foregroundStyle(.white.opacity(0.65))
                     .multilineTextAlignment(.center)
@@ -137,32 +122,22 @@ struct GmailDesktopWidgetView: View {
 
     private var interactiveLiveChrome: some View {
         ZStack {
-            RoundedRectangle(cornerRadius: 22, style: .continuous)
-                .fill(Color.black.opacity(0.35))
-
-            VStack(spacing: 0) {
-                VStack(spacing: 8) {
-                    interactiveHeader
-                    interactiveSearchBar
-                    smartShortcutsRow
-                    folderShortcutsRow
-                }
-                .padding(.horizontal, 6)
-                .padding(.vertical, 8)
-                .background(
-                    RoundedRectangle(cornerRadius: 16, style: .continuous)
-                        .fill(Color(red: 0.07, green: 0.08, blue: 0.1))
+            gmailGradientBackground
+                .overlay(
+                    RoundedRectangle(cornerRadius: 22, style: .continuous)
+                        .fill(Color.black.opacity(0.38))
                 )
-                .zIndex(20)
-                .allowsHitTesting(true)
-
-                GmailWebWidgetView(url: $webURL)
-                    .frame(minHeight: 280)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity)
-                    .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
-                    .zIndex(0)
+
+            VStack(alignment: .leading, spacing: 12) {
+                interactiveHeader
+                interactiveSearchBar
+                quickActionGrid
+                mailboxRow
+                categoryRow
+                footerRow
+                Spacer(minLength: 0)
             }
-            .padding(10)
+            .padding(14)
         }
     }
 
@@ -178,10 +153,10 @@ struct GmailDesktopWidgetView: View {
                 openMailInBrowser()
             }) {
                 Image(systemName: "envelope.open")
-                    .font(.system(size: 13, weight: .semibold))
-                    .foregroundStyle(.white.opacity(0.9))
-                    .padding(8)
-                    .background(Circle().fill(Color.white.opacity(0.12)))
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.92))
+                    .frame(width: 34, height: 34)
+                    .background(Circle().fill(Color.white.opacity(0.14)))
             }
             .buttonStyle(.plain)
             .help("Open Mail in app browser")
@@ -190,10 +165,10 @@ struct GmailDesktopWidgetView: View {
                 openCompose()
             }) {
                 Image(systemName: "square.and.pencil")
-                    .font(.system(size: 13, weight: .semibold))
-                    .foregroundStyle(.white.opacity(0.9))
-                    .padding(8)
-                    .background(Circle().fill(Color.white.opacity(0.12)))
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.92))
+                    .frame(width: 34, height: 34)
+                    .background(Circle().fill(Color.white.opacity(0.14)))
             }
             .buttonStyle(.plain)
             .help("Compose")
@@ -210,59 +185,160 @@ struct GmailDesktopWidgetView: View {
                 .font(.system(size: 12, weight: .medium))
                 .foregroundStyle(.white.opacity(0.95))
                 .onSubmit { submitSearch() }
-            Button("Go") {
+            Button("Search") {
                 submitSearch()
             }
-            .font(.system(size: 11, weight: .bold))
-            .foregroundStyle(Color(red: 0.95, green: 0.45, blue: 0.35))
+            .font(.system(size: 10, weight: .bold))
+            .foregroundStyle(Color(red: 0.97, green: 0.62, blue: 0.53))
             .buttonStyle(.plain)
         }
         .padding(.horizontal, 10)
         .padding(.vertical, 8)
         .background(
             RoundedRectangle(cornerRadius: 10, style: .continuous)
-                .fill(Color.white.opacity(0.08))
+                .fill(Color.black.opacity(0.32))
+        )
+        .overlay(
+            RoundedRectangle(cornerRadius: 10, style: .continuous)
+                .stroke(Color.white.opacity(0.12), lineWidth: 1)
         )
     }
 
-    private var smartShortcutsRow: some View {
-        ScrollView(.horizontal, showsIndicators: false) {
-            HStack(spacing: 8) {
-                shortcutChip("Primary", fragment: "#category/primary")
-                shortcutChip("Social", fragment: "#category/social")
-                shortcutChip("Promotions", fragment: "#category/promotions")
-                shortcutChip("Updates", fragment: "#category/updates")
-                shortcutChip("Forums", fragment: "#category/forums")
+    private var quickActionGrid: some View {
+        LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 0), spacing: 8), count: 2), spacing: 8) {
+            quickActionCard(
+                title: "Unread",
+                subtitle: unreadCountStored < 0 ? "Syncing" : "\(unreadCountStored) messages",
+                systemImage: "envelope.badge.fill"
+            ) {
+                openUnreadSearch()
+            }
+            quickActionCard(
+                title: "Compose",
+                subtitle: "Start a new message",
+                systemImage: "square.and.pencil"
+            ) {
+                openCompose()
+            }
+            quickActionCard(
+                title: "Starred",
+                subtitle: "Important mail",
+                systemImage: "star.fill"
+            ) {
+                openMailbox("#starred")
+            }
+            quickActionCard(
+                title: "Drafts",
+                subtitle: "Continue writing",
+                systemImage: "doc.text.fill"
+            ) {
+                openMailbox("#drafts")
+            }
+        }
+    }
+
+    private var mailboxRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Mailboxes")
+                .font(.system(size: 10, weight: .semibold))
+                .foregroundStyle(.white.opacity(0.62))
+            ScrollView(.horizontal, showsIndicators: false) {
+                HStack(spacing: 8) {
+                    shortcutChip("Inbox", fragment: "#inbox")
+                    shortcutChip("Sent", fragment: "#sent")
+                    shortcutChip("Snoozed", fragment: "#snoozed")
+                    shortcutChip("All Mail", fragment: "#all")
+                    shortcutChip("Spam", fragment: "#spam")
+                    shortcutChip("Trash", fragment: "#trash")
+                }
+            }
+        }
+    }
+
+    private var categoryRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Categories")
+                .font(.system(size: 10, weight: .semibold))
+                .foregroundStyle(.white.opacity(0.62))
+            ScrollView(.horizontal, showsIndicators: false) {
+                HStack(spacing: 8) {
+                    shortcutChip("Primary", fragment: "#category/primary")
+                    shortcutChip("Social", fragment: "#category/social")
+                    shortcutChip("Promotions", fragment: "#category/promotions")
+                    shortcutChip("Updates", fragment: "#category/updates")
+                    shortcutChip("Forums", fragment: "#category/forums")
+                }
             }
         }
     }
 
-    private var folderShortcutsRow: some View {
-        ScrollView(.horizontal, showsIndicators: false) {
-            HStack(spacing: 8) {
-                shortcutChip("Inbox", fragment: "#inbox")
-                shortcutChip("Starred", fragment: "#starred")
-                shortcutChip("Snoozed", fragment: "#snoozed")
-                shortcutChip("Sent", fragment: "#sent")
-                shortcutChip("Drafts", fragment: "#drafts")
-                shortcutChip("All Mail", fragment: "#all")
-                shortcutChip("Spam", fragment: "#spam")
-                shortcutChip("Trash", fragment: "#trash")
+    private var footerRow: some View {
+        HStack(spacing: 8) {
+            Button(action: {
+                NSApp.activate(ignoringOtherApps: true)
+                openMailInBrowser()
+            }) {
+                Label("Open Full Gmail", systemImage: "arrow.up.right.square")
+                    .font(.system(size: 11, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.94))
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 10)
+                    .background(
+                        RoundedRectangle(cornerRadius: 12, style: .continuous)
+                            .fill(Color.black.opacity(0.35))
+                    )
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 12, style: .continuous)
+                            .stroke(Color.white.opacity(0.14), lineWidth: 1)
+                    )
             }
+            .buttonStyle(.plain)
         }
     }
 
+    private func quickActionCard(title: String, subtitle: String, systemImage: String, action: @escaping () -> Void) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            action()
+        }) {
+            VStack(alignment: .leading, spacing: 5) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.9))
+                Text(title)
+                    .font(.system(size: 12, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                    .lineLimit(1)
+                Text(subtitle)
+                    .font(.system(size: 10, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.7))
+                    .lineLimit(1)
+            }
+            .frame(maxWidth: .infinity, alignment: .leading)
+            .padding(10)
+            .background(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .fill(Color.black.opacity(0.3))
+            )
+            .overlay(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .stroke(Color.white.opacity(0.13), lineWidth: 1)
+            )
+        }
+        .buttonStyle(.plain)
+    }
+
     private func shortcutChip(_ title: String, fragment: String) -> some View {
         Button(action: {
             NSApp.activate(ignoringOtherApps: true)
-            navigateToMailPath("/mail/u/{{u}}/\(fragment)")
+            openMailbox(fragment)
         }) {
             Text(title)
                 .font(.system(size: 10, weight: .bold))
                 .foregroundStyle(.white.opacity(0.92))
                 .padding(.horizontal, 10)
                 .padding(.vertical, 6)
-                .background(Capsule().fill(Color.white.opacity(0.12)))
+                .background(Capsule().fill(Color.black.opacity(0.3)))
                 .overlay(Capsule().stroke(Color.white.opacity(0.14), lineWidth: 1))
         }
         .buttonStyle(.plain)
@@ -331,117 +407,23 @@ struct GmailDesktopWidgetView: View {
 
     private func openUnreadSearch() {
         guard !isPreview else { return }
-        let url = WidgetDeepLinkURL.resolved(
-            base: baseURL,
-            actionPath: "/mail/u/{{u}}/#search/is:unread",
-            accountSlot: gmailAccountSlot
-        )
-        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+        openPath("/mail/u/{{u}}/#search/is:unread")
     }
 
-    private func navigateToMailPath(_ path: String) {
+    private func openMailbox(_ fragment: String) {
+        guard !isPreview else { return }
+        openPath("/mail/u/{{u}}/\(fragment)")
+    }
+
+    private func openPath(_ path: String) {
         let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: gmailAccountSlot)
-        DispatchQueue.main.async {
-            webURL = url
-        }
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
     }
 
     private func submitSearch() {
         let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !q.isEmpty else { return }
         let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed) ?? q
-        navigateToMailPath("/mail/u/{{u}}/#search/\(encoded)")
-    }
-}
-
-// MARK: - WKWebView
-
-private final class GmailWebViewClippingContainer: NSView {
-    override var isFlipped: Bool { true }
-}
-
-private struct GmailWebWidgetView: NSViewRepresentable {
-    @Binding var url: URL
-
-    func makeCoordinator() -> Coordinator {
-        Coordinator()
-    }
-
-    func makeNSView(context: Context) -> NSView {
-        let container = GmailWebViewClippingContainer()
-        container.wantsLayer = true
-        container.layer?.masksToBounds = true
-        container.layer?.cornerRadius = 12
-        container.layer?.borderWidth = 0
-
-        let config = WKWebViewConfiguration()
-        config.defaultWebpagePreferences.allowsContentJavaScript = true
-        let wv = WKWebView(frame: .zero, configuration: config)
-        wv.translatesAutoresizingMaskIntoConstraints = false
-        wv.focusRingType = .none
-        wv.customUserAgent =
-            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
-        wv.navigationDelegate = context.coordinator
-
-        container.addSubview(wv)
-        NSLayoutConstraint.activate([
-            wv.leadingAnchor.constraint(equalTo: container.leadingAnchor),
-            wv.trailingAnchor.constraint(equalTo: container.trailingAnchor),
-            wv.topAnchor.constraint(equalTo: container.topAnchor),
-            wv.bottomAnchor.constraint(equalTo: container.bottomAnchor),
-        ])
-
-        context.coordinator.webView = wv
-        context.coordinator.lastLoadedURLString = url.absoluteString
-        wv.load(URLRequest(url: url))
-        DispatchQueue.main.async {
-            Self.stripWebKitOuterChrome(wv)
-        }
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
-            Self.stripWebKitOuterChrome(wv)
-        }
-        return container
-    }
-
-    func updateNSView(_ container: NSView, context: Context) {
-        guard let wv = context.coordinator.webView else { return }
-        let next = url.absoluteString
-        guard context.coordinator.lastLoadedURLString != next else { return }
-        context.coordinator.lastLoadedURLString = next
-        wv.load(URLRequest(url: url))
-    }
-
-    /// WebKit embeds an `NSScrollView` that often draws a 1pt dark border around the web area (visible on large widgets).
-    private static func stripWebKitOuterChrome(_ webView: WKWebView) {
-        webView.focusRingType = .none
-        func walk(_ view: NSView) {
-            view.focusRingType = .none
-            view.layer?.borderWidth = 0
-            view.layer?.borderColor = NSColor.clear.cgColor
-            if let scroll = view as? NSScrollView {
-                scroll.borderType = .noBorder
-                scroll.drawsBackground = false
-                scroll.scrollerStyle = .overlay
-            }
-            if let clip = view as? NSClipView {
-                clip.drawsBackground = false
-            }
-            for sub in view.subviews {
-                walk(sub)
-            }
-        }
-        walk(webView)
-    }
-
-    final class Coordinator: NSObject, WKNavigationDelegate {
-        weak var webView: WKWebView?
-        var lastLoadedURLString: String?
-
-        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
-            GmailWebWidgetView.stripWebKitOuterChrome(webView)
-            DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
-                GmailWebWidgetView.stripWebKitOuterChrome(webView)
-            }
-        }
+        openPath("/mail/u/{{u}}/#search/\(encoded)")
     }
 }

+ 2 - 2
google_apps/Widgets/WidgetTemplates.swift

@@ -593,7 +593,7 @@ extension WidgetLayoutMode {
         case .gmail(let mode):
             switch mode {
             case .compact: return CGSize(width: 204, height: 192)
-            case .interactive: return CGSize(width: 540, height: 580)
+            case .interactive: return CGSize(width: 500, height: 430)
             }
         case .drive(let mode):
             switch mode {
@@ -664,7 +664,7 @@ extension WidgetLayoutMode {
         case .gmail(let mode):
             switch mode {
             case .compact: return CGSize(width: 186, height: 176)
-            case .interactive: return CGSize(width: 480, height: 440)
+            case .interactive: return CGSize(width: 440, height: 360)
             }
         case .drive(let mode):
             switch mode {