Browse Source

Add tiered Google Drive desktop widgets

- DriveWidgetMode: recent quick access, files preview with open/share, interactive WKWebView
- DriveDesktopWidgetView with sample recent/starred rows, search, folder chips, create/upload
- WidgetLayoutMode.drive; panel sizing and key-capable panel for interactive tier
- DesktopWidgetView wiring; simplify large maxActions (Drive no longer uses action grid)

Made-with: Cursor
huzaifahayat12 3 tháng trước cách đây
mục cha
commit
c3eb242f21

+ 4 - 6
google_apps/Widgets/DesktopWidgetView.swift

@@ -17,6 +17,8 @@ struct DesktopWidgetView: View {
         Group {
             if case .gmail(let gmailMode) = layoutMode {
                 GmailDesktopWidgetView(app: app, mode: gmailMode, gmailAccountSlot: gmailAccountSlot, isPreview: isPreview)
+            } else if case .drive(let driveMode) = layoutMode {
+                DriveDesktopWidgetView(app: app, mode: driveMode, isPreview: isPreview)
             } else if case .interactiveMap = layoutMode {
                 MapsInteractiveWidgetView(app: app, widgetSize: size, isPreview: isPreview)
             } else {
@@ -52,7 +54,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail:
+                        case .interactiveMap, .gmail, .drive:
                             EmptyView()
                         }
                     }
@@ -108,11 +110,7 @@ struct DesktopWidgetView: View {
         switch size {
         case .small: return 3
         case .medium: return 4
-        case .large:
-            switch WidgetAppProfile.from(app: app) {
-            case .drive: return 15
-            default: return 8
-            }
+        case .large: return 8
         }
     }
 

+ 614 - 0
google_apps/Widgets/DriveDesktopWidgetView.swift

@@ -0,0 +1,614 @@
+import AppKit
+import SwiftUI
+import WebKit
+
+/// Tiered Google Drive desktop widget: recent files, starred preview, or full embedded Drive.
+struct DriveDesktopWidgetView: View {
+    let app: LauncherApp
+    let mode: DriveWidgetMode
+    var isPreview: Bool = false
+
+    private var baseURL: URL { app.webURL ?? URL(string: "https://drive.google.com")! }
+
+    var body: some View {
+        Group {
+            switch mode {
+            case .recentQuick:
+                recentQuickBody
+            case .filesPreview:
+                filesPreviewBody
+            case .interactive:
+                interactiveBody
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    // MARK: - Small
+
+    private struct RecentEntry: Identifiable {
+        let id: String
+        let name: String
+        let systemImage: String
+        let time: String
+    }
+
+    private static let recentSamples: [RecentEntry] = [
+        RecentEntry(id: "1", name: "Q4 Budget.xlsx", systemImage: "tablecells", time: "2h ago"),
+        RecentEntry(id: "2", name: "Project Notes", systemImage: "doc.text", time: "Yesterday"),
+        RecentEntry(id: "3", name: "Design mockup.png", systemImage: "photo", time: "Mon"),
+    ]
+
+    private var recentQuickBody: some View {
+        ZStack(alignment: .topLeading) {
+            driveGradientBackground
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+            VStack(alignment: .leading, spacing: 10) {
+                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("Recent files")
+                            .font(.system(size: 10, weight: .semibold))
+                            .foregroundStyle(.white.opacity(0.65))
+                    }
+                    Spacer(minLength: 0)
+                }
+
+                Text("Sample")
+                    .font(.system(size: 9, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.45))
+
+                VStack(spacing: 6) {
+                    ForEach(Self.recentSamples) { item in
+                        recentRow(name: item.name, systemImage: item.systemImage, time: item.time) {
+                            openDriveSearch(query: item.name)
+                        }
+                    }
+                }
+
+                drivePrimaryButton(title: "Open Drive", systemImage: "externaldrive.fill") {
+                    openDrivePath("/drive/home")
+                }
+            }
+            .padding(14)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .disabled(isPreview)
+        .opacity(isPreview ? 0.85 : 1)
+    }
+
+    private func recentRow(name: String, systemImage: String, time: String, action: @escaping () -> Void) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            action()
+        }) {
+            HStack(spacing: 10) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 14, weight: .semibold))
+                    .foregroundStyle(Color(red: 0.75, green: 0.92, blue: 0.82))
+                    .frame(width: 28, height: 28)
+                    .background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.12)))
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(name)
+                        .font(.system(size: 12, weight: .semibold))
+                        .foregroundStyle(.white.opacity(0.95))
+                        .lineLimit(1)
+                    Text(time)
+                        .font(.system(size: 10, weight: .medium))
+                        .foregroundStyle(.white.opacity(0.5))
+                }
+                Spacer(minLength: 0)
+                Image(systemName: "chevron.right")
+                    .font(.system(size: 10, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.35))
+            }
+            .padding(10)
+            .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.22)))
+        }
+        .buttonStyle(.plain)
+        .disabled(isPreview)
+        .allowsHitTesting(!isPreview)
+    }
+
+    // MARK: - Medium
+
+    private struct StarredEntry: Identifiable {
+        let id: String
+        let name: String
+        let systemImage: String
+    }
+
+    private static let starredSamples: [StarredEntry] = [
+        StarredEntry(id: "s1", name: "Roadmap 2026", systemImage: "rectangle.on.rectangle"),
+        StarredEntry(id: "s2", name: "Team roster", systemImage: "doc.richtext"),
+    ]
+
+    private var filesPreviewBody: some View {
+        ZStack(alignment: .topLeading) {
+            driveGradientBackground
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+            ScrollView {
+                VStack(alignment: .leading, spacing: 12) {
+                    HStack(spacing: 8) {
+                        AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
+                        Text("Drive")
+                            .font(.system(size: 14, weight: .bold))
+                            .foregroundStyle(.white.opacity(0.95))
+                        Spacer(minLength: 0)
+                        Text("Sample")
+                            .font(.system(size: 10, weight: .bold))
+                            .foregroundStyle(.white.opacity(0.5))
+                            .padding(.horizontal, 8)
+                            .padding(.vertical, 4)
+                            .background(Capsule().fill(Color.white.opacity(0.12)))
+                    }
+
+                    drivePrimaryButton(title: "Open Drive", systemImage: "externaldrive.fill") {
+                        openDrivePath("/drive/home")
+                    }
+
+                    sectionTitle("Recent")
+                    VStack(spacing: 8) {
+                        ForEach(Self.recentSamples) { item in
+                            fileActionRow(
+                                name: item.name,
+                                systemImage: item.systemImage,
+                                subtitle: item.time,
+                                onOpen: { openDrivePath("/drive/recent") },
+                                onShare: { openDriveSearch(query: item.name) }
+                            )
+                        }
+                    }
+
+                    sectionTitle("Starred")
+                    VStack(spacing: 8) {
+                        ForEach(Self.starredSamples) { item in
+                            fileActionRow(
+                                name: item.name,
+                                systemImage: item.systemImage,
+                                subtitle: "Starred",
+                                onOpen: { openDrivePath("/drive/starred") },
+                                onShare: { openDriveSearch(query: item.name) }
+                            )
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity, alignment: .leading)
+                .padding(.bottom, 6)
+            }
+            .padding(14)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .disabled(isPreview)
+        .opacity(isPreview ? 0.85 : 1)
+    }
+
+    private func sectionTitle(_ text: String) -> some View {
+        Text(text)
+            .font(.system(size: 11, weight: .bold))
+            .foregroundStyle(.white.opacity(0.62))
+    }
+
+    private func fileActionRow(
+        name: String,
+        systemImage: String,
+        subtitle: String,
+        onOpen: @escaping () -> Void,
+        onShare: @escaping () -> Void
+    ) -> some View {
+        VStack(alignment: .leading, spacing: 8) {
+            HStack(spacing: 10) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(Color(red: 0.75, green: 0.92, blue: 0.82))
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(name)
+                        .font(.system(size: 12, weight: .bold))
+                        .foregroundStyle(.white.opacity(0.95))
+                        .lineLimit(1)
+                    Text(subtitle)
+                        .font(.system(size: 10, weight: .medium))
+                        .foregroundStyle(.white.opacity(0.5))
+                }
+                Spacer(minLength: 0)
+            }
+            HStack(spacing: 8) {
+                smallAction(title: "Open", systemImage: "arrow.up.right.square", action: onOpen)
+                smallAction(title: "Share", systemImage: "square.and.arrow.up", action: onShare)
+            }
+        }
+        .padding(10)
+        .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.24)))
+    }
+
+    private func smallAction(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            action()
+        }) {
+            HStack(spacing: 4) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 10, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 10, weight: .bold))
+            }
+            .foregroundStyle(.white.opacity(0.9))
+            .padding(.horizontal, 10)
+            .padding(.vertical, 6)
+            .background(Capsule().fill(Color.white.opacity(0.14)))
+        }
+        .buttonStyle(.plain)
+        .disabled(isPreview)
+        .allowsHitTesting(!isPreview)
+    }
+
+    // MARK: - Large (interactive)
+
+    @State private var searchText = ""
+    @State private var webURL: URL
+
+    init(app: LauncherApp, mode: DriveWidgetMode, isPreview: Bool = false) {
+        self.app = app
+        self.mode = mode
+        self.isPreview = isPreview
+        let base = app.webURL ?? URL(string: "https://drive.google.com")!
+        _webURL = State(initialValue: Self.defaultDriveURL(base: base))
+    }
+
+    private static func defaultDriveURL(base: URL) -> URL {
+        WidgetDeepLinkURL.resolved(base: base, actionPath: "/drive/home", accountSlot: 0)
+    }
+
+    private var interactiveBody: some View {
+        Group {
+            if isPreview {
+                interactivePreviewChrome
+            } else {
+                interactiveLiveChrome
+            }
+        }
+    }
+
+    private var interactivePreviewChrome: some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(
+                    LinearGradient(
+                        colors: [Color(red: 0.1, green: 0.28, blue: 0.22), Color(red: 0.08, green: 0.14, blue: 0.22)],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+            VStack(spacing: 12) {
+                AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("Google Drive")
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.92))
+                Text("Search, folders, previews, and create options load on your desktop.")
+                    .font(.system(size: 11, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.65))
+                    .multilineTextAlignment(.center)
+                    .padding(.horizontal, 8)
+            }
+            .padding(16)
+        }
+    }
+
+    private var interactiveLiveChrome: some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(Color.black.opacity(0.32))
+
+            VStack(spacing: 0) {
+                VStack(spacing: 8) {
+                    interactiveHeader
+                    interactiveSearchBar
+                    folderChipsRow
+                    createAndUploadRow
+                }
+                .padding(.horizontal, 6)
+                .padding(.vertical, 8)
+                .background(
+                    RoundedRectangle(cornerRadius: 16, style: .continuous)
+                        .fill(Color(red: 0.06, green: 0.1, blue: 0.09))
+                )
+                .zIndex(20)
+
+                DriveWebWidgetView(url: $webURL)
+                    .frame(minHeight: 280)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
+                    .zIndex(0)
+            }
+            .padding(10)
+        }
+    }
+
+    private var interactiveHeader: some View {
+        HStack(spacing: 8) {
+            AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+            Text("Drive")
+                .font(.system(size: 14, weight: .bold))
+                .foregroundStyle(.white.opacity(0.95))
+            Spacer(minLength: 0)
+            Button(action: {
+                NSApp.activate(ignoringOtherApps: true)
+                openDrivePath("/drive/home")
+            }) {
+                Image(systemName: "externaldrive.fill")
+                    .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 Drive in browser")
+        }
+    }
+
+    private var interactiveSearchBar: some View {
+        HStack(spacing: 8) {
+            Image(systemName: "magnifyingglass")
+                .font(.system(size: 12, weight: .semibold))
+                .foregroundStyle(.white.opacity(0.55))
+            TextField("Search in Drive", text: $searchText)
+                .textFieldStyle(.plain)
+                .font(.system(size: 12, weight: .medium))
+                .foregroundStyle(.white.opacity(0.95))
+                .onSubmit { submitSearch() }
+            Button("Go") { submitSearch() }
+                .font(.system(size: 11, weight: .bold))
+                .foregroundStyle(Color(red: 0.4, green: 0.85, blue: 0.65))
+                .buttonStyle(.plain)
+        }
+        .padding(.horizontal, 10)
+        .padding(.vertical, 8)
+        .background(
+            RoundedRectangle(cornerRadius: 10, style: .continuous)
+                .fill(Color.white.opacity(0.08))
+        )
+    }
+
+    private var folderChipsRow: some View {
+        ScrollView(.horizontal, showsIndicators: false) {
+            HStack(spacing: 8) {
+                driveChip("My Drive", path: "/drive/my-drive")
+                driveChip("Recent", path: "/drive/recent")
+                driveChip("Starred", path: "/drive/starred")
+                driveChip("Shared", path: "/drive/shared-with-me")
+                driveChip("Shared drives", path: "/drive/shared-drives")
+                driveChip("Computers", path: "/drive/computers")
+                driveChip("Trash", path: "/drive/trash")
+            }
+        }
+    }
+
+    private var createAndUploadRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Create")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.55))
+            ScrollView(.horizontal, showsIndicators: false) {
+                HStack(spacing: 8) {
+                    createChip("Doc", systemImage: "doc.text", url: "https://docs.google.com/document/create")
+                    createChip("Sheet", systemImage: "tablecells", url: "https://sheets.google.com/spreadsheets/create")
+                    createChip("Slides", systemImage: "rectangle.on.rectangle", url: "https://slides.google.com/presentation/create")
+                    createChip("Form", systemImage: "list.bullet.rectangle", url: "https://docs.google.com/forms/create")
+                    Button(action: {
+                        NSApp.activate(ignoringOtherApps: true)
+                        openDrivePath("/drive/home")
+                    }) {
+                        HStack(spacing: 4) {
+                            Image(systemName: "arrow.up.doc")
+                            Text("Upload")
+                        }
+                        .font(.system(size: 10, weight: .bold))
+                        .foregroundStyle(.white.opacity(0.92))
+                        .padding(.horizontal, 10)
+                        .padding(.vertical, 6)
+                        .background(Capsule().fill(Color.white.opacity(0.14)))
+                    }
+                    .buttonStyle(.plain)
+                    .help("Open Drive to upload files")
+                }
+            }
+        }
+    }
+
+    private func driveChip(_ title: String, path: String) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            navigateWeb(to: path)
+        }) {
+            Text(title)
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.92))
+                .padding(.horizontal, 10)
+                .padding(.vertical, 6)
+                .background(Capsule().fill(Color.white.opacity(0.12)))
+        }
+        .buttonStyle(.plain)
+    }
+
+    private func createChip(_ title: String, systemImage: String, url: String) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            guard let u = URL(string: url) else { return }
+            InAppBrowserWindowManager.shared.open(url: u, title: app.name)
+        }) {
+            HStack(spacing: 4) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 10, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 10, weight: .bold))
+            }
+            .foregroundStyle(.white.opacity(0.92))
+            .padding(.horizontal, 10)
+            .padding(.vertical, 6)
+            .background(Capsule().fill(Color.white.opacity(0.12)))
+        }
+        .buttonStyle(.plain)
+    }
+
+    // MARK: - Shared
+
+    private var driveGradientBackground: some View {
+        LinearGradient(
+            colors: [
+                Color(red: 0.12, green: 0.36, blue: 0.28),
+                Color(red: 0.08, green: 0.16, blue: 0.28),
+            ],
+            startPoint: .topLeading,
+            endPoint: .bottomTrailing
+        )
+    }
+
+    private func drivePrimaryButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            action()
+        }) {
+            HStack(spacing: 8) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 13, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 13, weight: .bold))
+            }
+            .foregroundStyle(Color(red: 0.06, green: 0.12, blue: 0.1))
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 11)
+            .background(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .fill(Color.white.opacity(0.9))
+            )
+        }
+        .buttonStyle(.plain)
+        .disabled(isPreview)
+        .allowsHitTesting(!isPreview)
+    }
+
+    // MARK: - Navigation
+
+    private func openDrivePath(_ path: String) {
+        guard !isPreview else { return }
+        let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+
+    private func openDriveSearch(query: String) {
+        guard !isPreview else { return }
+        let encoded = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? query
+        let path = "/drive/search?q=\(encoded)"
+        openDrivePath(path)
+    }
+
+    private func navigateWeb(to path: String) {
+        let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
+        DispatchQueue.main.async {
+            webURL = url
+        }
+    }
+
+    private func submitSearch() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return }
+        let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? q
+        navigateWeb(to: "/drive/search?q=\(encoded)")
+    }
+}
+
+// MARK: - WKWebView
+
+private final class DriveWebViewClippingContainer: NSView {
+    override var isFlipped: Bool { true }
+}
+
+private struct DriveWebWidgetView: NSViewRepresentable {
+    @Binding var url: URL
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator()
+    }
+
+    func makeNSView(context: Context) -> NSView {
+        let container = DriveWebViewClippingContainer()
+        container.wantsLayer = true
+        container.layer?.masksToBounds = true
+        container.layer?.cornerRadius = 12
+        container.layer?.borderWidth = 0
+
+        let config = WKWebViewConfiguration()
+        config.defaultWebpagePreferences.allowsContentJavaScript = true
+        let wv = WKWebView(frame: .zero, configuration: config)
+        wv.translatesAutoresizingMaskIntoConstraints = false
+        wv.focusRingType = .none
+        wv.customUserAgent =
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
+        wv.navigationDelegate = context.coordinator
+
+        container.addSubview(wv)
+        NSLayoutConstraint.activate([
+            wv.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            wv.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            wv.topAnchor.constraint(equalTo: container.topAnchor),
+            wv.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+        ])
+
+        context.coordinator.webView = wv
+        context.coordinator.lastLoadedURLString = url.absoluteString
+        wv.load(URLRequest(url: url))
+        DispatchQueue.main.async {
+            Self.stripWebKitOuterChrome(wv)
+        }
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+            Self.stripWebKitOuterChrome(wv)
+        }
+        return container
+    }
+
+    func updateNSView(_ container: NSView, context: Context) {
+        guard let wv = context.coordinator.webView else { return }
+        let next = url.absoluteString
+        guard context.coordinator.lastLoadedURLString != next else { return }
+        context.coordinator.lastLoadedURLString = next
+        wv.load(URLRequest(url: url))
+    }
+
+    private static func stripWebKitOuterChrome(_ webView: WKWebView) {
+        webView.focusRingType = .none
+        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(webView)
+    }
+
+    final class Coordinator: NSObject, WKNavigationDelegate {
+        weak var webView: WKWebView?
+        var lastLoadedURLString: String?
+
+        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+            DriveWebWidgetView.stripWebKitOuterChrome(webView)
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
+                DriveWebWidgetView.stripWebKitOuterChrome(webView)
+            }
+        }
+    }
+}

