Преглед на файлове

Add Google Photos medium widget; drop small/large variants

- Introduce PhotosDesktopWidgetView with embedded WKWebView, header, and Albums/Memories/Search shortcuts.
- WidgetLayoutMode.photos (single tier); templates expose only photos_medium.
- Map legacy photos_small and photos_large variant IDs to photos_medium.
- Wire DesktopWidgetView to PhotosDesktopWidgetView.

Made-with: Cursor
huzaifahayat12 преди 3 месеца
родител
ревизия
e5914a5ae8
променени са 3 файла, в които са добавени 210 реда и са изтрити 13 реда
  1. 3 1
      google_apps/Widgets/DesktopWidgetView.swift
  2. 196 0
      google_apps/Widgets/PhotosDesktopWidgetView.swift
  3. 11 12
      google_apps/Widgets/WidgetTemplates.swift

+ 3 - 1
google_apps/Widgets/DesktopWidgetView.swift

@@ -55,6 +55,8 @@ struct DesktopWidgetView: View {
                 GooglePlayDesktopWidgetView(app: app, mode: playMode, isPreview: isPreview)
             } else if case .gemini(let geminiMode) = layoutMode {
                 GeminiDesktopWidgetView(app: app, mode: geminiMode, isPreview: isPreview)
+            } else if case .photos = layoutMode {
+                PhotosDesktopWidgetView(app: app, isPreview: isPreview)
             } else if case .interactiveMap = layoutMode {
                 MapsInteractiveWidgetView(app: app, widgetSize: size, isPreview: isPreview)
             } else {
@@ -90,7 +92,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .shopping, .youtube, .meet, .contacts, .news, .forms, .docs, .sheets, .slides, .googlePlay, .gemini:
+                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .shopping, .youtube, .meet, .contacts, .news, .forms, .docs, .sheets, .slides, .googlePlay, .gemini, .photos:
                             EmptyView()
                         }
                     }

+ 196 - 0
google_apps/Widgets/PhotosDesktopWidgetView.swift

@@ -0,0 +1,196 @@
+import AppKit
+import SwiftUI
+import WebKit
+
+/// Google Photos: medium embedded recent gallery with Albums / Memories / Search shortcuts.
+struct PhotosDesktopWidgetView: View {
+    let app: LauncherApp
+    var isPreview: Bool = false
+
+    private var baseURL: URL {
+        app.webURL ?? URL(string: "https://photos.google.com/")!
+    }
+
+    var body: some View {
+        Group {
+            if isPreview {
+                photosPreviewChrome
+            } else {
+                VStack(spacing: 0) {
+                    galleryHeader(title: "Recent", subtitle: "photos.google.com")
+                    GooglePhotosEmbeddedWebView(startURL: baseURL)
+                        .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    mediumShortcutRow
+                }
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    private var photosPreviewChrome: some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(
+                    LinearGradient(
+                        colors: [Color(red: 0.12, green: 0.16, blue: 0.22), Color(red: 0.05, green: 0.08, blue: 0.12)],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+            VStack(spacing: 12) {
+                AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("Recent gallery")
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.92))
+                Text("Live Google Photos grid in the panel after you sign in.")
+                    .font(.system(size: 11, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.65))
+                    .multilineTextAlignment(.center)
+                    .padding(.horizontal, 12)
+            }
+            .padding(16)
+        }
+    }
+
+    private func galleryHeader(title: String, subtitle: String) -> some View {
+        HStack(spacing: 10) {
+            AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
+            VStack(alignment: .leading, spacing: 2) {
+                Text(app.name)
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Text(title)
+                    .font(.system(size: 11, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.65))
+            }
+            Spacer(minLength: 0)
+            Text(subtitle)
+                .font(.system(size: 9, weight: .medium))
+                .foregroundStyle(.white.opacity(0.4))
+        }
+        .padding(.horizontal, 14)
+        .padding(.vertical, 10)
+        .background(Color.black.opacity(0.35))
+    }
+
+    private var mediumShortcutRow: some View {
+        HStack(spacing: 8) {
+            shortcutPill(title: "Albums", systemImage: "rectangle.stack", path: "/albums")
+            shortcutPill(title: "Memories", systemImage: "sparkles", path: "/memories")
+            shortcutPill(title: "Search", systemImage: "magnifyingglass", path: "/search")
+        }
+        .padding(.horizontal, 12)
+        .padding(.vertical, 10)
+        .background(Color.black.opacity(0.4))
+    }
+
+    private func shortcutPill(title: String, systemImage: String, path: String) -> some View {
+        Button(action: { openPhotosPath(path) }) {
+            HStack(spacing: 6) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 11, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 11, weight: .bold))
+            }
+            .foregroundStyle(.white.opacity(0.92))
+            .padding(.horizontal, 12)
+            .padding(.vertical, 8)
+            .background(Capsule().fill(Color.white.opacity(0.12)))
+        }
+        .buttonStyle(.plain)
+    }
+
+    private func openPhotosPath(_ path: String) {
+        let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
+        NSApp.activate(ignoringOtherApps: true)
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+}
+
+// MARK: - WKWebView
+
+private final class PhotosWebViewClippingContainer: NSView {
+    override var isFlipped: Bool { true }
+}
+
+private struct GooglePhotosEmbeddedWebView: NSViewRepresentable {
+    let startURL: URL
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator()
+    }
+
+    func makeNSView(context: Context) -> NSView {
+        let container = PhotosWebViewClippingContainer()
+        container.wantsLayer = true
+        container.layer?.masksToBounds = true
+
+        let config = WKWebViewConfiguration()
+        config.defaultWebpagePreferences.allowsContentJavaScript = true
+        let embedded = WKWebView(frame: .zero, configuration: config)
+        embedded.translatesAutoresizingMaskIntoConstraints = false
+        embedded.focusRingType = .none
+        embedded.navigationDelegate = context.coordinator
+        context.coordinator.embeddedWebView = embedded
+        context.coordinator.pageURL = startURL
+        context.coordinator.reload()
+
+        container.addSubview(embedded)
+        NSLayoutConstraint.activate([
+            embedded.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            embedded.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            embedded.topAnchor.constraint(equalTo: container.topAnchor),
+            embedded.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+        ])
+
+        DispatchQueue.main.async {
+            Self.stripWebKitOuterChrome(embedded)
+        }
+        return container
+    }
+
+    func updateNSView(_ container: NSView, context: Context) {
+        guard context.coordinator.pageURL != startURL else { return }
+        context.coordinator.pageURL = startURL
+        context.coordinator.reload()
+    }
+
+    private static func stripWebKitOuterChrome(_ root: WKWebView) {
+        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(root)
+    }
+
+    final class Coordinator: NSObject, WKNavigationDelegate {
+        weak var embeddedWebView: WKWebView?
+        var pageURL = URL(string: "https://photos.google.com/")!
+
+        func reload() {
+            guard let embeddedWebView else { return }
+            embeddedWebView.load(URLRequest(url: pageURL))
+        }
+
+        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+            DispatchQueue.main.async {
+                GooglePhotosEmbeddedWebView.stripWebKitOuterChrome(webView)
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+                    GooglePhotosEmbeddedWebView.stripWebKitOuterChrome(webView)
+                }
+            }
+        }
+    }
+}

