Selaa lähdekoodia

Add Google Docs dashboard widget (Forms-style)

- DocsDesktopWidgetView: find field, docs.new, templates, browse list; in-app browser
- WidgetTemplates: DocsWidgetMode, docs(.dashboard), small + large variants; panel size overrides
- DesktopWidgetView: route .docs layout mode

Made-with: Cursor
huzaifahayat12 3 kuukautta sitten
vanhempi
sitoutus
e59b579059

+ 3 - 1
google_apps/Widgets/DesktopWidgetView.swift

@@ -41,6 +41,8 @@ struct DesktopWidgetView: View {
                 NewsDesktopWidgetView(app: app, mode: newsMode, isPreview: isPreview)
             } else if case .forms(let formsMode) = layoutMode {
                 FormsDesktopWidgetView(app: app, mode: formsMode, isPreview: isPreview)
+            } else if case .docs(let docsMode) = layoutMode {
+                DocsDesktopWidgetView(app: app, mode: docsMode, isPreview: isPreview)
             } else if case .sheets(let sheetsMode) = layoutMode {
                 SheetsDesktopWidgetView(app: app, mode: sheetsMode, isPreview: isPreview)
             } else if case .slides(let slidesMode) = layoutMode {
@@ -84,7 +86,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .youtube, .meet, .news, .forms, .sheets, .slides, .googlePlay, .gemini:
+                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books, .travel, .youtube, .meet, .news, .forms, .docs, .sheets, .slides, .googlePlay, .gemini:
                             EmptyView()
                         }
                     }

+ 163 - 0
google_apps/Widgets/DocsDesktopWidgetView.swift

@@ -0,0 +1,163 @@
+import AppKit
+import SwiftUI
+
+private let docsAccent = Color(red: 0.10, green: 0.52, blue: 0.96)
+private let docsPanelBg = Color(red: 0.07, green: 0.09, blue: 0.14)
+
+private enum DocsCreateURL {
+    static let newDoc = "https://docs.new"
+    static let templates = "https://docs.google.com/templates?type=docs"
+    static let homeList = "https://docs.google.com/document/u/0/"
+}
+
+/// Large Google Docs dashboard: find + create shortcuts (opens real Docs URLs in-app).
+struct DocsDesktopWidgetView: View {
+    let app: LauncherApp
+    let mode: DocsWidgetMode
+    var isPreview: Bool = false
+
+    @State private var findText: String = ""
+
+    var body: some View {
+        Group {
+            switch mode {
+            case .dashboard:
+                dashboardBody
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    private var dashboardBody: some View {
+        ZStack(alignment: .topLeading) {
+            LinearGradient(
+                colors: [
+                    Color(red: 0.08, green: 0.22, blue: 0.42),
+                    docsPanelBg,
+                ],
+                startPoint: .topLeading,
+                endPoint: .bottomTrailing
+            )
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+            VStack(alignment: .leading, spacing: 12) {
+                headerRow
+                findFieldRow
+                createRow
+                Spacer(minLength: 0)
+            }
+            .padding(14)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .disabled(isPreview)
+        .opacity(isPreview ? 0.88 : 1)
+    }
+
+    private var headerRow: some View {
+        HStack(spacing: 8) {
+            AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
+            VStack(alignment: .leading, spacing: 2) {
+                Text("Google Docs")
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Text("Dashboard")
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.55))
+            }
+            Spacer(minLength: 0)
+            Button(action: { openAbsolute(DocsCreateURL.homeList) }) {
+                Image(systemName: "arrow.up.right.square")
+                    .font(.system(size: 14, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.9))
+                    .padding(8)
+                    .background(Circle().fill(Color.white.opacity(0.12)))
+            }
+            .buttonStyle(.plain)
+            .help("Open Google Docs")
+        }
+    }
+
+    private var findFieldRow: some View {
+        HStack(spacing: 8) {
+            Image(systemName: "magnifyingglass")
+                .font(.system(size: 13, weight: .semibold))
+                .foregroundStyle(.white.opacity(0.5))
+            TextField("Find documents", text: $findText)
+                .textFieldStyle(.plain)
+                .font(.system(size: 13, weight: .medium))
+                .foregroundStyle(.white.opacity(0.95))
+                .onSubmit { submitFind() }
+            Button("Go", action: submitFind)
+                .font(.system(size: 11, weight: .bold))
+                .buttonStyle(.borderedProminent)
+                .tint(docsAccent)
+                .controlSize(.small)
+        }
+        .padding(.horizontal, 10)
+        .padding(.vertical, 8)
+        .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.white.opacity(0.1)))
+    }
+
+    private var createRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            sectionLabel("Create")
+            HStack(spacing: 6) {
+                createPill(title: "Doc", systemImage: "doc.badge.plus", urlString: DocsCreateURL.newDoc, help: "New blank document (docs.new)")
+                createPill(title: "Templates", systemImage: "square.grid.2x2", urlString: DocsCreateURL.templates, help: "Document templates")
+                createPill(title: "Browse", systemImage: "folder", urlString: DocsCreateURL.homeList, help: "All documents")
+            }
+        }
+    }
+
+    private func createPill(title: String, systemImage: String, urlString: String, help: String) -> some View {
+        Button {
+            openAbsolute(urlString)
+        } label: {
+            VStack(spacing: 4) {
+                Image(systemName: systemImage)
+                    .font(.system(size: 16, weight: .semibold))
+                Text(title)
+                    .font(.system(size: 10, weight: .bold))
+            }
+            .foregroundStyle(.white.opacity(0.92))
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 10)
+            .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.white.opacity(0.1)))
+            .overlay(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
+            )
+        }
+        .buttonStyle(.plain)
+        .help(help)
+    }
+
+    private func sectionLabel(_ text: String) -> some View {
+        Text(text)
+            .font(.system(size: 10, weight: .bold))
+            .foregroundStyle(.white.opacity(0.45))
+            .textCase(.uppercase)
+    }
+
+    private func submitFind() {
+        NSApp.activate(ignoringOtherApps: true)
+        let q = findText.trimmingCharacters(in: .whitespacesAndNewlines)
+        var c = URLComponents(string: DocsCreateURL.homeList)!
+        if !q.isEmpty {
+            c.queryItems = [URLQueryItem(name: "q", value: q)]
+        }
+        guard let url = c.url else { return }
+        openInApp(url)
+    }
+
+    private func openAbsolute(_ urlString: String) {
+        guard let url = URL(string: urlString) else { return }
+        NSApp.activate(ignoringOtherApps: true)
+        openInApp(url)
+    }
+
+    private func openInApp(_ url: URL) {
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+}

