فهرست منبع

Redesign Maps widgets by size and fix medium actions.

Introduce distinct small/medium/large Maps experiences, tighten panel sizing to remove excess space, and ensure medium widget search and shortcut actions open working map destinations.

Made-with: Cursor
huzaifahayat12 3 ماه پیش
والد
کامیت
412d5f0eca

+ 2 - 2
google_apps/Widgets/DesktopWidgetWindowManager.swift

@@ -185,9 +185,9 @@ final class DesktopWidgetWindowManager: ObservableObject {
         if variant.layoutMode == .interactiveMap {
             switch variant.size {
             case .small:
-                return CGSize(width: 340, height: 420)
+                return CGSize(width: 340, height: 154)
             case .medium:
-                return CGSize(width: 460, height: 400)
+                return CGSize(width: 460, height: 246)
             case .large:
                 return CGSize(width: 520, height: 560)
             }

+ 268 - 99
google_apps/Widgets/MapsInteractiveWidgetView.swift

@@ -6,7 +6,8 @@ import WebKit
 
 // MARK: - Public SwiftUI view
 
-/// Desktop widget embedding Google Maps in a `WKWebView`, with search, traffic, and navigation shortcuts.
+/// Google Maps desktop widgets with distinct mobile-style experiences by size:
+/// small = commute shortcuts, medium = nearby finder, large = interactive live map.
 struct MapsInteractiveWidgetView: View {
     let app: LauncherApp
     let widgetSize: WidgetSize
@@ -41,7 +42,7 @@ struct MapsInteractiveWidgetView: View {
             if isPreview {
                 previewChrome
             } else {
-                interactiveChrome
+                liveBody
             }
         }
         .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
@@ -59,10 +60,10 @@ struct MapsInteractiveWidgetView: View {
                 )
             VStack(spacing: 12) {
                 AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
-                Text("Live Maps")
+                Text(previewTitle)
                     .font(.system(size: 15, weight: .bold))
                     .foregroundStyle(.white.opacity(0.92))
-                Text("Interactive map loads on your desktop.")
+                Text(previewSubtitle)
                     .font(.system(size: 11, weight: .medium))
                     .foregroundStyle(.white.opacity(0.65))
                     .multilineTextAlignment(.center)
@@ -72,34 +73,133 @@ struct MapsInteractiveWidgetView: View {
         }
     }
 
-    private var interactiveChrome: some View {
+    private var previewTitle: String {
+        switch widgetSize {
+        case .small: return "Commute"
+        case .medium: return "Nearby"
+        case .large: return "Live Map"
+        }
+    }
+
+    private var previewSubtitle: String {
+        switch widgetSize {
+        case .small: return "Quick access to Home, Work, and traffic."
+        case .medium: return "Search nearby places with one-tap categories."
+        case .large: return "Interactive map with live controls and shortcuts."
+        }
+    }
+
+    @ViewBuilder
+    private var liveBody: some View {
+        switch widgetSize {
+        case .small:
+            smallCommuteWidget
+        case .medium:
+            mediumNearbyWidget
+        case .large:
+            largeInteractiveWidget
+        }
+    }
+
+    private var baseBackground: some View {
+        LinearGradient(
+            colors: [Color(red: 0.08, green: 0.12, blue: 0.16), Color(red: 0.07, green: 0.26, blue: 0.23)],
+            startPoint: .topLeading,
+            endPoint: .bottomTrailing
+        )
+    }
+
+    private var smallCommuteWidget: some View {
         ZStack {
-            RoundedRectangle(cornerRadius: 22, style: .continuous)
-                .fill(Color.black.opacity(0.35))
+            baseBackground
+            VStack(alignment: .leading, spacing: 10) {
+                HStack(spacing: 8) {
+                    AppIconView(app: app, size: 26, showAppBackground: false, iconPaddingFactor: 0.08)
+                    VStack(alignment: .leading, spacing: 1) {
+                        Text("Google Maps")
+                            .font(.system(size: 13, weight: .bold))
+                            .foregroundStyle(.white.opacity(0.95))
+                        Text("Commute")
+                            .font(.system(size: 10, weight: .semibold))
+                            .foregroundStyle(.white.opacity(0.65))
+                    }
+                    Spacer(minLength: 0)
+                }
+
+                HStack(spacing: 8) {
+                    mapFeatureButton(title: "Home", icon: "house.fill", emphasized: true) {
+                        openDirectionsTo(destination: "home", travelMode: "driving")
+                    }
+                    mapFeatureButton(title: "Work", icon: "briefcase.fill", emphasized: true) {
+                        openDirectionsTo(destination: "work", travelMode: "driving")
+                    }
+                }
+
+                HStack(spacing: 8) {
+                    mapFeatureButton(title: "Traffic", icon: "car.fill") {
+                        trafficLayerOn = true
+                        mapURL = Self.makeMapURL(center: nil, zoom: nil, layer: "traffic")
+                        InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
+                    }
+                    mapFeatureButton(title: "Open", icon: "arrow.up.right.square") {
+                        InAppBrowserWindowManager.shared.open(url: URL(string: "https://www.google.com/maps")!, title: app.name)
+                    }
+                }
+            }
+            .padding(12)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+    }
+
+    private var mediumNearbyWidget: some View {
+        ZStack {
+            baseBackground
+            VStack(alignment: .leading, spacing: 10) {
+                compactHeader(title: "Google Maps", subtitle: "Nearby Finder")
+                searchBar
+                travelModesRow
+                nearbyCategoryRow
+                HStack(spacing: 8) {
+                    mapFeatureButton(title: "My location", icon: "location.fill") {
+                        centerOnCurrentLocation()
+                    }
+                    mapFeatureButton(title: "Open Maps", icon: "arrow.up.right.square", emphasized: true) {
+                        InAppBrowserWindowManager.shared.open(url: URL(string: "https://www.google.com/maps")!, title: app.name)
+                    }
+                }
+            }
+            .padding(12)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+    }
+
+    private var largeInteractiveWidget: some View {
+        ZStack {
+            baseBackground
 
             VStack(spacing: 0) {
-                VStack(spacing: 8) {
-                    headerBar
+                VStack(spacing: 10) {
+                    compactHeader(title: "Google Maps", subtitle: "Live Navigator")
                     searchBar
-                    primaryToolbar
-                    quickPlacesBar
+                    largeActionsRow
+                    nearbyCategoryRow
                 }
-                .padding(.horizontal, 4)
-                .padding(.vertical, 6)
+                .padding(.horizontal, 8)
+                .padding(.vertical, 8)
                 .background(
                     RoundedRectangle(cornerRadius: 16, style: .continuous)
-                        .fill(Color(red: 0.07, green: 0.08, blue: 0.1))
+                        .fill(Color.black.opacity(0.28))
                 )
                 .overlay(
                     RoundedRectangle(cornerRadius: 16, style: .continuous)
-                        .stroke(Color.white.opacity(0.1), lineWidth: 1)
+                        .stroke(Color.white.opacity(0.14), lineWidth: 1)
                 )
                 .zIndex(20)
                 .layoutPriority(1)
                 .allowsHitTesting(true)
 
                 MapsWebWidgetView(url: $mapURL)
-                    .frame(height: mapContentHeight)
+                    .frame(minHeight: mapContentHeight)
                     .frame(maxWidth: .infinity)
                     .clipped()
                     .compositingGroup()
@@ -111,16 +211,32 @@ struct MapsInteractiveWidgetView: View {
         }
     }
 
-    private var headerBar: some View {
+    private func compactHeader(title: String, subtitle: String) -> some View {
         HStack(spacing: 8) {
             AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
-            Text("Google Maps")
-                .font(.system(size: 14, weight: .bold))
-                .foregroundStyle(.white.opacity(0.95))
-                .lineLimit(1)
+            VStack(alignment: .leading, spacing: 1) {
+                Text(title)
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                    .lineLimit(1)
+                Text(subtitle)
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.62))
+            }
             Spacer(minLength: 0)
+            Button(action: {
+                NSApp.activate(ignoringOtherApps: true)
+                InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
+            }) {
+                Image(systemName: "arrow.up.right.square")
+                    .font(.system(size: 12, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.94))
+                    .frame(width: 30, height: 30)
+                    .background(Circle().fill(Color.white.opacity(0.14)))
+            }
+            .buttonStyle(.plain)
+            .help("Open current map in full browser")
         }
-        .padding(.bottom, 6)
     }
 
     private var searchBar: some View {
@@ -154,96 +270,97 @@ struct MapsInteractiveWidgetView: View {
             RoundedRectangle(cornerRadius: 12, style: .continuous)
                 .stroke(Color.white.opacity(0.12), lineWidth: 1)
         )
-        .padding(.bottom, 8)
     }
 
-    private var primaryToolbar: some View {
-        VStack(alignment: .leading, spacing: 6) {
+    private var travelModesRow: some View {
+        HStack(spacing: 8) {
+            mapFeatureButton(title: "Drive", icon: "car.fill") {
+                openTravelMode("driving")
+            }
+            mapFeatureButton(title: "Transit", icon: "tram.fill") {
+                openTravelMode("transit")
+            }
+            mapFeatureButton(title: "Walk", icon: "figure.walk") {
+                openTravelMode("walking")
+            }
+        }
+    }
+
+    private var largeActionsRow: some View {
+        HStack(spacing: 8) {
+            mapFeatureButton(title: trafficLayerOn ? "Traffic On" : "Traffic", icon: "car.fill", emphasized: trafficLayerOn) {
+                trafficLayerOn.toggle()
+                applyTrafficLayer()
+            }
+            mapFeatureButton(title: "Directions", icon: "arrow.triangle.turn.up.right.diamond.fill") {
+                openTravelMode("driving")
+            }
+            mapFeatureButton(title: "My location", icon: "location.fill") {
+                centerOnCurrentLocation()
+            }
+            mapFeatureButton(title: "Satellite", icon: "globe.americas.fill") {
+                openMapMode("satellite")
+            }
+        }
+    }
+
+    private var nearbyCategoryRow: some View {
+        ScrollView(.horizontal, showsIndicators: false) {
             HStack(spacing: 8) {
-                MapToolbarButton(
-                    title: trafficLayerOn ? "Traffic on" : "Traffic",
-                    systemImage: "car.side.fill",
-                    isSelected: trafficLayerOn,
-                    helpText: "Show or hide live traffic on the map"
-                ) {
-                    NSApp.activate(ignoringOtherApps: true)
-                    trafficLayerOn.toggle()
-                    applyTrafficLayer()
+                MapQuickPlaceChip(title: "Food", icon: "fork.knife", helpText: "Search for restaurants nearby") {
+                    activateAndSearch(.restaurants)
                 }
-                MapToolbarButton(
-                    title: "Directions",
-                    systemImage: "arrow.triangle.turn.up.right.diamond.fill",
-                    helpText: "Open driving directions (set start and destination in Maps)"
-                ) {
-                    NSApp.activate(ignoringOtherApps: true)
-                    loadURLString("https://www.google.com/maps/dir/?api=1&travelmode=driving")
+                MapQuickPlaceChip(title: "Gas", icon: "fuelpump.fill", helpText: "Find gas stations") {
+                    activateAndSearch(.gasStations)
                 }
-                MapToolbarButton(
-                    title: "My location",
-                    systemImage: "location.fill",
-                    helpText: "Center the map on your current location"
-                ) {
-                    NSApp.activate(ignoringOtherApps: true)
-                    locationHelper.requestLocation { coord in
-                        let center = "\(coord.latitude),\(coord.longitude)"
-                        let layer: String? = trafficLayerOn ? "traffic" : nil
-                        let url = MapsInteractiveWidgetView.makeMapURL(center: center, zoom: 15, layer: layer)
-                        Task { @MainActor in
-                            mapURL = url
-                        }
-                    }
+                MapQuickPlaceChip(title: "Hotels", icon: "bed.double.fill", helpText: "Search for hotels") {
+                    activateAndSearch(.hotels)
                 }
-            }
-            HStack(spacing: 8) {
-                MapToolbarButton(
-                    title: "Satellite",
-                    systemImage: "globe.americas.fill",
-                    helpText: "Switch to satellite imagery"
-                ) {
-                    NSApp.activate(ignoringOtherApps: true)
-                    loadURLString("https://www.google.com/maps/@?api=1&map_action=map&basemap=satellite")
+                MapQuickPlaceChip(title: "Coffee", icon: "cup.and.saucer.fill", helpText: "Find coffee shops") {
+                    activateAndSearch(.coffee)
+                }
+                MapQuickPlaceChip(title: "Parking", icon: "parkingsign", helpText: "Find nearby parking") {
+                    activateAndSearch(.parking)
                 }
-                MapToolbarButton(
-                    title: "Open",
-                    systemImage: "arrow.up.right.square",
-                    isEmphasized: true,
-                    helpText: "Open this map in the full browser window"
-                ) {
-                    NSApp.activate(ignoringOtherApps: true)
-                    InAppBrowserWindowManager.shared.open(url: mapURL, title: app.name)
+                MapQuickPlaceChip(title: "Pharmacy", icon: "cross.case.fill", helpText: "Find nearby pharmacies") {
+                    activateAndSearch(.pharmacy)
                 }
-                Spacer(minLength: 0)
             }
-            .animation(.easeInOut(duration: 0.18), value: trafficLayerOn)
         }
-        .frame(maxWidth: .infinity, alignment: .leading)
-        .padding(.bottom, 8)
     }
 
-    private var quickPlacesBar: some View {
-        HStack(alignment: .center, spacing: 8) {
-            MapQuickPlaceChip(title: "Food", icon: "fork.knife", helpText: "Search for restaurants nearby") {
-                activateAndSearch(.restaurants)
-            }
-            MapQuickPlaceChip(title: "Gas", icon: "fuelpump.fill", helpText: "Find gas stations") {
-                activateAndSearch(.gasStations)
-            }
-            MapQuickPlaceChip(title: "Hotels", icon: "bed.double.fill", helpText: "Search for hotels") {
-                activateAndSearch(.hotels)
-            }
-            MapQuickPlaceChip(title: "Coffee", icon: "cup.and.saucer.fill", helpText: "Find coffee shops") {
-                activateAndSearch(.coffee)
+    private func mapFeatureButton(title: String, icon: String, emphasized: Bool = false, action: @escaping () -> Void) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            action()
+        }) {
+            HStack(spacing: 6) {
+                Image(systemName: icon)
+                    .font(.system(size: 11, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 11, weight: .bold))
+                    .lineLimit(1)
             }
+            .foregroundStyle(.white.opacity(0.95))
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 8)
+            .background(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .fill(emphasized ? Color.blue.opacity(0.42) : Color.white.opacity(0.14))
+            )
+            .overlay(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .stroke(Color.white.opacity(emphasized ? 0.32 : 0.16), lineWidth: 1)
+            )
         }
-        .frame(maxWidth: .infinity, alignment: .leading)
-        .padding(.bottom, 2)
+        .buttonStyle(.plain)
     }
 
     private var mapContentHeight: CGFloat {
         switch widgetSize {
-        case .small: return 220
-        case .medium: return 200
-        case .large: return 340
+        case .small: return 210
+        case .medium: return 220
+        case .large: return 300
         }
     }
 
@@ -259,6 +376,8 @@ struct MapsInteractiveWidgetView: View {
         case gasStations = "gas stations"
         case hotels
         case coffee
+        case parking
+        case pharmacy
     }
 
     private func activateAndSearch(_ place: QuickPlaceSearch) {
@@ -268,9 +387,7 @@ struct MapsInteractiveWidgetView: View {
 
     private func openSearchQueryString(_ query: String) {
         guard let url = Self.mapsSearchURL(for: query) else { return }
-        DispatchQueue.main.async {
-            mapURL = url
-        }
+        openMapDestination(url)
     }
 
     /// Official search URL: `https://www.google.com/maps/search/?api=1&query=…`
@@ -286,12 +403,64 @@ struct MapsInteractiveWidgetView: View {
     }
 
     private func loadURLString(_ string: String) {
-        if let u = URL(string: string) { mapURL = u }
+        if let u = URL(string: string) {
+            openMapDestination(u)
+        }
+    }
+
+    private func openDirections(mode: String) {
+        loadURLString("https://www.google.com/maps/dir/?api=1&travelmode=\(mode)")
+    }
+
+    private func openDirectionsTo(destination: String, travelMode: String) {
+        var c = URLComponents(string: "https://www.google.com/maps/dir/")!
+        c.queryItems = [
+            URLQueryItem(name: "api", value: "1"),
+            URLQueryItem(name: "destination", value: destination),
+            URLQueryItem(name: "travelmode", value: travelMode),
+        ]
+        if let url = c.url {
+            InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+        }
+    }
+
+    private func openTravelMode(_ mode: String) {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        if q.isEmpty {
+            openDirections(mode: mode)
+        } else {
+            openDirectionsTo(destination: q, travelMode: mode)
+        }
+    }
+
+    private func openMapMode(_ basemap: String) {
+        loadURLString("https://www.google.com/maps/@?api=1&map_action=map&basemap=\(basemap)")
+    }
+
+    private func centerOnCurrentLocation() {
+        locationHelper.requestLocation { coord in
+            let center = "\(coord.latitude),\(coord.longitude)"
+            let layer: String? = trafficLayerOn ? "traffic" : nil
+            let url = MapsInteractiveWidgetView.makeMapURL(center: center, zoom: 15, layer: layer)
+            openMapDestination(url)
+        }
     }
 
     private func applyTrafficLayer() {
         let layer: String? = trafficLayerOn ? "traffic" : nil
-        mapURL = MapsInteractiveWidgetView.makeMapURL(center: nil, zoom: nil, layer: layer)
+        let next = MapsInteractiveWidgetView.makeMapURL(center: nil, zoom: nil, layer: layer)
+        openMapDestination(next)
+    }
+
+    /// Large widget updates embedded map; small/medium open in-app browser directly.
+    private func openMapDestination(_ url: URL) {
+        if widgetSize == .large {
+            DispatchQueue.main.async {
+                mapURL = url
+            }
+        } else {
+            InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+        }
     }
 }
 

+ 2 - 2
google_apps/Widgets/WidgetPreviewCard.swift

@@ -6,9 +6,9 @@ extension WidgetVariant {
         if layoutMode == .interactiveMap {
             switch size {
             case .small:
-                return CGSize(width: 280, height: 320)
+                return CGSize(width: 280, height: 126)
             case .medium:
-                return CGSize(width: 400, height: 320)
+                return CGSize(width: 400, height: 194)
             case .large:
                 return CGSize(width: 440, height: 420)
             }

+ 4 - 0
google_apps/Widgets/WidgetTemplates.swift

@@ -617,6 +617,8 @@ extension WidgetLayoutMode {
             case .compact: return CGSize(width: 230, height: 260)
             case .agenda: return CGSize(width: 420, height: 480)
             }
+        case .interactiveMap:
+            return CGSize(width: 420, height: 300)
         case .books(.interactive):
             return CGSize(width: 420, height: 260)
         case .travel(.interactive):
@@ -688,6 +690,8 @@ extension WidgetLayoutMode {
             case .compact: return CGSize(width: 210, height: 240)
             case .agenda: return CGSize(width: 380, height: 440)
             }
+        case .interactiveMap:
+            return CGSize(width: 380, height: 270)
         case .books(.interactive):
             return CGSize(width: 380, height: 240)
         case .travel(.interactive):