|
|
@@ -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)
|
|
|
+ }
|
|
|
+}
|