WidgetTemplates.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import CoreGraphics
  2. import Foundation
  3. struct WidgetAction: Identifiable, Hashable {
  4. let id: String
  5. let title: String
  6. let systemImage: String
  7. /// If nil, opens the app base URL.
  8. let urlPath: String?
  9. init(id: String, title: String, systemImage: String, urlPath: String? = nil) {
  10. self.id = id
  11. self.title = title
  12. self.systemImage = systemImage
  13. self.urlPath = urlPath
  14. }
  15. }
  16. enum GmailWidgetMode: Hashable, Sendable {
  17. /// Unread count (local placeholder) and quick compose.
  18. case compact
  19. /// Sample inbox rows with reply / archive-style shortcuts.
  20. case inboxPreview
  21. /// Embedded Gmail (`WKWebView`) with search, category chips, and folder shortcuts.
  22. case interactive
  23. }
  24. enum DriveWidgetMode: Hashable, Sendable {
  25. /// Sample recent files and shortcuts to Drive recent.
  26. case recentQuick
  27. /// Recent + starred sample rows with open / share.
  28. case filesPreview
  29. /// Shortcut hub: search, folders, quick find, create, and tools — open in app browser (no embedded Drive UI).
  30. case interactive
  31. }
  32. enum KeepWidgetMode: Hashable, Sendable {
  33. /// Recent note snippets and new-note shortcut.
  34. case compact
  35. /// Embedded Google Keep (`WKWebView`) with search, labels, and create options.
  36. case interactive
  37. }
  38. enum TranslateWidgetMode: Hashable, Sendable {
  39. /// Embedded Google Translate (`WKWebView`): text, voice, conversation, history.
  40. case interactive
  41. }
  42. enum EarthWidgetMode: Hashable, Sendable {
  43. /// Embedded Google Earth Web (`WKWebView`): globe, layers, Voyager, projects.
  44. case interactive
  45. }
  46. enum GoogleSearchWidgetMode: Hashable, Sendable {
  47. /// Embedded google.com (`WKWebView`): search, voice, trends, recents.
  48. case interactive
  49. }
  50. enum CalendarWidgetMode: Hashable, Sendable {
  51. /// Today’s date and the next upcoming event (from Apple Calendar / synced accounts).
  52. case compact
  53. /// Multi-day agenda with quick links to create events in Google Calendar.
  54. case agenda
  55. }
  56. enum BooksWidgetMode: Hashable, Sendable {
  57. /// Embedded Google Books (`WKWebView`): library, reading progress, bookmarks, recommendations, search.
  58. case interactive
  59. }
  60. enum TravelWidgetMode: Hashable, Sendable {
  61. /// Toolbar search plus Explore / Flights / Hotels / Vacation rentals shortcuts; in-app browser (no embedded trip UI).
  62. case interactive
  63. }
  64. enum YoutubeWidgetMode: Hashable, Sendable {
  65. /// Search field opens YouTube results in the in-app browser.
  66. case searchBar
  67. /// Home, Shorts, Subscriptions, and extra feed shortcuts in the in-app browser.
  68. case interactiveNav
  69. }
  70. enum WidgetLayoutMode: Hashable {
  71. case iconOnly(title: String)
  72. case actionsRow
  73. case actionsGrid(columns: Int)
  74. /// Embedded Google Maps (`WKWebView`) with search, traffic, and navigation shortcuts.
  75. case interactiveMap
  76. /// Tiered Gmail: compact preview, inbox list, or full embedded inbox.
  77. case gmail(GmailWidgetMode)
  78. /// Tiered Google Drive: recent files, starred preview, or large shortcut hub.
  79. case drive(DriveWidgetMode)
  80. /// Tiered Google Keep: recent notes or full embedded Keep.
  81. case keep(KeepWidgetMode)
  82. /// Embedded Google Translate: full translator UI in-panel.
  83. case translate(TranslateWidgetMode)
  84. /// Embedded Google Earth Web: full globe UI in-panel.
  85. case earth(EarthWidgetMode)
  86. /// Embedded Google Search (`WKWebView`): full search experience in-panel.
  87. case googleSearch(GoogleSearchWidgetMode)
  88. /// Today + next event (small) or multi-day agenda + quick add (medium); uses EventKit + Google Calendar links.
  89. case calendar(CalendarWidgetMode)
  90. /// Embedded Google Books (`WKWebView`): library, reading, bookmarks, discover, search.
  91. case books(BooksWidgetMode)
  92. /// Google Travel: search, toolbar, and category shortcuts; full site in browser (no trip panel).
  93. case travel(TravelWidgetMode)
  94. /// YouTube: medium search bar or large navigation shortcuts; opens in-app browser.
  95. case youtube(YoutubeWidgetMode)
  96. }
  97. struct WidgetVariant: Identifiable, Hashable {
  98. let id: String
  99. let title: String
  100. let size: WidgetSize
  101. let showHeader: Bool
  102. let layoutMode: WidgetLayoutMode
  103. let actions: [WidgetAction]
  104. }
  105. enum WidgetTemplates {
  106. static func variants(for app: LauncherApp) -> [WidgetVariant] {
  107. func v(_ id: String, _ title: String, _ size: WidgetSize, _ showHeader: Bool, _ layout: WidgetLayoutMode, _ actions: [WidgetAction]) -> WidgetVariant {
  108. WidgetVariant(id: id, title: title, size: size, showHeader: showHeader, layoutMode: layout, actions: actions)
  109. }
  110. let profile = WidgetAppProfile.from(app: app)
  111. switch profile {
  112. case .photos:
  113. return [
  114. v("photos_small", "Photo", .small, false, .iconOnly(title: "Photo"), [a("photos", "Photos", "photo.on.rectangle", "/")]),
  115. v("photos_medium", "Google Photo", .medium, false, .iconOnly(title: "Google Photo"), [a("albums", "Albums", "rectangle.stack", "/albums")]),
  116. v("photos_large", "Photos Library", .large, true, .actionsGrid(columns: 2), [
  117. a("photos", "Photos", "photo.on.rectangle", "/"),
  118. a("albums", "Albums", "rectangle.stack", "/albums"),
  119. a("explore", "Explore", "magnifyingglass", "/search"),
  120. a("archive", "Archive", "archivebox", "/archive"),
  121. a("sharing", "Sharing", "person.2", "/sharing"),
  122. a("locked", "Locked Folder", "lock", "/lockedfolder"),
  123. a("favorites", "Favorites", "star", "/favorites"),
  124. a("trash", "Trash", "trash", "/trash"),
  125. ]),
  126. ]
  127. case .gmail:
  128. return [
  129. v("gmail_small", "Unread & compose", .small, false, .gmail(.compact), []),
  130. v("gmail_medium", "Inbox preview", .medium, false, .gmail(.inboxPreview), []),
  131. v("gmail_large", "Full inbox", .large, false, .gmail(.interactive), []),
  132. ]
  133. case .drive:
  134. return [
  135. v("drive_small", "Recent files", .small, false, .drive(.recentQuick), []),
  136. v("drive_medium", "Files & sharing", .medium, false, .drive(.filesPreview), []),
  137. v("drive_large", "Full Drive", .large, false, .drive(.interactive), []),
  138. ]
  139. case .docs:
  140. return [
  141. v("docs_small", "Docs", .small, false, .iconOnly(title: "Docs"), [a("home", "Open", "doc.text.fill", "/document/u/0/")]),
  142. v("docs_medium", "Docs Shortcuts", .medium, true, .actionsGrid(columns: 2), [
  143. a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
  144. a("new", "New doc", "plus", "/document/create"),
  145. a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
  146. a("search", "Search", "magnifyingglass", "/document/u/0/search"),
  147. ]),
  148. v("docs_large", "Docs Hub", .large, true, .actionsGrid(columns: 2), [
  149. a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
  150. a("new", "New doc", "plus", "/document/create"),
  151. a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
  152. a("starred", "Starred", "star", "/document/u/0/starred"),
  153. a("shared", "Shared", "person.2", "/document/u/0/shared-with-me"),
  154. a("trash", "Trash", "trash", "/document/u/0/trash"),
  155. a("search", "Search", "magnifyingglass", "/document/u/0/search"),
  156. a("offline", "Offline", "arrow.down.circle", "/document/u/0/offline"),
  157. ]),
  158. ]
  159. case .sheets:
  160. return [
  161. v("sheets_small", "Sheets", .small, false, .iconOnly(title: "Sheets"), [a("home", "Open", "tablecells.fill", "/spreadsheets/u/0/")]),
  162. v("sheets_medium", "Sheets Shortcuts", .medium, true, .actionsGrid(columns: 2), [
  163. a("recent", "Recent", "clock.arrow.circlepath", "/spreadsheets/u/0/"),
  164. a("new", "New sheet", "plus", "/spreadsheets/create"),
  165. a("templates", "Templates", "square.grid.2x2", "/spreadsheets/u/0/templates"),
  166. a("search", "Search", "magnifyingglass", "/spreadsheets/u/0/search"),
  167. ]),
  168. v("sheets_large", "Sheets Hub", .large, true, .actionsGrid(columns: 2), [
  169. a("recent", "Recent", "clock.arrow.circlepath", "/spreadsheets/u/0/"),
  170. a("new", "New sheet", "plus", "/spreadsheets/create"),
  171. a("templates", "Templates", "square.grid.2x2", "/spreadsheets/u/0/templates"),
  172. a("starred", "Starred", "star", "/spreadsheets/u/0/starred"),
  173. a("shared", "Shared", "person.2", "/spreadsheets/u/0/shared-with-me"),
  174. a("trash", "Trash", "trash", "/spreadsheets/u/0/trash"),
  175. a("search", "Search", "magnifyingglass", "/spreadsheets/u/0/search"),
  176. a("offline", "Offline", "arrow.down.circle", "/spreadsheets/u/0/offline"),
  177. ]),
  178. ]
  179. case .slides:
  180. return [
  181. v("slides_small", "Slides", .small, false, .iconOnly(title: "Slides"), [a("home", "Open", "rectangle.on.rectangle.fill", "/presentation/u/0/")]),
  182. v("slides_medium", "Slides Shortcuts", .medium, true, .actionsGrid(columns: 2), [
  183. a("recent", "Recent", "clock.arrow.circlepath", "/presentation/u/0/"),
  184. a("new", "New deck", "plus", "/presentation/create"),
  185. a("templates", "Templates", "square.grid.2x2", "/presentation/u/0/templates"),
  186. a("search", "Search", "magnifyingglass", "/presentation/u/0/search"),
  187. ]),
  188. v("slides_large", "Slides Hub", .large, true, .actionsGrid(columns: 2), [
  189. a("recent", "Recent", "clock.arrow.circlepath", "/presentation/u/0/"),
  190. a("new", "New deck", "plus", "/presentation/create"),
  191. a("templates", "Templates", "square.grid.2x2", "/presentation/u/0/templates"),
  192. a("starred", "Starred", "star", "/presentation/u/0/starred"),
  193. a("shared", "Shared", "person.2", "/presentation/u/0/shared-with-me"),
  194. a("trash", "Trash", "trash", "/presentation/u/0/trash"),
  195. a("search", "Search", "magnifyingglass", "/presentation/u/0/search"),
  196. a("offline", "Offline", "arrow.down.circle", "/presentation/u/0/offline"),
  197. ]),
  198. ]
  199. case .forms:
  200. return [
  201. v("forms_small", "Forms", .small, false, .iconOnly(title: "Forms"), [a("home", "Open", "list.bullet.rectangle.fill", "/forms/u/0/")]),
  202. v("forms_medium", "Forms Shortcuts", .medium, true, .actionsGrid(columns: 2), [
  203. a("recent", "Recent", "clock.arrow.circlepath", "/forms/u/0/"),
  204. a("new", "New form", "plus", "https://docs.google.com/forms/create"),
  205. a("contacts", "Contacts", "person.crop.circle", "/forms/u/0/contacts"),
  206. a("search", "Search", "magnifyingglass", "/forms/u/0/search"),
  207. ]),
  208. v("forms_large", "Forms Hub", .large, true, .actionsGrid(columns: 2), [
  209. a("recent", "Recent", "clock.arrow.circlepath", "/forms/u/0/"),
  210. a("new", "New form", "plus", "https://docs.google.com/forms/create"),
  211. a("starred", "Starred", "star", "/forms/u/0/starred"),
  212. a("shared", "Shared", "person.2", "/forms/u/0/shared-with-me"),
  213. a("trash", "Trash", "trash", "/forms/u/0/trash"),
  214. a("contacts", "Contacts", "person.crop.circle", "/forms/u/0/contacts"),
  215. a("search", "Search", "magnifyingglass", "/forms/u/0/search"),
  216. a("settings", "Settings", "gearshape", "/forms/u/0/settings"),
  217. ]),
  218. ]
  219. case .calendar:
  220. return [
  221. v("calendar_small", "Today & next", .small, false, .calendar(.compact), []),
  222. v("calendar_medium", "Agenda", .medium, false, .calendar(.agenda), []),
  223. ]
  224. case .maps:
  225. // Live map: `MapsInteractiveWidgetView` (embedded `WKWebView`); actions unused.
  226. return [
  227. v("maps_small", "Live map", .small, true, .interactiveMap, []),
  228. v("maps_medium", "Live map", .medium, true, .interactiveMap, []),
  229. v("maps_large", "Live map", .large, true, .interactiveMap, []),
  230. ]
  231. case .youtube:
  232. return [
  233. v("youtube_small", "Quick Watch", .small, false, .actionsRow, [
  234. a("home", "Home", "house", "/"),
  235. a("shorts", "Shorts", "bolt", "/shorts"),
  236. a("subs", "Subs", "play.rectangle", "/feed/subscriptions"),
  237. ]),
  238. v("youtube_medium", "Search", .medium, false, .youtube(.searchBar), []),
  239. v("youtube_large", "Navigate", .large, false, .youtube(.interactiveNav), []),
  240. ]
  241. case .translate:
  242. return [
  243. v("translate_small", "Translate", .small, false, .actionsRow, [
  244. a("open", "Translator", "character.bubble.fill", "/"),
  245. a("history", "History", "clock", "/m/"),
  246. a("saved", "Phrasebook", "bookmark", "/m/"),
  247. ]),
  248. v("translate_medium", "Translate Tools", .medium, true, .actionsGrid(columns: 2), [
  249. a("open", "Web translator", "globe", "/"),
  250. a("docs", "Documents", "doc.text", "https://translate.google.com/?tr=f&hl=en&tab=wT"),
  251. a("conversation", "Conversation", "bubble.left.and.bubble.right", "/m/"),
  252. a("history", "History", "clock", "/m/"),
  253. ]),
  254. v("translate_large", "Full Translate", .large, false, .translate(.interactive), []),
  255. ]
  256. case .search:
  257. return [
  258. v("search_medium", "Google Shortcuts", .medium, true, .actionsGrid(columns: 2), [
  259. a("web", "Web", "globe", "/"),
  260. a("images", "Images", "photo", "https://www.google.com/imghp"),
  261. a("shopping", "Shopping", "bag", "https://www.google.com/search?tbm=shop"),
  262. a("maps", "Maps", "map", "https://maps.google.com/maps"),
  263. ]),
  264. v("search_large", "Full Search", .large, false, .googleSearch(.interactive), []),
  265. ]
  266. case .earth:
  267. return [
  268. v("earth_medium", "Earth Explorer", .medium, true, .actionsGrid(columns: 2), [
  269. a("open", "Open", "globe.americas.fill", "/web/"),
  270. a("voyager", "Voyager", "sailboat", "/web/data/"),
  271. a("projects", "Projects", "folder", "/web/projects/"),
  272. a("search", "Search", "magnifyingglass", "/web/search"),
  273. ]),
  274. v("earth_large", "Full Earth", .large, false, .earth(.interactive), []),
  275. ]
  276. case .shopping:
  277. return [
  278. v("shopping_small", "Shopping", .small, false, .iconOnly(title: "Shopping"), [a("open", "Open", "bag.fill", "/")]),
  279. v("shopping_medium", "Shopping Browse", .medium, true, .actionsGrid(columns: 2), [
  280. a("home", "Home", "house", "/"),
  281. a("deals", "Deals", "tag", "/"),
  282. a("saved", "Saved", "bookmark", "/"),
  283. a("search", "Search", "magnifyingglass", "/"),
  284. ]),
  285. ]
  286. case .keep:
  287. return [
  288. v("keep_small", "Recent notes", .small, false, .keep(.compact), []),
  289. v("keep_large", "Full Keep", .large, false, .keep(.interactive), []),
  290. ]
  291. case .travel:
  292. return [
  293. v("travel_small", "Travel", .small, false, .iconOnly(title: "Travel"), [a("trips", "Trips", "airplane", "/travel")]),
  294. v("travel_medium", "Trip Planner", .medium, true, .actionsGrid(columns: 2), [
  295. a("trips", "Trips", "airplane", "/travel"),
  296. a("explore", "Explore", "globe", "/travel/explore"),
  297. a("flights", "Flights", "airplane.departure", "/flights"),
  298. a("hotels", "Hotels", "bed.double", "/hotels"),
  299. ]),
  300. v("travel_large", "Trips & explore", .large, false, .travel(.interactive), []),
  301. ]
  302. case .meet:
  303. return [
  304. v("meet_small", "Meet", .small, false, .actionsRow, [
  305. a("join", "Join", "video.badge.plus", "/"),
  306. a("new", "New", "plus.circle", "/new"),
  307. a("landing", "Home", "house", "/landing"),
  308. ]),
  309. v("meet_large", "Meet Hub", .large, true, .actionsGrid(columns: 2), [
  310. a("new", "New meeting", "video.badge.plus", "/new"),
  311. a("join", "Join", "person.wave.2", "/"),
  312. a("landing", "Home", "house", "/landing"),
  313. a("schedule", "Schedule", "calendar", "https://calendar.google.com/calendar/u/0/r/eventedit"),
  314. ]),
  315. ]
  316. case .contacts:
  317. return [
  318. v("contacts_small", "Contacts", .small, false, .actionsRow, [
  319. a("contacts", "Contacts", "person.crop.circle", "/"),
  320. a("duplicates", "Duplicates", "person.2", "/duplicates"),
  321. a("labels", "Labels", "tag", "/"),
  322. ]),
  323. v("contacts_medium", "Contacts Hub", .medium, true, .actionsGrid(columns: 2), [
  324. a("contacts", "All", "person.crop.circle", "/"),
  325. a("frequent", "Frequent", "clock", "/"),
  326. a("duplicates", "Duplicates", "person.2", "/duplicates"),
  327. a("trash", "Trash", "trash", "/trash"),
  328. ]),
  329. ]
  330. case .blogger:
  331. return [
  332. v("blogger_small", "Blogger", .small, false, .iconOnly(title: "Blogger"), [a("home", "Open", "text.book.closed.fill", "/")]),
  333. v("blogger_medium", "Blogger Tools", .medium, true, .actionsGrid(columns: 2), [
  334. a("home", "Reading list", "list.bullet", "/"),
  335. a("create", "New post", "square.and.pencil", "/"),
  336. a("reading", "Reading list", "book", "/reading-list"),
  337. a("search", "Search", "magnifyingglass", "/"),
  338. ]),
  339. ]
  340. case .play:
  341. return [
  342. v("play_small", "Play", .small, false, .iconOnly(title: "Play"), [a("store", "Store", "play.circle.fill", "/store/apps")]),
  343. v("play_medium", "Play Store", .medium, true, .actionsGrid(columns: 2), [
  344. a("apps", "Apps", "app.badge", "/store/apps"),
  345. a("games", "Games", "gamecontroller", "/store/games"),
  346. a("movies", "Movies", "film", "/store/movies"),
  347. a("books", "Books", "book", "/store/books"),
  348. ]),
  349. ]
  350. case .news:
  351. return [
  352. v("news_small", "News", .small, false, .iconOnly(title: "News"), [a("home", "Headlines", "newspaper.fill", "/home")]),
  353. v("news_medium", "News Feeds", .medium, true, .actionsGrid(columns: 2), [
  354. a("foryou", "For you", "person.crop.circle", "/foryou"),
  355. a("headlines", "Headlines", "newspaper", "/topstories"),
  356. a("topics", "Topics", "square.grid.2x2", "/topics"),
  357. a("search", "Search", "magnifyingglass", "/search"),
  358. ]),
  359. ]
  360. case .chat:
  361. return [
  362. v("chat_small", "Chat", .small, false, .actionsRow, [
  363. a("home", "Home", "bubble.left.and.bubble.right.fill", "/u/0/"),
  364. a("spaces", "Spaces", "square.grid.3x3.fill", "/u/0/spaces"),
  365. a("meet", "Meet", "video", "https://meet.google.com/"),
  366. ]),
  367. v("chat_large", "Chat Hub", .large, true, .actionsGrid(columns: 2), [
  368. a("home", "Home", "house", "/u/0/"),
  369. a("spaces", "Spaces", "square.grid.3x3.fill", "/u/0/spaces"),
  370. a("browse", "Browse", "square.grid.2x2", "/u/0/browse"),
  371. a("meet", "Meet", "video", "https://meet.google.com/"),
  372. ]),
  373. ]
  374. case .finance:
  375. return [
  376. v("finance_small", "Finance", .small, false, .actionsRow, [
  377. a("home", "Markets", "chart.line.uptrend.xyaxis", "/finance"),
  378. a("markets", "Markets", "chart.bar", "/finance/markets"),
  379. a("portfolio", "Portfolio", "briefcase", "/finance/portfolio"),
  380. ]),
  381. v("finance_medium", "Finance Hub", .medium, true, .actionsGrid(columns: 2), [
  382. a("home", "Home", "house", "/finance"),
  383. a("markets", "Markets", "chart.bar", "/finance/markets"),
  384. a("portfolio", "Portfolio", "briefcase", "/finance/portfolio"),
  385. a("news", "News", "newspaper", "/finance/news"),
  386. ]),
  387. ]
  388. case .jamboard:
  389. return [
  390. v("jamboard_small", "Jamboard", .small, false, .actionsRow, [
  391. a("open", "Boards", "paintbrush.fill", "/"),
  392. a("create", "New jam", "plus", "/"),
  393. a("shared", "Shared", "person.2", "/"),
  394. ]),
  395. v("jamboard_large", "Jamboard", .large, true, .actionsGrid(columns: 2), [
  396. a("open", "Home", "house", "/"),
  397. a("create", "New jam", "plus", "/"),
  398. a("shared", "Shared with me", "person.2", "/"),
  399. a("trash", "Trash", "trash", "/"),
  400. ]),
  401. ]
  402. case .classroom:
  403. return [
  404. v("classroom_small", "Classroom", .small, false, .actionsRow, [
  405. a("home", "Classes", "graduationcap.fill", "/u/0/h/"),
  406. a("calendar", "Calendar", "calendar", "/u/0/calendar"),
  407. a("todo", "To-do", "checklist", "/u/0/notifications"),
  408. ]),
  409. v("classroom_large", "Classroom Hub", .large, true, .actionsGrid(columns: 2), [
  410. a("home", "Home", "house", "/u/0/h/"),
  411. a("calendar", "Calendar", "calendar", "/u/0/calendar"),
  412. a("archive", "Archive", "archivebox", "/u/0/archived"),
  413. a("settings", "Settings", "gearshape", "/u/0/settings"),
  414. ]),
  415. ]
  416. case .artsCulture:
  417. return [
  418. v("arts_small", "Arts & Culture", .small, false, .iconOnly(title: "Arts"), [a("home", "Explore", "building.columns.fill", "/")]),
  419. v("arts_medium", "Arts Browse", .medium, true, .actionsGrid(columns: 2), [
  420. a("home", "Home", "house", "/"),
  421. a("explore", "Explore", "globe", "/explore"),
  422. a("nearby", "Near you", "mappin.and.ellipse", "/nearby"),
  423. a("search", "Search", "magnifyingglass", "/search"),
  424. ]),
  425. ]
  426. case .voice:
  427. return [
  428. v("voice_small", "Voice", .small, false, .actionsRow, [
  429. a("messages", "Messages", "bubble.left", "/messages"),
  430. a("calls", "Calls", "phone", "/calls"),
  431. a("voicemail", "Voicemail", "recordingtape", "/voicemail"),
  432. ]),
  433. v("voice_large", "Voice Hub", .large, true, .actionsGrid(columns: 2), [
  434. a("messages", "Messages", "bubble.left", "/messages"),
  435. a("calls", "Calls", "phone", "/calls"),
  436. a("voicemail", "Voicemail", "recordingtape", "/voicemail"),
  437. a("settings", "Settings", "gearshape", "/settings"),
  438. ]),
  439. ]
  440. case .chromeWebStore:
  441. return [
  442. v("webstore_small", "Web Store", .small, false, .iconOnly(title: "Web Store"), [a("home", "Store", "bag.fill", "/webstore")]),
  443. v("webstore_medium", "Chrome Web Store", .medium, true, .actionsGrid(columns: 2), [
  444. a("extensions", "Extensions", "puzzlepiece.extension", "/webstore/category/extensions"),
  445. a("themes", "Themes", "paintpalette", "/webstore/category/themes"),
  446. a("home", "Home", "house", "/webstore"),
  447. a("search", "Search", "magnifyingglass", "/webstore/search"),
  448. ]),
  449. ]
  450. case .fi:
  451. return [
  452. v("fi_small", "Fi", .small, false, .actionsRow, [
  453. a("home", "Account", "antenna.radiowaves.left.and.right", "/"),
  454. a("billing", "Billing", "creditcard", "/billing"),
  455. a("coverage", "Coverage", "map", "/coverage"),
  456. ]),
  457. v("fi_medium", "Google Fi", .medium, true, .actionsGrid(columns: 2), [
  458. a("home", "Home", "house", "/"),
  459. a("billing", "Billing", "creditcard", "/billing"),
  460. a("coverage", "Coverage", "map", "/coverage"),
  461. a("support", "Support", "questionmark.circle", "/support"),
  462. ]),
  463. ]
  464. case .ads:
  465. return [
  466. v("ads_small", "Ads", .small, false, .actionsRow, [
  467. a("home", "Overview", "megaphone.fill", "/aw/"),
  468. a("campaigns", "Campaigns", "chart.bar", "/aw/campaigns"),
  469. a("reports", "Reports", "doc.text", "/aw/reports"),
  470. ]),
  471. v("ads_medium", "Google Ads", .medium, true, .actionsGrid(columns: 2), [
  472. a("home", "Home", "house", "/aw/"),
  473. a("campaigns", "Campaigns", "chart.bar", "/aw/campaigns"),
  474. a("tools", "Tools", "wrench", "/aw/tools"),
  475. a("billing", "Billing", "creditcard", "/aw/billing"),
  476. ]),
  477. ]
  478. case .remoteDesktop:
  479. return [
  480. v("remote_small", "Remote", .small, false, .actionsRow, [
  481. a("access", "Remote access", "desktopcomputer", "/access"),
  482. a("support", "Support", "person.crop.circle", "/support"),
  483. a("downloads", "Downloads", "arrow.down.circle", "/"),
  484. ]),
  485. v("remote_large", "Remote Desktop", .large, true, .actionsGrid(columns: 2), [
  486. a("access", "Remote access", "desktopcomputer", "/access"),
  487. a("support", "Support", "person.crop.circle", "/support"),
  488. a("downloads", "Downloads", "arrow.down.circle", "/"),
  489. a("help", "Help", "questionmark.circle", "/support"),
  490. ]),
  491. ]
  492. case .gemini:
  493. return [
  494. v("gemini_small", "Gemini", .small, false, .actionsRow, [
  495. a("app", "Gemini", "sparkles", "/app"),
  496. a("home", "Home", "house", "/"),
  497. a("extensions", "Extensions", "puzzlepiece", "/extensions"),
  498. ]),
  499. v("gemini_large", "Gemini Hub", .large, true, .actionsGrid(columns: 2), [
  500. a("app", "Chat", "sparkles", "/app"),
  501. a("home", "Home", "house", "/"),
  502. a("extensions", "Extensions", "puzzlepiece", "/extensions"),
  503. a("help", "Help", "questionmark.circle", "/help"),
  504. ]),
  505. ]
  506. case .books:
  507. return [
  508. v("books_large", "Library & reading", .large, false, .books(.interactive), []),
  509. ]
  510. case .unknown:
  511. return genericUtilityVariants(prefix: "generic", appName: app.name)
  512. }
  513. }
  514. static func variant(for app: LauncherApp, variantID: String?) -> WidgetVariant {
  515. let variants = variants(for: app)
  516. if let variantID, let match = variants.first(where: { $0.id == variantID }) {
  517. return match
  518. }
  519. return variants.first ?? WidgetVariant(
  520. id: "fallback_small",
  521. title: "Quick Access",
  522. size: .small,
  523. showHeader: false,
  524. layoutMode: .actionsRow,
  525. actions: [a("open", "Open", "arrow.up.right", nil)]
  526. )
  527. }
  528. }
  529. private extension WidgetTemplates {
  530. static func genericUtilityVariants(prefix: String, appName: String) -> [WidgetVariant] {
  531. [
  532. WidgetVariant(
  533. id: "\(prefix)_small",
  534. title: "Quick Access",
  535. size: .small,
  536. showHeader: false,
  537. layoutMode: .actionsRow,
  538. actions: [
  539. a("open", "Open", "arrow.up.right", "/"),
  540. a("search", "Search", "magnifyingglass", "/"),
  541. a("favorites", "Favorites", "star", "/"),
  542. ]
  543. ),
  544. WidgetVariant(
  545. id: "\(prefix)_medium",
  546. title: appName,
  547. size: .medium,
  548. showHeader: true,
  549. layoutMode: .actionsGrid(columns: 2),
  550. actions: [
  551. a("open", "Open", "arrow.up.right", "/"),
  552. a("search", "Search", "magnifyingglass", "/"),
  553. a("favorites", "Favorites", "star", "/"),
  554. a("recent", "Recent", "clock.arrow.circlepath", "/"),
  555. ]
  556. ),
  557. ]
  558. }
  559. static func a(_ id: String, _ title: String, _ image: String, _ path: String?) -> WidgetAction {
  560. WidgetAction(id: id, title: title, systemImage: image, urlPath: path)
  561. }
  562. }
  563. extension WidgetLayoutMode {
  564. /// Panels that embed interactive web content need to become key so text fields and the web view receive input.
  565. var needsKeyCapablePanel: Bool {
  566. switch self {
  567. case .interactiveMap: true
  568. case .gmail(.interactive): true
  569. case .drive(.interactive): true
  570. case .keep(.interactive): true
  571. case .translate(.interactive): true
  572. case .earth(.interactive): true
  573. case .googleSearch(.interactive): true
  574. case .books(.interactive): true
  575. case .travel(.interactive): true
  576. case .youtube(.searchBar): true
  577. case .youtube(.interactiveNav): true
  578. case .calendar: false
  579. default: false
  580. }
  581. }
  582. /// Fixed sizes for tiered Gmail / Drive widgets (variant size already matches the tier).
  583. func desktopPanelSizeOverride() -> CGSize? {
  584. switch self {
  585. case .gmail(let mode):
  586. switch mode {
  587. case .compact: return CGSize(width: 220, height: 220)
  588. case .inboxPreview: return CGSize(width: 420, height: 480)
  589. case .interactive: return CGSize(width: 540, height: 580)
  590. }
  591. case .drive(let mode):
  592. switch mode {
  593. case .recentQuick: return CGSize(width: 230, height: 260)
  594. case .filesPreview: return CGSize(width: 420, height: 480)
  595. case .interactive: return CGSize(width: 540, height: 580)
  596. }
  597. case .keep(let mode):
  598. switch mode {
  599. case .compact: return CGSize(width: 230, height: 260)
  600. case .interactive: return CGSize(width: 540, height: 580)
  601. }
  602. case .translate(.interactive):
  603. return CGSize(width: 560, height: 640)
  604. case .earth(.interactive):
  605. return CGSize(width: 580, height: 640)
  606. case .googleSearch(.interactive):
  607. return CGSize(width: 480, height: 220)
  608. case .calendar(let mode):
  609. switch mode {
  610. case .compact: return CGSize(width: 230, height: 260)
  611. case .agenda: return CGSize(width: 420, height: 480)
  612. }
  613. case .books(.interactive):
  614. return CGSize(width: 420, height: 260)
  615. case .travel(.interactive):
  616. return CGSize(width: 520, height: 196)
  617. case .youtube(.searchBar):
  618. return CGSize(width: 400, height: 118)
  619. case .youtube(.interactiveNav):
  620. return CGSize(width: 420, height: 292)
  621. default:
  622. return nil
  623. }
  624. }
  625. func widgetPreviewCardSize() -> CGSize? {
  626. switch self {
  627. case .gmail(let mode):
  628. switch mode {
  629. case .compact: return CGSize(width: 200, height: 200)
  630. case .inboxPreview: return CGSize(width: 380, height: 420)
  631. case .interactive: return CGSize(width: 480, height: 440)
  632. }
  633. case .drive(let mode):
  634. switch mode {
  635. case .recentQuick: return CGSize(width: 210, height: 230)
  636. case .filesPreview: return CGSize(width: 380, height: 420)
  637. case .interactive: return CGSize(width: 480, height: 440)
  638. }
  639. case .keep(let mode):
  640. switch mode {
  641. case .compact: return CGSize(width: 210, height: 230)
  642. case .interactive: return CGSize(width: 480, height: 440)
  643. }
  644. case .translate(.interactive):
  645. return CGSize(width: 480, height: 460)
  646. case .earth(.interactive):
  647. return CGSize(width: 490, height: 470)
  648. case .googleSearch(.interactive):
  649. return CGSize(width: 440, height: 210)
  650. case .calendar(let mode):
  651. switch mode {
  652. case .compact: return CGSize(width: 210, height: 240)
  653. case .agenda: return CGSize(width: 380, height: 440)
  654. }
  655. case .books(.interactive):
  656. return CGSize(width: 380, height: 240)
  657. case .travel(.interactive):
  658. return CGSize(width: 460, height: 178)
  659. case .youtube(.searchBar):
  660. return CGSize(width: 360, height: 108)
  661. case .youtube(.interactiveNav):
  662. return CGSize(width: 380, height: 268)
  663. default:
  664. return nil
  665. }
  666. }
  667. }