BooksDesktopWidgetView.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import AppKit
  2. import SwiftUI
  3. /// Google Books widget: search and shortcuts only. Results and library open in the in-app browser (no embedded web view).
  4. struct BooksDesktopWidgetView: View {
  5. let app: LauncherApp
  6. let mode: BooksWidgetMode
  7. var isPreview: Bool = false
  8. @State private var searchText = ""
  9. private var baseURL: URL { app.webURL ?? URL(string: "https://books.google.com")! }
  10. var body: some View {
  11. Group {
  12. switch mode {
  13. case .interactive:
  14. interactiveBody
  15. }
  16. }
  17. .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
  18. }
  19. private var interactiveBody: some View {
  20. Group {
  21. if isPreview {
  22. interactivePreviewChrome
  23. } else {
  24. interactiveLiveChrome
  25. }
  26. }
  27. }
  28. private var interactivePreviewChrome: some View {
  29. ZStack {
  30. RoundedRectangle(cornerRadius: 22, style: .continuous)
  31. .fill(
  32. LinearGradient(
  33. colors: [Color(red: 0.2, green: 0.28, blue: 0.42), Color(red: 0.1, green: 0.12, blue: 0.22)],
  34. startPoint: .topLeading,
  35. endPoint: .bottomTrailing
  36. )
  37. )
  38. VStack(spacing: 12) {
  39. AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
  40. Text("Google Books")
  41. .font(.system(size: 15, weight: .bold))
  42. .foregroundStyle(.white.opacity(0.92))
  43. Text("Search and shortcuts open Books in the app browser — no embedded page here.")
  44. .font(.system(size: 11, weight: .medium))
  45. .foregroundStyle(.white.opacity(0.65))
  46. .multilineTextAlignment(.center)
  47. .padding(.horizontal, 8)
  48. }
  49. .padding(16)
  50. }
  51. }
  52. private var interactiveLiveChrome: some View {
  53. VStack(spacing: 8) {
  54. interactiveHeader
  55. interactiveSearchBar
  56. primaryShortcutsRow
  57. secondaryShortcutsRow
  58. }
  59. .padding(.horizontal, 14)
  60. .padding(.vertical, 14)
  61. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  62. .background(
  63. RoundedRectangle(cornerRadius: 22, style: .continuous)
  64. .fill(Color(red: 0.08, green: 0.1, blue: 0.16))
  65. )
  66. }
  67. private var interactiveHeader: some View {
  68. HStack(spacing: 8) {
  69. AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
  70. Text("Books")
  71. .font(.system(size: 14, weight: .bold))
  72. .foregroundStyle(.white.opacity(0.95))
  73. Spacer(minLength: 0)
  74. Button(action: {
  75. NSApp.activate(ignoringOtherApps: true)
  76. openBooksInAppBrowser(path: "/", queryItems: nil)
  77. }) {
  78. Image(systemName: "arrow.up.right.square")
  79. .font(.system(size: 13, weight: .semibold))
  80. .foregroundStyle(.white.opacity(0.9))
  81. .padding(8)
  82. .background(Circle().fill(Color.white.opacity(0.12)))
  83. }
  84. .buttonStyle(.plain)
  85. .help("Open Google Books in app browser")
  86. }
  87. }
  88. private var interactiveSearchBar: some View {
  89. HStack(spacing: 8) {
  90. Image(systemName: "magnifyingglass")
  91. .font(.system(size: 12, weight: .semibold))
  92. .foregroundStyle(.white.opacity(0.55))
  93. TextField("Search books & authors", text: $searchText)
  94. .textFieldStyle(.plain)
  95. .font(.system(size: 12, weight: .medium))
  96. .foregroundStyle(.white.opacity(0.95))
  97. .onSubmit { submitSearch() }
  98. Button("Search") { submitSearch() }
  99. .font(.system(size: 11, weight: .bold))
  100. .foregroundStyle(Color(red: 0.65, green: 0.82, blue: 1))
  101. .buttonStyle(.plain)
  102. }
  103. .padding(.horizontal, 10)
  104. .padding(.vertical, 8)
  105. .background(
  106. RoundedRectangle(cornerRadius: 10, style: .continuous)
  107. .fill(Color.white.opacity(0.08))
  108. )
  109. }
  110. private var primaryShortcutsRow: some View {
  111. VStack(alignment: .leading, spacing: 6) {
  112. Text("Library & reading")
  113. .font(.system(size: 10, weight: .bold))
  114. .foregroundStyle(.white.opacity(0.55))
  115. ScrollView(.horizontal, showsIndicators: false) {
  116. HStack(spacing: 8) {
  117. booksChip("Reading", systemImage: "book.fill", path: "/")
  118. booksChip("Library", systemImage: "books.vertical", path: "/ebooks/library")
  119. booksChip("Bookmarks", systemImage: "bookmark", path: "/ebooks/library")
  120. }
  121. }
  122. }
  123. }
  124. private var secondaryShortcutsRow: some View {
  125. VStack(alignment: .leading, spacing: 6) {
  126. Text("Discover")
  127. .font(.system(size: 10, weight: .bold))
  128. .foregroundStyle(.white.opacity(0.55))
  129. ScrollView(.horizontal, showsIndicators: false) {
  130. HStack(spacing: 8) {
  131. booksChip("Recommendations", systemImage: "sparkles", path: "/ebooks")
  132. booksChip("Audiobooks", systemImage: "headphones", path: "/audiobooks")
  133. }
  134. }
  135. }
  136. }
  137. private func booksChip(_ title: String, systemImage: String, path: String) -> some View {
  138. Button(action: {
  139. NSApp.activate(ignoringOtherApps: true)
  140. openBooksInAppBrowser(path: path, queryItems: nil)
  141. }) {
  142. HStack(spacing: 5) {
  143. Image(systemName: systemImage)
  144. .font(.system(size: 10, weight: .semibold))
  145. Text(title)
  146. .font(.system(size: 10, weight: .bold))
  147. }
  148. .foregroundStyle(.white.opacity(0.92))
  149. .padding(.horizontal, 10)
  150. .padding(.vertical, 6)
  151. .background(Capsule().fill(Color.white.opacity(0.12)))
  152. }
  153. .buttonStyle(.plain)
  154. .disabled(isPreview)
  155. .allowsHitTesting(!isPreview)
  156. }
  157. /// Builds `https://books.google.com` + path + optional query items. Query values are encoded once by `URLComponents`.
  158. private func booksURL(path: String, queryItems: [URLQueryItem]?) -> URL {
  159. var c = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
  160. let p = path.hasPrefix("/") ? path : "/\(path)"
  161. c?.path = p
  162. c?.queryItems = queryItems
  163. return c?.url ?? baseURL
  164. }
  165. private func openBooksInAppBrowser(path: String, queryItems: [URLQueryItem]?) {
  166. guard !isPreview else { return }
  167. let url = booksURL(path: path, queryItems: queryItems)
  168. InAppBrowserWindowManager.shared.open(url: url, title: app.name)
  169. }
  170. /// Google Book Search uses `tbm=bks` on google.com (avoids broken `books.google.com/ebooks/search` and double-encoding bugs).
  171. private func submitSearch() {
  172. let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
  173. guard !q.isEmpty else { return }
  174. guard !isPreview else { return }
  175. NSApp.activate(ignoringOtherApps: true)
  176. var c = URLComponents(string: "https://www.google.com/search")
  177. c?.queryItems = [
  178. URLQueryItem(name: "tbm", value: "bks"),
  179. URLQueryItem(name: "q", value: q),
  180. ]
  181. guard let url = c?.url else { return }
  182. InAppBrowserWindowManager.shared.open(url: url, title: app.name)
  183. }
  184. }