Selaa lähdekoodia

Refine large Travel widget: toolbar, four shortcuts, fix Hotels icon and rentals URL

- Keep header (icon, title, refresh, open) and search + Go; drop embedded
  trip/itinerary/flights panel.
- Add Explore, Flights, Hotels, and Vacation rentals row below search;
  shortcuts open google.com/travel in the in-app browser.
- Use bed.double.fill for Hotels (figure.sleep was missing on some systems).
- Point Vacation rentals at /travel/hotels?q=vacation+rentals (dedicated
  /travel/vacation-rentals returns 404).
- Adjust desktop and preview card sizes and layout-mode comments.

Made-with: Cursor
huzaifahayat12 3 kuukautta sitten
vanhempi
sitoutus
66b8706f97

+ 85 - 208
google_apps/Widgets/TravelDesktopWidgetView.swift

@@ -1,25 +1,24 @@
 import AppKit
 import SwiftUI
 
-/// Canonical Travel URLs on `google.com/travel` (paths must be rooted at `/travel/…` when resolving against `https://www.google.com`).
 private enum TravelWebEndpoints {
     static let home = URL(string: "https://www.google.com/travel")!
-    /// Base used with `WidgetDeepLinkURL` so `/travel/trips` becomes `https://www.google.com/travel/trips`, not `https://www.google.com/trips`.
+    /// Resolves `/travel/…` paths to `https://www.google.com/travel/…`.
     static let pathBase = URL(string: "https://www.google.com")!
-    static let trips = "/travel/trips"
-    static let explore = "/travel/explore"
-    static let flights = "/travel/flights"
-    static let hotels = "/travel/hotels"
+    static let explorePath = "/travel/explore"
+    static let flightsPath = "/travel/flights"
+    static let hotelsPath = "/travel/hotels"
+    /// Vacation rentals live under Hotels with a search hint (`/travel/vacation-rentals` 404s).
+    static let vacationRentalsPath = "/travel/hotels?q=vacation+rentals"
 }
 