+ 13 - 16
google_apps/Widgets/WidgetTemplates.swift

@@ -101,6 +101,11 @@ enum FormsWidgetMode: Hashable, Sendable {
     case dashboard
 }
 
+enum DocsWidgetMode: Hashable, Sendable {
+    /// Find + create shortcuts (docs.new, templates, home); opens in-app browser.
+    case dashboard
+}
+
 enum SheetsWidgetMode: Hashable, Sendable {
     /// Find + create shortcuts (sheets.new, templates, home); opens in-app browser.
     case dashboard
@@ -153,6 +158,8 @@ enum WidgetLayoutMode: Hashable {
     case news(NewsWidgetMode)
     /// Google Forms: large interactive dashboard; small/medium use default grids.
     case forms(FormsWidgetMode)
+    /// Google Docs: large dashboard like Forms.
+    case docs(DocsWidgetMode)
     /// Google Sheets: large dashboard like Forms.
     case sheets(SheetsWidgetMode)
     /// Google Slides: large dashboard like Forms.
@@ -210,22 +217,7 @@ enum WidgetTemplates {
         case .docs:
             return [
                 v("docs_small", "Docs", .small, false, .iconOnly(title: "Docs"), [a("home", "Open", "doc.text.fill", "/document/u/0/")]),
-                v("docs_medium", "Docs Shortcuts", .medium, true, .actionsGrid(columns: 2), [
-                    a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
-                    a("new", "New doc", "plus", "/document/create"),
-                    a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
-                    a("search", "Search", "magnifyingglass", "/document/u/0/search"),
-                ]),
-                v("docs_large", "Docs Hub", .large, true, .actionsGrid(columns: 2), [
-                    a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
-                    a("new", "New doc", "plus", "/document/create"),
-                    a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
-                    a("starred", "Starred", "star", "/document/u/0/starred"),
-                    a("shared", "Shared", "person.2", "/document/u/0/shared-with-me"),
-                    a("trash", "Trash", "trash", "/document/u/0/trash"),
-                    a("search", "Search", "magnifyingglass", "/document/u/0/search"),
-                    a("offline", "Offline", "arrow.down.circle", "/document/u/0/offline"),
-                ]),
+                v("docs_large", "Docs dashboard", .large, false, .docs(.dashboard), []),
             ]
         case .sheets:
             return [
@@ -575,6 +567,7 @@ extension WidgetLayoutMode {
         case .youtube(.interactiveNav): true
         case .news(_): true
         case .forms(_): true
+        case .docs(_): true
         case .sheets(_): true
         case .slides(_): true
         case .googlePlay(_): true
@@ -637,6 +630,8 @@ extension WidgetLayoutMode {
             }
         case .forms(.dashboard):
             return CGSize(width: 440, height: 300)
+        case .docs(.dashboard):
+            return CGSize(width: 440, height: 300)
         case .sheets(.dashboard):
             return CGSize(width: 440, height: 300)
         case .slides(.dashboard):
@@ -701,6 +696,8 @@ extension WidgetLayoutMode {
             }
         case .forms(.dashboard):
             return CGSize(width: 380, height: 280)
+        case .docs(.dashboard):
+            return CGSize(width: 380, height: 280)
         case .sheets(.dashboard):
             return CGSize(width: 380, height: 280)
         case .slides(.dashboard):