WidgetTemplates.swift 33 KB

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