-/// Large Google Travel: native trip summary, search, and shortcuts — opens full Travel in the in-app browser when you ask it to.
+/// Large Google Travel: toolbar, search, and four shortcuts (Explore, Flights, Hotels, Vacation rentals). No embedded trip panel.
 struct TravelDesktopWidgetView: View {
     let app: LauncherApp
     let mode: TravelWidgetMode
     var isPreview: Bool = false
 
     @State private var searchText: String = ""
-    @State private var lastRefreshed: Date = .init()
 
     var body: some View {
         Group {
@@ -56,7 +55,7 @@ struct TravelDesktopWidgetView: View {
                 Text("Google Travel")
                     .font(.system(size: 15, weight: .bold))
                     .foregroundStyle(.white.opacity(0.92))
-                Text("Trips, flights, and hotels at a glance — open Google Travel in one tap when you need the full site.")
+                Text("Search destinations or jump to Explore, Flights, Hotels, and Vacation rentals in the in-app browser.")
                     .font(.system(size: 11, weight: .medium))
                     .foregroundStyle(.white.opacity(0.72))
                     .multilineTextAlignment(.center)
@@ -66,16 +65,14 @@ struct TravelDesktopWidgetView: View {
         }
     }
 
-    /// Edge-to-edge layout: no inset “frame” or embedded web view below the toolbar.
     private var travelLiveChrome: some View {
-        VStack(spacing: 0) {
-            travelToolbar
-            nativeTripPanel
-        }
-        .background(
-            RoundedRectangle(cornerRadius: 22, style: .continuous)
-                .fill(Color(nsColor: .windowBackgroundColor))
-        )
+        travelToolbar
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .background(
+                RoundedRectangle(cornerRadius: 22, style: .continuous)
+                    .fill(Color(red: 0.06, green: 0.22, blue: 0.26))
+            )
+            .preferredColorScheme(.dark)
     }
 
     private var travelToolbar: some View {
@@ -86,7 +83,7 @@ struct TravelDesktopWidgetView: View {
                     .font(.system(size: 14, weight: .bold))
                     .foregroundStyle(.white.opacity(0.95))
                 Spacer(minLength: 0)
-                Button(action: { lastRefreshed = Date() }) {
+                Button(action: refreshAction) {
                     Image(systemName: "arrow.clockwise")
                         .font(.system(size: 13, weight: .semibold))
                         .foregroundStyle(.white.opacity(0.9))
@@ -94,7 +91,7 @@ struct TravelDesktopWidgetView: View {
                         .background(Circle().fill(Color.white.opacity(0.12)))
                 }
                 .buttonStyle(.plain)
-                .help("Refresh summary")
+                .help("Repeat search, or open Travel home if the field is empty")
                 Button(action: { openInAppBrowser(TravelWebEndpoints.home) }) {
                     Image(systemName: "arrow.up.right.square")
                         .font(.system(size: 13, weight: .semibold))
@@ -125,224 +122,104 @@ struct TravelDesktopWidgetView: View {
                     .controlSize(.small)
             }
 
-            ScrollView(.horizontal, showsIndicators: false) {
-                HStack(spacing: 6) {
-                    travelChip(title: "Trips", systemImage: "suitcase.fill", path: TravelWebEndpoints.trips)
-                    travelChip(title: "Explore", systemImage: "globe.americas.fill", path: TravelWebEndpoints.explore)
-                    travelChip(title: "Flights", systemImage: "airplane.departure", path: TravelWebEndpoints.flights)
-                    travelChip(title: "Hotels", systemImage: "bed.double.fill", path: TravelWebEndpoints.hotels)
-                }
-            }
+            travelCategoryRow
         }
         .padding(.horizontal, 12)
         .padding(.top, 12)
         .padding(.bottom, 10)
         .frame(maxWidth: .infinity)
-        .background(Color(red: 0.06, green: 0.22, blue: 0.26))
     }
 
-    private func travelChip(title: String, systemImage: String, path: String) -> some View {
-        Button {
-            NSApp.activate(ignoringOtherApps: true)
-            let url = WidgetDeepLinkURL.resolved(base: TravelWebEndpoints.pathBase, actionPath: path, accountSlot: 0)
-            openInAppBrowser(url)
-        } label: {
-            HStack(spacing: 4) {
-                Image(systemName: systemImage)
-                    .font(.system(size: 10, weight: .semibold))
-                Text(title)
-                    .font(.system(size: 11, weight: .semibold))
+    private var travelCategoryRow: some View {
+        HStack(spacing: 0) {
+            travelCategoryButton(title: "Explore", path: TravelWebEndpoints.explorePath) {
+                exploreCategoryIcon
             }
-            .foregroundStyle(.white.opacity(0.92))
-            .padding(.horizontal, 10)
-            .padding(.vertical, 6)
-            .background(
-                Capsule()
-                    .fill(Color.white.opacity(0.14))
-            )
-        }
-        .buttonStyle(.plain)
-    }
-
-    private func submitSearch() {
-        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
-        guard !q.isEmpty else { return }
-        NSApp.activate(ignoringOtherApps: true)
-        var c = URLComponents(string: "https://www.google.com/travel/explore")!
-        c.queryItems = [URLQueryItem(name: "q", value: q)]
-        if let url = c.url {
-            openInAppBrowser(url)
-        }
-    }
-
-    private func openInAppBrowser(_ url: URL) {
-        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
-    }
-
-    // MARK: - Native trip panel (sample data — illustrative only)
-
-    private var nativeTripPanel: some View {
-        ScrollView {
-            VStack(alignment: .leading, spacing: 14) {
-                tripHeaderCard
-                itinerarySection
-                flightsSection
-                hotelsSection
-                updatesSection
+            travelCategoryButton(title: "Flights", path: TravelWebEndpoints.flightsPath) {
+                Image(systemName: "airplane")
+                    .font(.system(size: 20, weight: .medium))
             }
-            .padding(14)
-        }
-        .frame(maxWidth: .infinity, maxHeight: .infinity)
-    }
-
-    private var tripHeaderCard: some View {
-        VStack(alignment: .leading, spacing: 6) {
-            HStack {
-                Text("Tokyo spring trip")
-                    .font(.system(size: 16, weight: .bold))
-                    .foregroundStyle(.primary)
-                Spacer()
-                Label("Live", systemImage: "dot.radiowaves.left.and.right")
-                    .font(.system(size: 10, weight: .semibold))
-                    .foregroundStyle(.green)
-                    .padding(.horizontal, 8)
-                    .padding(.vertical, 4)
-                    .background(Capsule().fill(Color.green.opacity(0.15)))
+            travelCategoryButton(title: "Hotels", path: TravelWebEndpoints.hotelsPath) {
+                Image(systemName: "bed.double.fill")
+                    .font(.system(size: 20, weight: .medium))
+            }
+            travelCategoryButton(title: "Vacation rentals", path: TravelWebEndpoints.vacationRentalsPath) {
+                Image(systemName: "house.fill")
+                    .font(.system(size: 20, weight: .medium))
             }
-            Text("Apr 12 – Apr 20 · 8 nights")
-                .font(.system(size: 12, weight: .medium))
-                .foregroundStyle(.secondary)
-            Text("Updated \(Self.relativeTime(lastRefreshed))")
-                .font(.system(size: 10, weight: .medium))
-                .foregroundStyle(.tertiary)
         }
-        .padding(12)
-        .frame(maxWidth: .infinity, alignment: .leading)
-        .background(
-            RoundedRectangle(cornerRadius: 14, style: .continuous)
-                .fill(Color.primary.opacity(0.06))
-        )
+        .padding(.top, 6)
     }
 
-    private var itinerarySection: some View {
-        VStack(alignment: .leading, spacing: 8) {
-            sectionTitle("Itinerary")
-            itineraryRow(day: "Sat 12", title: "Arrive NRT · Airport Express", detail: "Check-in after 3:00 PM")
-            itineraryRow(day: "Sun 13", title: "Shibuya & Harajuku", detail: "Reservations: lunch 12:30")
-            itineraryRow(day: "Mon 14", title: "Day trip · Kamakura", detail: "Return by 7:00 PM")
+    /// Globe + magnifying glass, similar to Google Travel’s Explore affordance.
+    private var exploreCategoryIcon: some View {
+        ZStack {
+            Image(systemName: "globe.americas.fill")
+                .font(.system(size: 19, weight: .medium))
+            Image(systemName: "magnifyingglass")
+                .font(.system(size: 9, weight: .bold))
+                .offset(x: 9, y: 7)
         }
+        .frame(width: 28, height: 26)
     }
 
-    private func itineraryRow(day: String, title: String, detail: String) -> some View {
-        HStack(alignment: .top, spacing: 10) {
-            Text(day)
-                .font(.system(size: 11, weight: .bold, design: .rounded))
-                .foregroundStyle(.white)
-                .padding(.horizontal, 8)
-                .padding(.vertical, 4)
-                .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color(red: 0.1, green: 0.45, blue: 0.48)))
-            VStack(alignment: .leading, spacing: 2) {
+    private func travelCategoryButton<Icon: View>(
+        title: String,
+        path: String,
+        @ViewBuilder icon: () -> Icon
+    ) -> some View {
+        Button {
+            openTravelPath(path)
+        } label: {
+            VStack(spacing: 5) {
+                icon()
+                    .foregroundStyle(Color.white.opacity(0.72))
                 Text(title)
-                    .font(.system(size: 12, weight: .semibold))
-                Text(detail)
-                    .font(.system(size: 11))
-                    .foregroundStyle(.secondary)
+                    .font(.system(size: 10, weight: .medium))
+                    .foregroundStyle(Color.white.opacity(0.78))
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+                    .minimumScaleFactor(0.85)
             }
-            Spacer(minLength: 0)
-        }
-        .padding(10)
-        .background(
-            RoundedRectangle(cornerRadius: 12, style: .continuous)
-                .strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
-        )
-    }
-
-    private var flightsSection: some View {
-        VStack(alignment: .leading, spacing: 8) {
-            sectionTitle("Flights")
-            travelInfoCard(
-                icon: "airplane.departure",
-                title: "SFO → NRT",
-                subtitle: "JL 1 · Departs 12:45 PM · On time",
-                footnote: "Seat 12A · Terminal INT"
-            )
-            travelInfoCard(
-                icon: "airplane.arrival",
-                title: "NRT → SFO",
-                subtitle: "JL 2 · Apr 20 · 5:10 PM",
-                footnote: "Check-in opens 24h before"
-            )
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 6)
+            .contentShape(Rectangle())
         }
+        .buttonStyle(.plain)
     }
 
-    private var hotelsSection: some View {
-        VStack(alignment: .leading, spacing: 8) {
-            sectionTitle("Hotels")
-            travelInfoCard(
-                icon: "bed.double.fill",
-                title: "Hotel Gracery Shinjuku",
-                subtitle: "Apr 12 – Apr 16 · 4 nights",
-                footnote: "Confirmation #8K2P9Q · Late checkout requested"
-            )
-            travelInfoCard(
-                icon: "bed.double.fill",
-                title: "Ryokan Asakusa",
-                subtitle: "Apr 16 – Apr 20 · 4 nights",
-                footnote: "Traditional breakfast included"
-            )
-        }
+    private func openTravelPath(_ path: String) {
+        NSApp.activate(ignoringOtherApps: true)
+        let url = WidgetDeepLinkURL.resolved(base: TravelWebEndpoints.pathBase, actionPath: path, accountSlot: 0)
+        openInAppBrowser(url)
     }
 
-    private var updatesSection: some View {
-        VStack(alignment: .leading, spacing: 8) {
-            sectionTitle("Destination")
-            Text("Tokyo · Partly cloudy 18°C · JPY favorable vs. last month for your dates.")
-                .font(.system(size: 11))
-                .foregroundStyle(.secondary)
-                .padding(10)
-                .frame(maxWidth: .infinity, alignment: .leading)
-                .background(
-                    RoundedRectangle(cornerRadius: 12, style: .continuous)
-                        .fill(Color.orange.opacity(0.08))
-                )
+    private func refreshAction() {
+        NSApp.activate(ignoringOtherApps: true)
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        if q.isEmpty {
+            openInAppBrowser(TravelWebEndpoints.home)
+        } else {
+            openExplore(query: q)
         }
     }
 
-    private func sectionTitle(_ text: String) -> some View {
-        Text(text)
-            .font(.system(size: 11, weight: .bold))
-            .foregroundStyle(.secondary)
-            .textCase(.uppercase)
+    private func submitSearch() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return }
+        NSApp.activate(ignoringOtherApps: true)
+        openExplore(query: q)
     }
 
-    private func travelInfoCard(icon: String, title: String, subtitle: String, footnote: String) -> some View {
-        HStack(alignment: .top, spacing: 10) {
-            Image(systemName: icon)
-                .font(.system(size: 14, weight: .semibold))
-                .foregroundStyle(Color(red: 0.1, green: 0.45, blue: 0.48))
-                .frame(width: 24)
-            VStack(alignment: .leading, spacing: 4) {
-                Text(title)
-                    .font(.system(size: 13, weight: .semibold))
-                Text(subtitle)
-                    .font(.system(size: 11, weight: .medium))
-                    .foregroundStyle(.secondary)
-                Text(footnote)
-                    .font(.system(size: 10))
-                    .foregroundStyle(.tertiary)
-            }
-            Spacer(minLength: 0)
+    private func openExplore(query q: String) {
+        var c = URLComponents(string: "https://www.google.com/travel/explore")!
+        c.queryItems = [URLQueryItem(name: "q", value: q)]
+        if let url = c.url {
+            openInAppBrowser(url)
         }
-        .padding(10)
-        .background(
-            RoundedRectangle(cornerRadius: 12, style: .continuous)
-                .fill(Color.primary.opacity(0.04))
-        )
     }
 
-    private static func relativeTime(_ date: Date) -> String {
-        let f = RelativeDateTimeFormatter()
-        f.unitsStyle = .abbreviated
-        return f.localizedString(for: date, relativeTo: Date())
+    private func openInAppBrowser(_ url: URL) {
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
     }
 }

+ 4 - 4
google_apps/Widgets/WidgetTemplates.swift

@@ -69,7 +69,7 @@ enum BooksWidgetMode: Hashable, Sendable {
 }
 
 enum TravelWidgetMode: Hashable, Sendable {
-    /// Native trip summary with shortcuts; full Travel opens in the in-app browser on demand.
+    /// Toolbar search plus Explore / Flights / Hotels / Vacation rentals shortcuts; in-app browser (no embedded trip UI).
     case interactive
 }
 
@@ -95,7 +95,7 @@ enum WidgetLayoutMode: Hashable {
     case calendar(CalendarWidgetMode)
     /// Embedded Google Books (`WKWebView`): library, reading, bookmarks, discover, search.
     case books(BooksWidgetMode)
-    /// Google Travel: native trip-style summary; browser for full site.
+    /// Google Travel: search, toolbar, and category shortcuts; full site in browser (no trip panel).
     case travel(TravelWidgetMode)
 }
 
@@ -635,7 +635,7 @@ extension WidgetLayoutMode {
         case .books(.interactive):
             return CGSize(width: 420, height: 260)
         case .travel(.interactive):
-            return CGSize(width: 560, height: 620)
+            return CGSize(width: 520, height: 196)
         default:
             return nil
         }
@@ -674,7 +674,7 @@ extension WidgetLayoutMode {
         case .books(.interactive):
             return CGSize(width: 380, height: 240)
         case .travel(.interactive):
-            return CGSize(width: 480, height: 450)
+            return CGSize(width: 460, height: 178)
         default:
             return nil
         }