+ 28 - 29
google_apps/Widgets/WidgetTemplates.swift

@@ -25,6 +25,15 @@ enum GmailWidgetMode: Hashable, Sendable {
     case interactive
 }
 
+enum DriveWidgetMode: Hashable, Sendable {
+    /// Sample recent files and shortcuts to Drive recent.
+    case recentQuick
+    /// Recent + starred sample rows with open / share.
+    case filesPreview
+    /// Embedded Google Drive (`WKWebView`) with search, folders, create, and upload shortcut.
+    case interactive
+}
+
 enum WidgetLayoutMode: Hashable {
     case iconOnly(title: String)
     case actionsRow
@@ -33,6 +42,8 @@ enum WidgetLayoutMode: Hashable {
     case interactiveMap
     /// Tiered Gmail: compact preview, inbox list, or full embedded inbox.
     case gmail(GmailWidgetMode)
+    /// Tiered Google Drive: recent files, starred preview, or full embedded Drive.
+    case drive(DriveWidgetMode)
 }
 
 struct WidgetVariant: Identifiable, Hashable {
@@ -75,34 +86,9 @@ enum WidgetTemplates {
             ]
         case .drive:
             return [
-                v("drive_small", "Quick access", .small, false, .actionsRow, [
-                    a("recent", "Recent", "clock.arrow.circlepath", "/drive/recent"),
-                    a("mydrive", "My Drive", "externaldrive.fill", "/drive/my-drive"),
-                    a("search", "Search", "magnifyingglass", "/drive/search"),
-                ]),
-                v("drive_medium", "Files & sharing", .medium, true, .actionsGrid(columns: 2), [
-                    a("mydrive", "My Drive", "externaldrive.fill", "/drive/my-drive"),
-                    a("recent", "Recent", "clock.arrow.circlepath", "/drive/recent"),
-                    a("shared", "Shared", "person.2", "/drive/shared-with-me"),
-                    a("newdoc", "New Doc", "doc.text", "https://docs.google.com/document/create"),
-                ]),
-                v("drive_large", "Drive hub", .large, true, .actionsGrid(columns: 2), [
-                    a("mydrive", "My Drive", "externaldrive.fill", "/drive/my-drive"),
-                    a("recent", "Recent", "clock.arrow.circlepath", "/drive/recent"),
-                    a("search", "Search", "magnifyingglass", "/drive/search"),
-                    a("shared", "Shared", "person.2", "/drive/shared-with-me"),
-                    a("shareddrives", "Shared drives", "person.3.fill", "/drive/shared-drives"),
-                    a("starred", "Starred", "star", "/drive/starred"),
-                    a("newdoc", "New Doc", "doc.text.fill", "https://docs.google.com/document/create"),
-                    a("newsheet", "New Sheet", "tablecells.fill", "https://sheets.google.com/spreadsheets/create"),
-                    a("newslide", "New Slides", "rectangle.on.rectangle.fill", "https://slides.google.com/presentation/create"),
-                    a("pdfs", "PDFs", "doc.text.fill", "/drive/search?q=type:pdf"),
-                    a("images", "Images", "photo", "/drive/search?q=type:image"),
-                    a("videos", "Videos", "film", "/drive/search?q=type:video"),
-                    a("trash", "Trash", "trash", "/drive/trash"),
-                    a("storage", "Storage", "internaldrive", "/settings/storage"),
-                    a("settings", "Settings", "gearshape", "/drive/settings"),
-                ]),
+                v("drive_small", "Recent files", .small, false, .drive(.recentQuick), []),
+                v("drive_medium", "Files & sharing", .medium, false, .drive(.filesPreview), []),
+                v("drive_large", "Full Drive", .large, false, .drive(.interactive), []),
             ]
         case .docs:
             return [
@@ -589,11 +575,12 @@ extension WidgetLayoutMode {
         switch self {
         case .interactiveMap: true
         case .gmail(.interactive): true
+        case .drive(.interactive): true
         default: false
         }
     }
 
-    /// Fixed sizes for tiered Gmail widgets (variant size already matches the tier).
+    /// Fixed sizes for tiered Gmail / Drive widgets (variant size already matches the tier).
     func desktopPanelSizeOverride() -> CGSize? {
         switch self {
         case .gmail(let mode):
@@ -602,6 +589,12 @@ extension WidgetLayoutMode {
             case .inboxPreview: return CGSize(width: 420, height: 480)
             case .interactive: return CGSize(width: 540, height: 580)
             }
+        case .drive(let mode):
+            switch mode {
+            case .recentQuick: return CGSize(width: 230, height: 260)
+            case .filesPreview: return CGSize(width: 420, height: 480)
+            case .interactive: return CGSize(width: 540, height: 580)
+            }
         default:
             return nil
         }
@@ -615,6 +608,12 @@ extension WidgetLayoutMode {
             case .inboxPreview: return CGSize(width: 380, height: 420)
             case .interactive: return CGSize(width: 480, height: 440)
             }
+        case .drive(let mode):
+            switch mode {
+            case .recentQuick: return CGSize(width: 210, height: 230)
+            case .filesPreview: return CGSize(width: 380, height: 420)
+            case .interactive: return CGSize(width: 480, height: 440)
+            }
         default:
             return nil
         }