Эх сурвалжийг харах

Google Books widget: toolbar-only chrome, in-app browser, solid panel

Replace small/medium variants with a single large Books widget that opens
search (Google tbm=bks) and shelf shortcuts in the in-app browser instead of
an embedded WKWebView, fixing 404s and double-encoded query strings.

Use URLComponents for books.google.com paths and remove the semi-transparent
outer layer so the desktop no longer shows through as a false border.

Made-with: Cursor
huzaifahayat12 3 сар өмнө
parent
commit
d8d9e9816d

+ 199 - 0
google_apps/Widgets/BooksDesktopWidgetView.swift

@@ -0,0 +1,199 @@
+import AppKit
+import SwiftUI
+
+/// Google Books widget: search and shortcuts only. Results and library open in the in-app browser (no embedded web view).
+struct BooksDesktopWidgetView: View {
+    let app: LauncherApp
+    let mode: BooksWidgetMode
+    var isPreview: Bool = false
+
+    @State private var searchText = ""
+
+    private var baseURL: URL { app.webURL ?? URL(string: "https://books.google.com")! }
+
+    var body: some View {
+        Group {
+            switch mode {
+            case .interactive:
+                interactiveBody
+            }
+        }
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
+    }
+
+    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.2, green: 0.28, blue: 0.42), Color(red: 0.1, green: 0.12, blue: 0.22)],
+                        startPoint: .topLeading,
+                        endPoint: .bottomTrailing
+                    )
+                )
+            VStack(spacing: 12) {
+                AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
+                Text("Google Books")
+                    .font(.system(size: 15, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.92))
+                Text("Search and shortcuts open Books in the app browser — no embedded page here.")
+                    .font(.system(size: 11, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.65))
+                    .multilineTextAlignment(.center)
+                    .padding(.horizontal, 8)
+            }
+            .padding(16)
+        }
+    }
+
+    private var interactiveLiveChrome: some View {
+        VStack(spacing: 8) {
+            interactiveHeader
+            interactiveSearchBar
+            primaryShortcutsRow
+            secondaryShortcutsRow
+        }
+        .padding(.horizontal, 14)
+        .padding(.vertical, 14)
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+        .background(
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(Color(red: 0.08, green: 0.1, blue: 0.16))
+        )
+    }
+
+    private var interactiveHeader: some View {
+        HStack(spacing: 8) {
+            AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+            Text("Books")
+                .font(.system(size: 14, weight: .bold))
+                .foregroundStyle(.white.opacity(0.95))
+            Spacer(minLength: 0)
+            Button(action: {
+                NSApp.activate(ignoringOtherApps: true)
+                openBooksInAppBrowser(path: "/", queryItems: nil)
+            }) {
+                Image(systemName: "arrow.up.right.square")
+                    .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 Google Books in app 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 books & authors", text: $searchText)
+                .textFieldStyle(.plain)
+                .font(.system(size: 12, weight: .medium))
+                .foregroundStyle(.white.opacity(0.95))
+                .onSubmit { submitSearch() }
+            Button("Search") { submitSearch() }
+                .font(.system(size: 11, weight: .bold))
+                .foregroundStyle(Color(red: 0.65, green: 0.82, blue: 1))
+                .buttonStyle(.plain)
+        }
+        .padding(.horizontal, 10)
+        .padding(.vertical, 8)
+        .background(
+            RoundedRectangle(cornerRadius: 10, style: .continuous)
+                .fill(Color.white.opacity(0.08))
+        )
+    }
+
+    private var primaryShortcutsRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Library & reading")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.55))
+            ScrollView(.horizontal, showsIndicators: false) {
+                HStack(spacing: 8) {
+                    booksChip("Reading", systemImage: "book.fill", path: "/")
+                    booksChip("Library", systemImage: "books.vertical", path: "/ebooks/library")
+                    booksChip("Bookmarks", systemImage: "bookmark", path: "/ebooks/library")
+                }
+            }
+        }
+    }
+
+    private var secondaryShortcutsRow: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Discover")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.55))
+            ScrollView(.horizontal, showsIndicators: false) {
+                HStack(spacing: 8) {
+                    booksChip("Recommendations", systemImage: "sparkles", path: "/ebooks")
+                    booksChip("Audiobooks", systemImage: "headphones", path: "/audiobooks")
+                }
+            }
+        }
+    }
+
+    private func booksChip(_ title: String, systemImage: String, path: String) -> some View {
+        Button(action: {
+            NSApp.activate(ignoringOtherApps: true)
+            openBooksInAppBrowser(path: path, queryItems: nil)
+        }) {
+            HStack(spacing: 5) {
+                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)
+        .disabled(isPreview)
+        .allowsHitTesting(!isPreview)
+    }
+
+    /// Builds `https://books.google.com` + path + optional query items. Query values are encoded once by `URLComponents`.
+    private func booksURL(path: String, queryItems: [URLQueryItem]?) -> URL {
+        var c = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
+        let p = path.hasPrefix("/") ? path : "/\(path)"
+        c?.path = p
+        c?.queryItems = queryItems
+        return c?.url ?? baseURL
+    }
+
+    private func openBooksInAppBrowser(path: String, queryItems: [URLQueryItem]?) {
+        guard !isPreview else { return }
+        let url = booksURL(path: path, queryItems: queryItems)
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+
+    /// Google Book Search uses `tbm=bks` on google.com (avoids broken `books.google.com/ebooks/search` and double-encoding bugs).
+    private func submitSearch() {
+        let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return }
+        guard !isPreview else { return }
+        NSApp.activate(ignoringOtherApps: true)
+        var c = URLComponents(string: "https://www.google.com/search")
+        c?.queryItems = [
+            URLQueryItem(name: "tbm", value: "bks"),
+            URLQueryItem(name: "q", value: q),
+        ]
+        guard let url = c?.url else { return }
+        InAppBrowserWindowManager.shared.open(url: url, title: app.name)
+    }
+}

