Bläddra i källkod

Add Google Shopping interactive widget with category shortcuts.

Introduce a Travel-style Shopping desktop widget with typed search and category buttons, then wire a new shopping layout mode and size/key-capable behavior so search input works and large shopping panels open correctly.

Made-with: Cursor
huzaifahayat12 3 månader sedan
förälder
incheckning
4bc697aafe

+ 3 - 1
google_apps/Widgets/DesktopWidgetView.swift

@@ -33,6 +33,8 @@ struct DesktopWidgetView: View {
                 BooksDesktopWidgetView(app: app, mode: booksMode, isPreview: isPreview)
             } else if case .travel(let travelMode) = layoutMode {
                 TravelDesktopWidgetView(app: app, mode: travelMode, isPreview: isPreview)
+            } else if case .shopping(let shoppingMode) = layoutMode {
+                ShoppingDesktopWidgetView(app: app, mode: shoppingMode, isPreview: isPreview)
             } else if case .youtube(let youtubeMode) = layoutMode {
                 YouTubeDesktopWidgetView(app: app, mode: youtubeMode, isPreview: isPreview)
             } else if case .meet(let meetMode) = layoutMode {
@@ -86,7 +88,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .youtube, .meet, .news, .forms, .docs, .sheets, .slides, .googlePlay, .gemini:
+                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .shopping, .youtube, .meet, .news, .forms, .docs, .sheets, .slides, .googlePlay, .gemini:
                             EmptyView()
                         }
                     }

+ 217 - 0
google_apps/Widgets/ShoppingDesktopWidgetView.swift

@@ -0,0 +1,217 @@
+import AppKit
+import SwiftUI
+
+/// Paths on `shopping.google.com` for category shortcuts (same pattern as Travel).
+private enum ShoppingWebEndpoints {
+    static let dealsPath = "/deals"
+    static let listsPath = "/lists"
+    static let browsePath = "/browse"
+}
+
+/// Large Google Shopping: toolbar search and four category shortcuts (Shop, Deals, Lists, Browse). Opens in-app browser.
+struct ShoppingDesktopWidgetView: View {
+    let app: LauncherApp
+    let mode: ShoppingWidgetMode
+    var isPreview: Bool = false
+
+    @State private var searchText: String = ""
+
+    private var shoppingBase: URL {
+        app.webURL ?? URL(string: "https://shopping.google.com")!
+    }
+
+    var body: some View {
+        Group {
+            switch mode {
+            case .interactive:
+                interactiveBody
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    private var interactiveBody: some View {
+        Group {
+            if isPreview {
+                shoppingPreviewChrome
+            } else {
+                shoppingLiveChrome
+            }
+        }
+    }
+
+    private var shoppingPreviewChrome: some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(
+                    LinearGradient(
+                        colors: [Color(red: 0.06, green: 0.32, blue: 0.24), Color(red: 0.06, green: 0.14, blue: 0.12)],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+            VStack(spacing: 12) {
+                AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("Google Shopping")
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.92))
+                Text("Search products or open Deals, Lists, and Browse in the in-app browser.")
+                    .font(.system(size: 11, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.72))
+                    .multilineTextAlignment(.center)
+                    .padding(.horizontal, 8)
+            }
+            .padding(16)
+        }
+    }
+
+    private var shoppingLiveChrome: some View {
+        shoppingToolbar
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .background(
+                RoundedRectangle(cornerRadius: 22, style: .continuous)
+                    .fill(Color(red: 0.05, green: 0.16, blue: 0.12))
+            )
+            .preferredColorScheme(.dark)
+    }
+
+    private var shoppingAccent: Color {
+        Color(red: 0.0, green: 0.62, blue: 0.45)
+    }
+
+    private var shoppingToolbar: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            HStack(spacing: 8) {
+                AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("Shopping")
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Spacer(minLength: 0)
+                Button(action: refreshAction) {
+                    Image(systemName: "arrow.clockwise")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.9))
+                        .padding(8)
+                        .background(Circle().fill(Color.white.opacity(0.12)))
+                }
+                .buttonStyle(.plain)
+                .help("Repeat product search, or open Shopping home if the field is empty")
+                Button(action: { openInAppBrowser(shoppingBase) }) {
+                    Image(systemName: "arrow.up.right.square")
+                        .font(.system(size: 13, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.9))
+                        .padding(8)
+                        .background(Circle().fill(Color.white.opacity(0.12)))
+                }
+                .buttonStyle(.plain)
+                .help("Open Google Shopping in browser")
+            }
+
+            HStack(spacing: 6) {
+                TextField("Search products", text: $searchText)
+                    .textFieldStyle(.plain)
+                    .font(.system(size: 12, weight: .medium))
+                    .padding(.horizontal, 10)
+                    .padding(.vertical, 6)
+                    .background(
+                        RoundedRectangle(cornerRadius: 10, style: .continuous)
+                            .fill(Color.white.opacity(0.1))
+                    )
+                    .foregroundStyle(.white.opacity(0.95))
+                    .onSubmit { submitSearch() }
+                Button("Go", action: submitSearch)
+                    .font(.system(size: 12, weight: .semibold))
+                    .buttonStyle(.borderedProminent)
+                    .tint(shoppingAccent)
+                    .controlSize(.small)
+            }
+
+            shoppingCategoryRow
+        }
+        .padding(.horizontal, 12)
+        .padding(.top, 12)
+        .padding(.bottom, 10)
+        .frame(maxWidth: .infinity)
+    }
+
+    private var shoppingCategoryRow: some View {
+        HStack(spacing: 0) {
+            shoppingCategoryButton(title: "Shop", path: "/") {
+                Image(systemName: "bag.fill")
+                    .font(.system(size: 20, weight: .medium))
+            }
+            shoppingCategoryButton(title: "Deals", path: ShoppingWebEndpoints.dealsPath) {
+                Image(systemName: "tag.fill")
+                    .font(.system(size: 20, weight: .medium))
+            }
+            shoppingCategoryButton(title: "Lists", path: ShoppingWebEndpoints.listsPath) {
+                Image(systemName: "list.bullet.rectangle.fill")
+                    .font(.system(size: 20, weight: .medium))
+            }
+            shoppingCategoryButton(title: "Browse", path: ShoppingWebEndpoints.browsePath) {
+                Image(systemName: "square.grid.2x2.fill")
+                    .font(.system(size: 20, weight: .medium))
+            }
+        }
+        .padding(.top, 6)
+    }
+
+    private func shoppingCategoryButton<Icon: View>(
+        title: String,
+        path: String,
+        @ViewBuilder icon: () -> Icon
+    ) -> some View {
+        Button {
+            openShoppingPath(path)
+        } label: {
+            VStack(spacing: 5) {
+                icon()
+                    .foregroundStyle(Color.white.opacity(0.72))
+                Text(title)
+                    .font(.system(size: 10, weight: .medium))
+                    .foregroundStyle(Color.white.opacity(0.78))
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+                    .minimumScaleFactor(0.85)
+            }
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 6)
+            .contentShape(Rectangle())
+        }
+        .buttonStyle(.plain)
+    }
+
+    private func openShoppingPath(_ path: String) {
+        NSApp.activate(ignoringOtherApps: true)
+        let url = WidgetDeepLinkURL.resolved(base: shoppingBase, actionPath: path, accountSlot: 0)
+        openInAppBrowser(url)
+    }
+
+    private func refreshAction() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        NSApp.activate(ignoringOtherApps: true)
+        if q.isEmpty {
+            openInAppBrowser(shoppingBase)
+        } else {
+            openProductSearch(query: q)
+        }
+    }
+
+    private func submitSearch() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return }
+        NSApp.activate(ignoringOtherApps: true)
+        openProductSearch(query: q)
+    }
+
+    private func openProductSearch(query q: String) {
+        var c = URLComponents(string: "https://shopping.google.com/search")!
+        c.queryItems = [URLQueryItem(name: "q", value: q)]
+        guard let url = c.url else { return }
+        openInAppBrowser(url)
+    }
+
+    private func openInAppBrowser(_ url: URL) {
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+}

