| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- import CoreGraphics
- import Foundation
- struct WidgetAction: Identifiable, Hashable {
- let id: String
- let title: String
- let systemImage: String
- /// If nil, opens the app base URL.
- let urlPath: String?
- init(id: String, title: String, systemImage: String, urlPath: String? = nil) {
- self.id = id
- self.title = title
- self.systemImage = systemImage
- self.urlPath = urlPath
- }
- }
- enum GmailWidgetMode: Hashable, Sendable {
- /// Unread count (local placeholder) and quick compose.
- case compact
- /// Sample inbox rows with reply / archive-style shortcuts.
- case inboxPreview
- /// Embedded Gmail (`WKWebView`) with search, category chips, and folder shortcuts.
- case interactive
- }
- enum DriveWidgetMode: Hashable, Sendable {
- /// Sample recent files and shortcuts to Drive recent.
- case recentQuick
- /// Recent + starred sample rows with open / share.
- case filesPreview
- /// Shortcut hub: search, folders, quick find, create, and tools — open in app browser (no embedded Drive UI).
- case interactive
- }
- enum KeepWidgetMode: Hashable, Sendable {
- /// Recent note snippets and new-note shortcut.
- case compact
- /// Embedded Google Keep (`WKWebView`) with search, labels, and create options.
- case interactive
- }
- enum TranslateWidgetMode: Hashable, Sendable {
- /// Embedded Google Translate (`WKWebView`): text, voice, conversation, history.
- case interactive
- }
- enum EarthWidgetMode: Hashable, Sendable {
- /// Embedded Google Earth Web (`WKWebView`): globe, layers, Voyager, projects.
- case interactive
- }
- enum GoogleSearchWidgetMode: Hashable, Sendable {
- /// Embedded google.com (`WKWebView`): search, voice, trends, recents.
- case interactive
- }
- enum WidgetLayoutMode: Hashable {
- case iconOnly(title: String)
- case actionsRow
- case actionsGrid(columns: Int)
- /// Embedded Google Maps (`WKWebView`) with search, traffic, and navigation shortcuts.
- case interactiveMap
- /// Tiered Gmail: compact preview, inbox list, or full embedded inbox.
- case gmail(GmailWidgetMode)
- /// Tiered Google Drive: recent files, starred preview, or large shortcut hub.
- case drive(DriveWidgetMode)
- /// Tiered Google Keep: recent notes or full embedded Keep.
- case keep(KeepWidgetMode)
- /// Embedded Google Translate: full translator UI in-panel.
- case translate(TranslateWidgetMode)
- /// Embedded Google Earth Web: full globe UI in-panel.
- case earth(EarthWidgetMode)
- /// Embedded Google Search (`WKWebView`): full search experience in-panel.
- case googleSearch(GoogleSearchWidgetMode)
- }
- struct WidgetVariant: Identifiable, Hashable {
- let id: String
- let title: String
- let size: WidgetSize
- let showHeader: Bool
- let layoutMode: WidgetLayoutMode
- let actions: [WidgetAction]
- }
- enum WidgetTemplates {
- static func variants(for app: LauncherApp) -> [WidgetVariant] {
- func v(_ id: String, _ title: String, _ size: WidgetSize, _ showHeader: Bool, _ layout: WidgetLayoutMode, _ actions: [WidgetAction]) -> WidgetVariant {
- WidgetVariant(id: id, title: title, size: size, showHeader: showHeader, layoutMode: layout, actions: actions)
- }
- let profile = WidgetAppProfile.from(app: app)
- switch profile {
- case .photos:
- return [
- v("photos_small", "Photo", .small, false, .iconOnly(title: "Photo"), [a("photos", "Photos", "photo.on.rectangle", "/")]),
- v("photos_medium", "Google Photo", .medium, false, .iconOnly(title: "Google Photo"), [a("albums", "Albums", "rectangle.stack", "/albums")]),
- v("photos_large", "Photos Library", .large, true, .actionsGrid(columns: 2), [
- a("photos", "Photos", "photo.on.rectangle", "/"),
- a("albums", "Albums", "rectangle.stack", "/albums"),
- a("explore", "Explore", "magnifyingglass", "/search"),
- a("archive", "Archive", "archivebox", "/archive"),
- a("sharing", "Sharing", "person.2", "/sharing"),
- a("locked", "Locked Folder", "lock", "/lockedfolder"),
- a("favorites", "Favorites", "star", "/favorites"),
- a("trash", "Trash", "trash", "/trash"),
- ]),
- ]
- case .gmail:
- return [
- v("gmail_small", "Unread & compose", .small, false, .gmail(.compact), []),
- v("gmail_medium", "Inbox preview", .medium, false, .gmail(.inboxPreview), []),
- v("gmail_large", "Full inbox", .large, false, .gmail(.interactive), []),
- ]
- case .drive:
- return [
- v("drive_small", "Recent files", .small, false, .drive(.recentQuick), []),
- v("drive_medium", "Files & sharing", .medium, false, .drive(.filesPreview), []),
- v("drive_large", "Full Drive", .large, false, .drive(.interactive), []),
- ]
- case .docs:
- return [
- v("docs_small", "Docs", .small, false, .iconOnly(title: "Docs"), [a("home", "Open", "doc.text.fill", "/document/u/0/")]),
- v("docs_medium", "Docs Shortcuts", .medium, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
- a("new", "New doc", "plus", "/document/create"),
- a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
- a("search", "Search", "magnifyingglass", "/document/u/0/search"),
- ]),
- v("docs_large", "Docs Hub", .large, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/document/u/0/"),
- a("new", "New doc", "plus", "/document/create"),
- a("templates", "Templates", "square.grid.2x2", "/document/u/0/templates/home"),
- a("starred", "Starred", "star", "/document/u/0/starred"),
- a("shared", "Shared", "person.2", "/document/u/0/shared-with-me"),
- a("trash", "Trash", "trash", "/document/u/0/trash"),
- a("search", "Search", "magnifyingglass", "/document/u/0/search"),
- a("offline", "Offline", "arrow.down.circle", "/document/u/0/offline"),
- ]),
- ]
- case .sheets:
- return [
- v("sheets_small", "Sheets", .small, false, .iconOnly(title: "Sheets"), [a("home", "Open", "tablecells.fill", "/spreadsheets/u/0/")]),
- v("sheets_medium", "Sheets Shortcuts", .medium, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/spreadsheets/u/0/"),
- a("new", "New sheet", "plus", "/spreadsheets/create"),
- a("templates", "Templates", "square.grid.2x2", "/spreadsheets/u/0/templates"),
- a("search", "Search", "magnifyingglass", "/spreadsheets/u/0/search"),
- ]),
- v("sheets_large", "Sheets Hub", .large, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/spreadsheets/u/0/"),
- a("new", "New sheet", "plus", "/spreadsheets/create"),
- a("templates", "Templates", "square.grid.2x2", "/spreadsheets/u/0/templates"),
- a("starred", "Starred", "star", "/spreadsheets/u/0/starred"),
- a("shared", "Shared", "person.2", "/spreadsheets/u/0/shared-with-me"),
- a("trash", "Trash", "trash", "/spreadsheets/u/0/trash"),
- a("search", "Search", "magnifyingglass", "/spreadsheets/u/0/search"),
- a("offline", "Offline", "arrow.down.circle", "/spreadsheets/u/0/offline"),
- ]),
- ]
- case .slides:
- return [
- v("slides_small", "Slides", .small, false, .iconOnly(title: "Slides"), [a("home", "Open", "rectangle.on.rectangle.fill", "/presentation/u/0/")]),
- v("slides_medium", "Slides Shortcuts", .medium, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/presentation/u/0/"),
- a("new", "New deck", "plus", "/presentation/create"),
- a("templates", "Templates", "square.grid.2x2", "/presentation/u/0/templates"),
- a("search", "Search", "magnifyingglass", "/presentation/u/0/search"),
- ]),
- v("slides_large", "Slides Hub", .large, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/presentation/u/0/"),
- a("new", "New deck", "plus", "/presentation/create"),
- a("templates", "Templates", "square.grid.2x2", "/presentation/u/0/templates"),
- a("starred", "Starred", "star", "/presentation/u/0/starred"),
- a("shared", "Shared", "person.2", "/presentation/u/0/shared-with-me"),
- a("trash", "Trash", "trash", "/presentation/u/0/trash"),
- a("search", "Search", "magnifyingglass", "/presentation/u/0/search"),
- a("offline", "Offline", "arrow.down.circle", "/presentation/u/0/offline"),
- ]),
- ]
- case .forms:
- return [
- v("forms_small", "Forms", .small, false, .iconOnly(title: "Forms"), [a("home", "Open", "list.bullet.rectangle.fill", "/forms/u/0/")]),
- v("forms_medium", "Forms Shortcuts", .medium, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/forms/u/0/"),
- a("new", "New form", "plus", "https://docs.google.com/forms/create"),
- a("contacts", "Contacts", "person.crop.circle", "/forms/u/0/contacts"),
- a("search", "Search", "magnifyingglass", "/forms/u/0/search"),
- ]),
- v("forms_large", "Forms Hub", .large, true, .actionsGrid(columns: 2), [
- a("recent", "Recent", "clock.arrow.circlepath", "/forms/u/0/"),
- a("new", "New form", "plus", "https://docs.google.com/forms/create"),
- a("starred", "Starred", "star", "/forms/u/0/starred"),
- a("shared", "Shared", "person.2", "/forms/u/0/shared-with-me"),
- a("trash", "Trash", "trash", "/forms/u/0/trash"),
- a("contacts", "Contacts", "person.crop.circle", "/forms/u/0/contacts"),
- a("search", "Search", "magnifyingglass", "/forms/u/0/search"),
- a("settings", "Settings", "gearshape", "/forms/u/0/settings"),
- ]),
- ]
- case .calendar:
- return [
- v("calendar_small", "Today", .small, false, .actionsRow, [
- a("today", "Today", "calendar", "/calendar/u/0/r"),
- a("create", "Create", "plus.circle", "/calendar/u/0/r/eventedit"),
- ]),
- v("calendar_medium", "Schedule", .medium, true, .actionsGrid(columns: 2), [
- a("today", "Today", "calendar", "/calendar/u/0/r"),
- a("week", "Week", "calendar.badge.clock", "/calendar/u/0/r/week"),
- a("create", "New Event", "plus.circle", "/calendar/u/0/r/eventedit"),
- a("tasks", "Tasks", "checkmark.square", "https://tasks.google.com/"),
- ]),
- v("calendar_large", "Calendar Hub", .large, true, .actionsGrid(columns: 2), [
- a("today", "Today", "calendar", "/calendar/u/0/r"),
- a("week", "Week", "calendar.badge.clock", "/calendar/u/0/r/week"),
- a("month", "Month", "calendar", "/calendar/u/0/r/month"),
- a("create", "New Event", "plus.circle", "/calendar/u/0/r/eventedit"),
- a("tasks", "Tasks", "checkmark.square", "https://tasks.google.com/"),
- a("settings", "Settings", "gearshape", "/calendar/u/0/r/settings"),
- a("search", "Search", "magnifyingglass", "/calendar/u/0/r/search"),
- a("print", "Print", "printer", "/calendar/u/0/r/print"),
- ]),
- ]
- case .maps:
- // Live map: `MapsInteractiveWidgetView` (embedded `WKWebView`); actions unused.
- return [
- v("maps_small", "Live map", .small, true, .interactiveMap, []),
- v("maps_medium", "Live map", .medium, true, .interactiveMap, []),
- v("maps_large", "Live map", .large, true, .interactiveMap, []),
- ]
- case .youtube:
- return [
- v("youtube_small", "Quick Watch", .small, false, .actionsRow, [
- a("home", "Home", "house", "/"),
- a("shorts", "Shorts", "bolt", "/shorts"),
- a("subs", "Subs", "play.rectangle", "/feed/subscriptions"),
- ]),
- v("youtube_medium", "Browse", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/"),
- a("trending", "Trending", "flame", "/feed/trending"),
- a("shorts", "Shorts", "bolt", "/shorts"),
- a("subs", "Subscriptions", "play.rectangle", "/feed/subscriptions"),
- ]),
- v("youtube_large", "Library", .large, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/"),
- a("shorts", "Shorts", "play.square", "/shorts"),
- a("subscriptions", "Subscriptions", "play.rectangle", "/feed/subscriptions"),
- a("history", "History", "clock", "/feed/history"),
- a("playlists", "Playlists", "list.bullet.rectangle", "/feed/playlists"),
- a("liked", "Liked videos", "hand.thumbsup", "/playlist?list=LL"),
- ]),
- ]
- case .translate:
- return [
- v("translate_small", "Translate", .small, false, .actionsRow, [
- a("open", "Translator", "character.bubble.fill", "/"),
- a("history", "History", "clock", "/m/"),
- a("saved", "Phrasebook", "bookmark", "/m/"),
- ]),
- v("translate_medium", "Translate Tools", .medium, true, .actionsGrid(columns: 2), [
- a("open", "Web translator", "globe", "/"),
- a("docs", "Documents", "doc.text", "https://translate.google.com/?tr=f&hl=en&tab=wT"),
- a("conversation", "Conversation", "bubble.left.and.bubble.right", "/m/"),
- a("history", "History", "clock", "/m/"),
- ]),
- v("translate_large", "Full Translate", .large, false, .translate(.interactive), []),
- ]
- case .search:
- return [
- v("search_medium", "Google Shortcuts", .medium, true, .actionsGrid(columns: 2), [
- a("web", "Web", "globe", "/"),
- a("images", "Images", "photo", "https://www.google.com/imghp"),
- a("shopping", "Shopping", "bag", "https://www.google.com/search?tbm=shop"),
- a("maps", "Maps", "map", "https://maps.google.com/maps"),
- ]),
- v("search_large", "Full Search", .large, false, .googleSearch(.interactive), []),
- ]
- case .earth:
- return [
- v("earth_medium", "Earth Explorer", .medium, true, .actionsGrid(columns: 2), [
- a("open", "Open", "globe.americas.fill", "/web/"),
- a("voyager", "Voyager", "sailboat", "/web/data/"),
- a("projects", "Projects", "folder", "/web/projects/"),
- a("search", "Search", "magnifyingglass", "/web/search"),
- ]),
- v("earth_large", "Full Earth", .large, false, .earth(.interactive), []),
- ]
- case .shopping:
- return [
- v("shopping_small", "Shopping", .small, false, .iconOnly(title: "Shopping"), [a("open", "Open", "bag.fill", "/")]),
- v("shopping_medium", "Shopping Browse", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/"),
- a("deals", "Deals", "tag", "/"),
- a("saved", "Saved", "bookmark", "/"),
- a("search", "Search", "magnifyingglass", "/"),
- ]),
- ]
- case .keep:
- return [
- v("keep_small", "Recent notes", .small, false, .keep(.compact), []),
- v("keep_large", "Full Keep", .large, false, .keep(.interactive), []),
- ]
- case .travel:
- return [
- v("travel_small", "Travel", .small, false, .iconOnly(title: "Travel"), [a("trips", "Trips", "airplane", "/travel")]),
- v("travel_medium", "Trip Planner", .medium, true, .actionsGrid(columns: 2), [
- a("trips", "Trips", "airplane", "/travel"),
- a("explore", "Explore", "globe", "/travel/explore"),
- a("flights", "Flights", "airplane.departure", "/flights"),
- a("hotels", "Hotels", "bed.double", "/hotels"),
- ]),
- ]
- case .meet:
- return [
- v("meet_small", "Meet", .small, false, .actionsRow, [
- a("join", "Join", "video.badge.plus", "/"),
- a("new", "New", "plus.circle", "/new"),
- a("landing", "Home", "house", "/landing"),
- ]),
- v("meet_large", "Meet Hub", .large, true, .actionsGrid(columns: 2), [
- a("new", "New meeting", "video.badge.plus", "/new"),
- a("join", "Join", "person.wave.2", "/"),
- a("landing", "Home", "house", "/landing"),
- a("schedule", "Schedule", "calendar", "https://calendar.google.com/calendar/u/0/r/eventedit"),
- ]),
- ]
- case .contacts:
- return [
- v("contacts_small", "Contacts", .small, false, .actionsRow, [
- a("contacts", "Contacts", "person.crop.circle", "/"),
- a("duplicates", "Duplicates", "person.2", "/duplicates"),
- a("labels", "Labels", "tag", "/"),
- ]),
- v("contacts_medium", "Contacts Hub", .medium, true, .actionsGrid(columns: 2), [
- a("contacts", "All", "person.crop.circle", "/"),
- a("frequent", "Frequent", "clock", "/"),
- a("duplicates", "Duplicates", "person.2", "/duplicates"),
- a("trash", "Trash", "trash", "/trash"),
- ]),
- ]
- case .blogger:
- return [
- v("blogger_small", "Blogger", .small, false, .iconOnly(title: "Blogger"), [a("home", "Open", "text.book.closed.fill", "/")]),
- v("blogger_medium", "Blogger Tools", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Reading list", "list.bullet", "/"),
- a("create", "New post", "square.and.pencil", "/"),
- a("reading", "Reading list", "book", "/reading-list"),
- a("search", "Search", "magnifyingglass", "/"),
- ]),
- ]
- case .play:
- return [
- v("play_small", "Play", .small, false, .iconOnly(title: "Play"), [a("store", "Store", "play.circle.fill", "/store/apps")]),
- v("play_medium", "Play Store", .medium, true, .actionsGrid(columns: 2), [
- a("apps", "Apps", "app.badge", "/store/apps"),
- a("games", "Games", "gamecontroller", "/store/games"),
- a("movies", "Movies", "film", "/store/movies"),
- a("books", "Books", "book", "/store/books"),
- ]),
- ]
- case .news:
- return [
- v("news_small", "News", .small, false, .iconOnly(title: "News"), [a("home", "Headlines", "newspaper.fill", "/home")]),
- v("news_medium", "News Feeds", .medium, true, .actionsGrid(columns: 2), [
- a("foryou", "For you", "person.crop.circle", "/foryou"),
- a("headlines", "Headlines", "newspaper", "/topstories"),
- a("topics", "Topics", "square.grid.2x2", "/topics"),
- a("search", "Search", "magnifyingglass", "/search"),
- ]),
- ]
- case .chat:
- return [
- v("chat_small", "Chat", .small, false, .actionsRow, [
- a("home", "Home", "bubble.left.and.bubble.right.fill", "/u/0/"),
- a("spaces", "Spaces", "square.grid.3x3.fill", "/u/0/spaces"),
- a("meet", "Meet", "video", "https://meet.google.com/"),
- ]),
- v("chat_large", "Chat Hub", .large, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/u/0/"),
- a("spaces", "Spaces", "square.grid.3x3.fill", "/u/0/spaces"),
- a("browse", "Browse", "square.grid.2x2", "/u/0/browse"),
- a("meet", "Meet", "video", "https://meet.google.com/"),
- ]),
- ]
- case .finance:
- return [
- v("finance_small", "Finance", .small, false, .actionsRow, [
- a("home", "Markets", "chart.line.uptrend.xyaxis", "/finance"),
- a("markets", "Markets", "chart.bar", "/finance/markets"),
- a("portfolio", "Portfolio", "briefcase", "/finance/portfolio"),
- ]),
- v("finance_medium", "Finance Hub", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/finance"),
- a("markets", "Markets", "chart.bar", "/finance/markets"),
- a("portfolio", "Portfolio", "briefcase", "/finance/portfolio"),
- a("news", "News", "newspaper", "/finance/news"),
- ]),
- ]
- case .jamboard:
- return [
- v("jamboard_small", "Jamboard", .small, false, .actionsRow, [
- a("open", "Boards", "paintbrush.fill", "/"),
- a("create", "New jam", "plus", "/"),
- a("shared", "Shared", "person.2", "/"),
- ]),
- v("jamboard_large", "Jamboard", .large, true, .actionsGrid(columns: 2), [
- a("open", "Home", "house", "/"),
- a("create", "New jam", "plus", "/"),
- a("shared", "Shared with me", "person.2", "/"),
- a("trash", "Trash", "trash", "/"),
- ]),
- ]
- case .classroom:
- return [
- v("classroom_small", "Classroom", .small, false, .actionsRow, [
- a("home", "Classes", "graduationcap.fill", "/u/0/h/"),
- a("calendar", "Calendar", "calendar", "/u/0/calendar"),
- a("todo", "To-do", "checklist", "/u/0/notifications"),
- ]),
- v("classroom_large", "Classroom Hub", .large, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/u/0/h/"),
- a("calendar", "Calendar", "calendar", "/u/0/calendar"),
- a("archive", "Archive", "archivebox", "/u/0/archived"),
- a("settings", "Settings", "gearshape", "/u/0/settings"),
- ]),
- ]
- case .artsCulture:
- return [
- v("arts_small", "Arts & Culture", .small, false, .iconOnly(title: "Arts"), [a("home", "Explore", "building.columns.fill", "/")]),
- v("arts_medium", "Arts Browse", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/"),
- a("explore", "Explore", "globe", "/explore"),
- a("nearby", "Near you", "mappin.and.ellipse", "/nearby"),
- a("search", "Search", "magnifyingglass", "/search"),
- ]),
- ]
- case .voice:
- return [
- v("voice_small", "Voice", .small, false, .actionsRow, [
- a("messages", "Messages", "bubble.left", "/messages"),
- a("calls", "Calls", "phone", "/calls"),
- a("voicemail", "Voicemail", "recordingtape", "/voicemail"),
- ]),
- v("voice_large", "Voice Hub", .large, true, .actionsGrid(columns: 2), [
- a("messages", "Messages", "bubble.left", "/messages"),
- a("calls", "Calls", "phone", "/calls"),
- a("voicemail", "Voicemail", "recordingtape", "/voicemail"),
- a("settings", "Settings", "gearshape", "/settings"),
- ]),
- ]
- case .chromeWebStore:
- return [
- v("webstore_small", "Web Store", .small, false, .iconOnly(title: "Web Store"), [a("home", "Store", "bag.fill", "/webstore")]),
- v("webstore_medium", "Chrome Web Store", .medium, true, .actionsGrid(columns: 2), [
- a("extensions", "Extensions", "puzzlepiece.extension", "/webstore/category/extensions"),
- a("themes", "Themes", "paintpalette", "/webstore/category/themes"),
- a("home", "Home", "house", "/webstore"),
- a("search", "Search", "magnifyingglass", "/webstore/search"),
- ]),
- ]
- case .fi:
- return [
- v("fi_small", "Fi", .small, false, .actionsRow, [
- a("home", "Account", "antenna.radiowaves.left.and.right", "/"),
- a("billing", "Billing", "creditcard", "/billing"),
- a("coverage", "Coverage", "map", "/coverage"),
- ]),
- v("fi_medium", "Google Fi", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/"),
- a("billing", "Billing", "creditcard", "/billing"),
- a("coverage", "Coverage", "map", "/coverage"),
- a("support", "Support", "questionmark.circle", "/support"),
- ]),
- ]
- case .ads:
- return [
- v("ads_small", "Ads", .small, false, .actionsRow, [
- a("home", "Overview", "megaphone.fill", "/aw/"),
- a("campaigns", "Campaigns", "chart.bar", "/aw/campaigns"),
- a("reports", "Reports", "doc.text", "/aw/reports"),
- ]),
- v("ads_medium", "Google Ads", .medium, true, .actionsGrid(columns: 2), [
- a("home", "Home", "house", "/aw/"),
- a("campaigns", "Campaigns", "chart.bar", "/aw/campaigns"),
- a("tools", "Tools", "wrench", "/aw/tools"),
- a("billing", "Billing", "creditcard", "/aw/billing"),
- ]),
- ]
- case .remoteDesktop:
- return [
- v("remote_small", "Remote", .small, false, .actionsRow, [
- a("access", "Remote access", "desktopcomputer", "/access"),
- a("support", "Support", "person.crop.circle", "/support"),
- a("downloads", "Downloads", "arrow.down.circle", "/"),
- ]),
- v("remote_large", "Remote Desktop", .large, true, .actionsGrid(columns: 2), [
- a("access", "Remote access", "desktopcomputer", "/access"),
- a("support", "Support", "person.crop.circle", "/support"),
- a("downloads", "Downloads", "arrow.down.circle", "/"),
- a("help", "Help", "questionmark.circle", "/support"),
- ]),
- ]
- case .gemini:
- return [
- v("gemini_small", "Gemini", .small, false, .actionsRow, [
- a("app", "Gemini", "sparkles", "/app"),
- a("home", "Home", "house", "/"),
- a("extensions", "Extensions", "puzzlepiece", "/extensions"),
- ]),
- v("gemini_large", "Gemini Hub", .large, true, .actionsGrid(columns: 2), [
- a("app", "Chat", "sparkles", "/app"),
- a("home", "Home", "house", "/"),
- a("extensions", "Extensions", "puzzlepiece", "/extensions"),
- a("help", "Help", "questionmark.circle", "/help"),
- ]),
- ]
- 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"),
- ]),
- ]
- case .unknown:
- return genericUtilityVariants(prefix: "generic", appName: app.name)
- }
- }
- static func variant(for app: LauncherApp, variantID: String?) -> WidgetVariant {
- let variants = variants(for: app)
- if let variantID, let match = variants.first(where: { $0.id == variantID }) {
- return match
- }
- return variants.first ?? WidgetVariant(
- id: "fallback_small",
- title: "Quick Access",
- size: .small,
- showHeader: false,
- layoutMode: .actionsRow,
- actions: [a("open", "Open", "arrow.up.right", nil)]
- )
- }
- }
- private extension WidgetTemplates {
- static func genericUtilityVariants(prefix: String, appName: String) -> [WidgetVariant] {
- [
- WidgetVariant(
- id: "\(prefix)_small",
- title: "Quick Access",
- size: .small,
- showHeader: false,
- layoutMode: .actionsRow,
- actions: [
- a("open", "Open", "arrow.up.right", "/"),
- a("search", "Search", "magnifyingglass", "/"),
- a("favorites", "Favorites", "star", "/"),
- ]
- ),
- WidgetVariant(
- id: "\(prefix)_medium",
- title: appName,
- size: .medium,
- showHeader: true,
- layoutMode: .actionsGrid(columns: 2),
- actions: [
- a("open", "Open", "arrow.up.right", "/"),
- a("search", "Search", "magnifyingglass", "/"),
- a("favorites", "Favorites", "star", "/"),
- a("recent", "Recent", "clock.arrow.circlepath", "/"),
- ]
- ),
- ]
- }
- static func a(_ id: String, _ title: String, _ image: String, _ path: String?) -> WidgetAction {
- WidgetAction(id: id, title: title, systemImage: image, urlPath: path)
- }
- }
- extension WidgetLayoutMode {
- /// Panels that embed interactive web content need to become key so text fields and the web view receive input.
- var needsKeyCapablePanel: Bool {
- switch self {
- case .interactiveMap: true
- case .gmail(.interactive): true
- case .drive(.interactive): true
- case .keep(.interactive): true
- case .translate(.interactive): true
- case .earth(.interactive): true
- case .googleSearch(.interactive): true
- default: false
- }
- }
- /// Fixed sizes for tiered Gmail / Drive widgets (variant size already matches the tier).
- func desktopPanelSizeOverride() -> CGSize? {
- switch self {
- case .gmail(let mode):
- switch mode {
- case .compact: return CGSize(width: 220, height: 220)
- case .inboxPreview: return CGSize(width: 420, height: 480)
- case .interactive: return CGSize(width: 540, height: 580)
- }
- case .drive(let mode):
- switch mode {
- case .recentQuick: return CGSize(width: 230, height: 260)
- case .filesPreview: return CGSize(width: 420, height: 480)
- case .interactive: return CGSize(width: 540, height: 580)
- }
- case .keep(let mode):
- switch mode {
- case .compact: return CGSize(width: 230, height: 260)
- case .interactive: return CGSize(width: 540, height: 580)
- }
- case .translate(.interactive):
- return CGSize(width: 560, height: 640)
- case .earth(.interactive):
- return CGSize(width: 580, height: 640)
- case .googleSearch(.interactive):
- return CGSize(width: 480, height: 220)
- default:
- return nil
- }
- }
- func widgetPreviewCardSize() -> CGSize? {
- switch self {
- case .gmail(let mode):
- switch mode {
- case .compact: return CGSize(width: 200, height: 200)
- case .inboxPreview: return CGSize(width: 380, height: 420)
- case .interactive: return CGSize(width: 480, height: 440)
- }
- case .drive(let mode):
- switch mode {
- case .recentQuick: return CGSize(width: 210, height: 230)
- case .filesPreview: return CGSize(width: 380, height: 420)
- case .interactive: return CGSize(width: 480, height: 440)
- }
- case .keep(let mode):
- switch mode {
- case .compact: return CGSize(width: 210, height: 230)
- case .interactive: return CGSize(width: 480, height: 440)
- }
- case .translate(.interactive):
- return CGSize(width: 480, height: 460)
- case .earth(.interactive):
- return CGSize(width: 490, height: 470)
- case .googleSearch(.interactive):
- return CGSize(width: 440, height: 210)
- default:
- return nil
- }
- }
- }
|