LauncherApp.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import Foundation
  2. extension Notification.Name {
  3. /// Posted when `statusBarAppIDsData` changes so extra menu bar icons can refresh.
  4. static let statusBarShortcutIDsChanged = Notification.Name("google_apps.statusBarShortcutIDsChanged")
  5. }
  6. struct LauncherApp: Identifiable, Hashable {
  7. let id: UUID
  8. let name: String
  9. let assetIconName: String
  10. let fallbackSymbolName: String
  11. let description: String
  12. let isCreateNew: Bool
  13. let webURL: URL?
  14. init(
  15. id: UUID = UUID(),
  16. name: String,
  17. assetIconName: String,
  18. fallbackSymbolName: String,
  19. description: String,
  20. isCreateNew: Bool,
  21. webURL: URL?
  22. ) {
  23. self.id = id
  24. self.name = name
  25. self.assetIconName = assetIconName
  26. self.fallbackSymbolName = fallbackSymbolName
  27. self.description = description
  28. self.isCreateNew = isCreateNew
  29. self.webURL = webURL
  30. }
  31. }
  32. struct PersistedCustomApp: Codable {
  33. let id: UUID
  34. let name: String
  35. let urlString: String
  36. }
  37. extension LauncherApp {
  38. static let createNewTile = LauncherApp(
  39. id: UUID(uuidString: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA")!,
  40. name: "Create New App",
  41. assetIconName: "icon_create_new_app",
  42. fallbackSymbolName: "plus",
  43. description: "Create and add a custom app shortcut.",
  44. isCreateNew: true,
  45. webURL: nil
  46. )
  47. static let sampleApps: [LauncherApp] = [
  48. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000001")!, name: "Google Shopping", assetIconName: "icon_google_shopping", fallbackSymbolName: "bag.fill", description: "Explore products and offers.", isCreateNew: false, webURL: URL(string: "https://shopping.google.com")),
  49. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000002")!, name: "Gmail", assetIconName: "icon_gmail", fallbackSymbolName: "envelope.fill", description: "Read and send emails quickly.", isCreateNew: false, webURL: URL(string: "https://mail.google.com")),
  50. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000003")!, name: "Google Docs", assetIconName: "icon_google_docs", fallbackSymbolName: "doc.text.fill", description: "Create and edit documents.", isCreateNew: false, webURL: URL(string: "https://docs.google.com")),
  51. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000004")!, name: "Google Drive", assetIconName: "icon_google_drive", fallbackSymbolName: "externaldrive.fill", description: "Store and sync your files.", isCreateNew: false, webURL: URL(string: "https://drive.google.com")),
  52. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000005")!, name: "Google Earth", assetIconName: "icon_google_earth", fallbackSymbolName: "globe.americas.fill", description: "Explore places worldwide.", isCreateNew: false, webURL: URL(string: "https://earth.google.com")),
  53. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000006")!, name: "Google Photos", assetIconName: "icon_google_photos", fallbackSymbolName: "photo.stack.fill", description: "View and organize photos.", isCreateNew: false, webURL: URL(string: "https://photos.google.com")),
  54. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000007")!, name: "Google Maps", assetIconName: "icon_google_maps", fallbackSymbolName: "map.fill", description: "Find routes and places.", isCreateNew: false, webURL: URL(string: "https://maps.google.com")),
  55. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000008")!, name: "Google Translate", assetIconName: "icon_google_translate", fallbackSymbolName: "character.bubble.fill", description: "Translate text and speech.", isCreateNew: false, webURL: URL(string: "https://translate.google.com")),
  56. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000009")!, name: "Google Sheets", assetIconName: "icon_google_sheets", fallbackSymbolName: "tablecells.fill", description: "Work with spreadsheets.", isCreateNew: false, webURL: URL(string: "https://sheets.google.com")),
  57. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000010")!, name: "Google Search", assetIconName: "icon_google_search", fallbackSymbolName: "magnifyingglass.circle.fill", description: "Search the web quickly.", isCreateNew: false, webURL: URL(string: "https://www.google.com")),
  58. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000011")!, name: "YouTube", assetIconName: "icon_youtube", fallbackSymbolName: "play.rectangle.fill", description: "Watch videos and channels.", isCreateNew: false, webURL: URL(string: "https://www.youtube.com")),
  59. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000012")!, name: "Google Calendar", assetIconName: "icon_google_calendar", fallbackSymbolName: "calendar", description: "Manage events and schedules.", isCreateNew: false, webURL: URL(string: "https://calendar.google.com")),
  60. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000013")!, name: "Google Keep", assetIconName: "icon_google_keep", fallbackSymbolName: "lightbulb.fill", description: "Capture notes and reminders.", isCreateNew: false, webURL: URL(string: "https://keep.google.com")),
  61. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000014")!, name: "Google Slides", assetIconName: "icon_google_slides", fallbackSymbolName: "rectangle.on.rectangle.fill", description: "Build and present slides.", isCreateNew: false, webURL: URL(string: "https://slides.google.com")),
  62. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000015")!, name: "Google Forms", assetIconName: "icon_google_forms", fallbackSymbolName: "list.bullet.rectangle.fill", description: "Create forms and surveys.", isCreateNew: false, webURL: URL(string: "https://forms.google.com")),
  63. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000016")!, name: "Google Books", assetIconName: "icon_google_books", fallbackSymbolName: "book.fill", description: "Browse and read books.", isCreateNew: false, webURL: URL(string: "https://books.google.com")),
  64. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000017")!, name: "Google Travel", assetIconName: "icon_google_travel", fallbackSymbolName: "airplane", description: "Plan and organize trips.", isCreateNew: false, webURL: URL(string: "https://travel.google.com")),
  65. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000018")!, name: "Google Meet", assetIconName: "icon_google_meet", fallbackSymbolName: "video.fill", description: "Start and join video meetings.", isCreateNew: false, webURL: URL(string: "https://meet.google.com")),
  66. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000019")!, name: "Google Contacts", assetIconName: "icon_google_contacts", fallbackSymbolName: "person.crop.circle.fill", description: "Manage your contacts.", isCreateNew: false, webURL: URL(string: "https://contacts.google.com")),
  67. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001A")!, name: "Google Blogger", assetIconName: "icon_google_blogger", fallbackSymbolName: "text.book.closed.fill", description: "Publish and read blogs.", isCreateNew: false, webURL: URL(string: "https://www.blogger.com")),
  68. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001B")!, name: "Google Play", assetIconName: "icon_google_play", fallbackSymbolName: "play.circle.fill", description: "Apps, games, movies, and books.", isCreateNew: false, webURL: URL(string: "https://play.google.com")),
  69. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001C")!, name: "Google News", assetIconName: "icon_google_news", fallbackSymbolName: "newspaper.fill", description: "Headlines tailored to you.", isCreateNew: false, webURL: URL(string: "https://news.google.com")),
  70. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001D")!, name: "Google Chat", assetIconName: "icon_google_chat", fallbackSymbolName: "bubble.left.and.bubble.right.fill", description: "Team messaging and spaces.", isCreateNew: false, webURL: URL(string: "https://chat.google.com")),
  71. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001E")!, name: "Google Finance", assetIconName: "icon_google_finance", fallbackSymbolName: "chart.line.uptrend.xyaxis", description: "Markets, news, and portfolios.", isCreateNew: false, webURL: URL(string: "https://www.google.com/finance")),
  72. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-00000000001F")!, name: "Google Jamboard", assetIconName: "icon_google_jamboard", fallbackSymbolName: "paintbrush.fill", description: "Collaborative whiteboards.", isCreateNew: false, webURL: URL(string: "https://jamboard.google.com")),
  73. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000020")!, name: "Google Classroom", assetIconName: "icon_google_classroom", fallbackSymbolName: "graduationcap.fill", description: "Teaching and learning hub.", isCreateNew: false, webURL: URL(string: "https://classroom.google.com")),
  74. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000021")!, name: "Google Arts & Culture", assetIconName: "icon_google_arts_culture", fallbackSymbolName: "building.columns.fill", description: "Museums, art, and history.", isCreateNew: false, webURL: URL(string: "https://artsandculture.google.com")),
  75. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000022")!, name: "Google Voice", assetIconName: "icon_google_voice", fallbackSymbolName: "phone.fill", description: "Calls, voicemail, and SMS.", isCreateNew: false, webURL: URL(string: "https://voice.google.com")),
  76. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000023")!, name: "Chrome Web Store", assetIconName: "icon_chrome_web_store", fallbackSymbolName: "bag.fill", description: "Extensions and themes for Chrome.", isCreateNew: false, webURL: URL(string: "https://chrome.google.com/webstore")),
  77. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000024")!, name: "Google Fi Wireless", assetIconName: "icon_google_fi", fallbackSymbolName: "antenna.radiowaves.left.and.right", description: "Phone plan and account.", isCreateNew: false, webURL: URL(string: "https://fi.google.com")),
  78. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000025")!, name: "Google Ads", assetIconName: "icon_google_ads", fallbackSymbolName: "megaphone.fill", description: "Advertising campaigns.", isCreateNew: false, webURL: URL(string: "https://ads.google.com")),
  79. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000026")!, name: "Chrome Remote Desktop", assetIconName: "icon_chrome_remote_desktop", fallbackSymbolName: "desktopcomputer", description: "Access computers remotely.", isCreateNew: false, webURL: URL(string: "https://remotedesktop.google.com")),
  80. LauncherApp(id: UUID(uuidString: "F0000000-0000-0000-0000-000000000027")!, name: "Gemini", assetIconName: "icon_gemini", fallbackSymbolName: "sparkles", description: "Google AI assistant.", isCreateNew: false, webURL: URL(string: "https://gemini.google.com")),
  81. ]
  82. static func customAppsFromStorageJSON(_ json: String) -> [LauncherApp] {
  83. guard !json.isEmpty, let data = json.data(using: .utf8) else { return [] }
  84. do {
  85. let persisted = try JSONDecoder().decode([PersistedCustomApp].self, from: data)
  86. return persisted.compactMap { item in
  87. guard let parsedURL = URL(string: item.urlString) else { return nil }
  88. return LauncherApp(
  89. id: item.id,
  90. name: item.name,
  91. assetIconName: "",
  92. fallbackSymbolName: "globe",
  93. description: "Custom app shortcut.",
  94. isCreateNew: false,
  95. webURL: parsedURL
  96. )
  97. }
  98. } catch {
  99. return []
  100. }
  101. }
  102. static func allLaunchableAppsFromUserDefaults() -> [LauncherApp] {
  103. let customJSON = UserDefaults.standard.string(forKey: "customLauncherAppsData") ?? ""
  104. return sampleApps + customAppsFromStorageJSON(customJSON)
  105. }
  106. static func statusBarShortcutIDSet() -> Set<UUID> {
  107. let raw = UserDefaults.standard.string(forKey: "statusBarAppIDsData") ?? ""
  108. guard !raw.isEmpty, let data = raw.data(using: .utf8) else { return [] }
  109. do {
  110. let strings = try JSONDecoder().decode([String].self, from: data)
  111. return Set(strings.compactMap(UUID.init(uuidString:)))
  112. } catch {
  113. return []
  114. }
  115. }
  116. static func statusBarShortcutsOrdered() -> [LauncherApp] {
  117. let ids = statusBarShortcutIDSet()
  118. return allLaunchableAppsFromUserDefaults()
  119. .filter { ids.contains($0.id) }
  120. .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
  121. }
  122. /// Writes `statusBarAppIDsData` in the same JSON format as SwiftUI `@AppStorage`.
  123. static func persistStatusBarShortcutIDs(_ ids: Set<UUID>) {
  124. do {
  125. let strings = ids.map(\.uuidString).sorted()
  126. let data = try JSONEncoder().encode(strings)
  127. UserDefaults.standard.set(String(decoding: data, as: UTF8.self), forKey: "statusBarAppIDsData")
  128. LauncherApp.notifyStatusBarShortcutIdentifiersChanged()
  129. } catch {
  130. // Ignore persistence failures; menu bar state may be slightly stale until next change.
  131. }
  132. }
  133. static func notifyStatusBarShortcutIdentifiersChanged() {
  134. DispatchQueue.main.async {
  135. NotificationCenter.default.post(name: .statusBarShortcutIDsChanged, object: nil)
  136. }
  137. }
  138. }