+ 17 - 4
google_apps/Widgets/WidgetTemplates.swift

@@ -73,6 +73,11 @@ enum TravelWidgetMode: Hashable, Sendable {
     case interactive
 }
 
+enum ShoppingWidgetMode: Hashable, Sendable {
+    /// Toolbar search plus Shop / Deals / Lists / Browse shortcuts; in-app browser.
+    case interactive
+}
+
 enum YoutubeWidgetMode: Hashable, Sendable {
     /// Search field opens YouTube results in the in-app browser.
     case searchBar
@@ -150,6 +155,8 @@ enum WidgetLayoutMode: Hashable {
     case books(BooksWidgetMode)
     /// Google Travel: search, toolbar, and category shortcuts; full site in browser (no trip panel).
     case travel(TravelWidgetMode)
+    /// Google Shopping: search, toolbar, and category shortcuts; full site in browser.
+    case shopping(ShoppingWidgetMode)
     /// YouTube: medium search bar or large navigation shortcuts; opens in-app browser.
     case youtube(YoutubeWidgetMode)
     /// Google Meet: Calendar-backed Meet links; join/create via in-app browser.
@@ -289,12 +296,13 @@ enum WidgetTemplates {
         case .shopping:
             return [
                 v("shopping_small", "Shopping", .small, false, .iconOnly(title: "Shopping"), [a("open", "Open", "bag.fill", "/")]),
-                v("shopping_medium", "Shopping Browse", .medium, true, .actionsGrid(columns: 2), [
+                v("shopping_medium", "Shopping shortcuts", .medium, true, .actionsGrid(columns: 2), [
                     a("home", "Home", "house", "/"),
-                    a("deals", "Deals", "tag", "/"),
-                    a("saved", "Saved", "bookmark", "/"),
-                    a("search", "Search", "magnifyingglass", "/"),
+                    a("deals", "Deals", "tag", "/deals"),
+                    a("lists", "Lists", "list.bullet.rectangle", "/lists"),
+                    a("browse", "Browse", "square.grid.2x2", "/browse"),
                 ]),
+                v("shopping_large", "Shop & deals", .large, false, .shopping(.interactive), []),
             ]
         case .keep:
             return [
@@ -563,6 +571,7 @@ extension WidgetLayoutMode {
         case .googleSearch(.interactive): true
         case .books(.interactive): true
         case .travel(.interactive): true
+        case .shopping(.interactive): true
         case .youtube(.searchBar): true
         case .youtube(.interactiveNav): true
         case .news(_): true
@@ -613,6 +622,8 @@ extension WidgetLayoutMode {
             return CGSize(width: 420, height: 260)
         case .travel(.interactive):
             return CGSize(width: 520, height: 196)
+        case .shopping(.interactive):
+            return CGSize(width: 520, height: 196)
         case .youtube(.searchBar):
             return CGSize(width: 400, height: 118)
         case .youtube(.interactiveNav):
@@ -679,6 +690,8 @@ extension WidgetLayoutMode {
             return CGSize(width: 380, height: 240)
         case .travel(.interactive):
             return CGSize(width: 460, height: 178)
+        case .shopping(.interactive):
+            return CGSize(width: 460, height: 178)
         case .youtube(.searchBar):
             return CGSize(width: 360, height: 108)
         case .youtube(.interactiveNav):