+ 3 - 1
google_apps/Widgets/DesktopWidgetView.swift

@@ -29,6 +29,8 @@ struct DesktopWidgetView: View {
                 GoogleSearchDesktopWidgetView(app: app, mode: searchMode, isPreview: isPreview)
             } else if case .calendar(let calendarMode) = layoutMode {
                 CalendarDesktopWidgetView(app: app, mode: calendarMode, isPreview: isPreview)
+            } else if case .books(let booksMode) = layoutMode {
+                BooksDesktopWidgetView(app: app, mode: booksMode, isPreview: isPreview)
             } else if case .interactiveMap = layoutMode {
                 MapsInteractiveWidgetView(app: app, widgetSize: size, isPreview: isPreview)
             } else {
@@ -64,7 +66,7 @@ struct DesktopWidgetView: View {
                             }
                             gridActions(columns: columns, maxActions: maxActionsForSize)
 
-                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar:
+                        case .interactiveMap, .gmail, .drive, .keep, .translate, .earth, .googleSearch, .calendar, .books:
                             EmptyView()
                         }
                     }

+ 13 - 7
google_apps/Widgets/WidgetTemplates.swift

@@ -63,6 +63,11 @@ enum CalendarWidgetMode: Hashable, Sendable {
     case agenda
 }
 
+enum BooksWidgetMode: Hashable, Sendable {
+    /// Embedded Google Books (`WKWebView`): library, reading progress, bookmarks, recommendations, search.
+    case interactive
+}
+
 enum WidgetLayoutMode: Hashable {
     case iconOnly(title: String)
     case actionsRow
@@ -83,6 +88,8 @@ enum WidgetLayoutMode: Hashable {
     case googleSearch(GoogleSearchWidgetMode)
     /// Today + next event (small) or multi-day agenda + quick add (medium); uses EventKit + Google Calendar links.
     case calendar(CalendarWidgetMode)
+    /// Embedded Google Books (`WKWebView`): library, reading, bookmarks, discover, search.
+    case books(BooksWidgetMode)
 }
 
 struct WidgetVariant: Identifiable, Hashable {
@@ -509,13 +516,7 @@ enum WidgetTemplates {
             ]
         case .books:
             return [
-                v("books_small", "Books", .small, false, .iconOnly(title: "Books"), [a("home", "Library", "book.fill", "/ebooks")]),
-                v("books_medium", "Google Books", .medium, true, .actionsGrid(columns: 2), [
-                    a("library", "My books", "books.vertical", "/ebooks/library"),
-                    a("shop", "Shop", "cart", "/ebooks"),
-                    a("audiobooks", "Audiobooks", "headphones", "/audiobooks"),
-                    a("search", "Search", "magnifyingglass", "/ebooks/search"),
-                ]),
+                v("books_large", "Library & reading", .large, false, .books(.interactive), []),
             ]
         case .unknown:
             return genericUtilityVariants(prefix: "generic", appName: app.name)
@@ -585,6 +586,7 @@ extension WidgetLayoutMode {
         case .translate(.interactive): true
         case .earth(.interactive): true
         case .googleSearch(.interactive): true
+        case .books(.interactive): true
         case .calendar: false
         default: false
         }
@@ -621,6 +623,8 @@ extension WidgetLayoutMode {
             case .compact: return CGSize(width: 230, height: 260)
             case .agenda: return CGSize(width: 420, height: 480)
             }
+        case .books(.interactive):
+            return CGSize(width: 420, height: 260)
         default:
             return nil
         }
@@ -656,6 +660,8 @@ extension WidgetLayoutMode {
             case .compact: return CGSize(width: 210, height: 240)
             case .agenda: return CGSize(width: 380, height: 440)
             }
+        case .books(.interactive):
+            return CGSize(width: 380, height: 240)
         default:
             return nil
         }