+ 11 - 12
google_apps/Widgets/WidgetTemplates.swift

@@ -180,6 +180,8 @@ enum WidgetLayoutMode: Hashable {
     case googlePlay(GooglePlayWidgetMode)
     /// Google Gemini: large mini-assistant panel.
     case gemini(GeminiWidgetMode)
+    /// Google Photos: medium embedded recent gallery + shortcuts (single variant).
+    case photos
 }
 
 struct WidgetVariant: Identifiable, Hashable {
@@ -201,18 +203,7 @@ enum WidgetTemplates {
         switch profile {
         case .photos:
             return [
-                v("photos_small", "Photo", .small, false, .iconOnly(title: "Photo"), [a("photos", "Photos", "photo.on.rectangle", "/")]),
-                v("photos_medium", "Google Photo", .medium, false, .iconOnly(title: "Google Photo"), [a("albums", "Albums", "rectangle.stack", "/albums")]),
-                v("photos_large", "Photos Library", .large, true, .actionsGrid(columns: 2), [
-                    a("photos", "Photos", "photo.on.rectangle", "/"),
-                    a("albums", "Albums", "rectangle.stack", "/albums"),
-                    a("explore", "Explore", "magnifyingglass", "/search"),
-                    a("archive", "Archive", "archivebox", "/archive"),
-                    a("sharing", "Sharing", "person.2", "/sharing"),
-                    a("locked", "Locked Folder", "lock", "/lockedfolder"),
-                    a("favorites", "Favorites", "star", "/favorites"),
-                    a("trash", "Trash", "trash", "/trash"),
-                ]),
+                v("photos_medium", "Recent gallery", .medium, false, .photos, []),
             ]
         case .gmail:
             return [
@@ -510,6 +501,9 @@ enum WidgetTemplates {
             if WidgetAppProfile.from(app: app) == .gmail, id == "gmail_medium" {
                 return "gmail_large"
             }
+            if WidgetAppProfile.from(app: app) == .photos {
+                if id == "photos_small" || id == "photos_large" { return "photos_medium" }
+            }
             return id
         }()
         if let normalizedID, let match = variants.first(where: { $0.id == normalizedID }) {
@@ -586,6 +580,7 @@ extension WidgetLayoutMode {
         case .slides(_): true
         case .googlePlay(_): true
         case .gemini(_): true
+        case .photos: true
         case .meet: false
         case .calendar: false
         default: false
@@ -657,6 +652,8 @@ extension WidgetLayoutMode {
             return CGSize(width: 400, height: 168)
         case .gemini(.assistant):
             return CGSize(width: 400, height: 236)
+        case .photos:
+            return CGSize(width: 420, height: 460)
         default:
             return nil
         }
@@ -726,6 +723,8 @@ extension WidgetLayoutMode {
             return CGSize(width: 360, height: 158)
         case .gemini(.assistant):
             return CGSize(width: 400, height: 236)
+        case .photos:
+            return CGSize(width: 380, height: 420)
         default:
             return nil
         }