|
|
@@ -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)
|
|
|
+ }
|
|
|
+}
|