AppDelegate.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. //
  2. // AppDelegate.swift
  3. // zoom_app
  4. //
  5. // Created by Dev Mac 1 on 14/04/2026.
  6. //
  7. import Cocoa
  8. import UserNotifications
  9. @main
  10. class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
  11. private let darkModeDefaultsKey = "settings.darkModeEnabled"
  12. private let loginStateKey = "zoom_app.isLoggedIn"
  13. private var statusItem: NSStatusItem?
  14. private var statusMenu: NSMenu?
  15. private var mainWindowController: NSWindowController?
  16. private weak var mainWindow: NSWindow?
  17. private var mainOriginalContentViewController: NSViewController?
  18. private enum MenuItemTag: Int {
  19. case openHome = 10
  20. case openMeetings = 11
  21. case openScheduler = 12
  22. case openWidgets = 13
  23. case openSettings = 14
  24. case newMeeting = 20
  25. case joinMeeting = 21
  26. case scheduleMeeting = 22
  27. case stopRecording = 23
  28. case auth = 30
  29. case quit = 99
  30. }
  31. private enum DefaultWindowSize {
  32. static let width: CGFloat = 1020
  33. static let height: CGFloat = 690
  34. }
  35. func applicationDidFinishLaunching(_ aNotification: Notification) {
  36. // Ensure we are a regular app so windows can appear reliably.
  37. // (Some configurations behave like an accessory/menubar app otherwise.)
  38. NSApp.setActivationPolicy(.regular)
  39. let darkEnabled = systemPrefersDarkMode()
  40. UserDefaults.standard.set(darkEnabled, forKey: darkModeDefaultsKey)
  41. NSApp.appearance = NSAppearance(named: darkEnabled ? .darkAqua : .aqua)
  42. UNUserNotificationCenter.current().delegate = self
  43. configureStatusItem()
  44. showSplashThenMainWindow()
  45. }
  46. func applicationWillTerminate(_ aNotification: Notification) {
  47. // Insert code here to tear down your application
  48. }
  49. func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
  50. // If enabled, macOS can restore the last window frame (size/position) on next launch.
  51. // This app wants a consistent default launch size instead.
  52. return false
  53. }
  54. private func applyDefaultWindowSizeIfNeeded() {
  55. let targetSize = NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height)
  56. for window in NSApplication.shared.windows {
  57. // Only touch the main app window (not OAuth popups, alerts, etc.).
  58. guard window.contentViewController is ViewController else { continue }
  59. window.isRestorable = false
  60. window.setContentSize(targetSize)
  61. window.center()
  62. }
  63. }
  64. private func systemPrefersDarkMode() -> Bool {
  65. let global = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain)
  66. let style = global?["AppleInterfaceStyle"] as? String
  67. return style?.lowercased() == "dark"
  68. }
  69. private func showSplashThenMainWindow() {
  70. let (window, originalVC) = openMainWindowIfNeededAndReturnWindow()
  71. guard let window else { return }
  72. let splashVC = SplashViewController()
  73. mainOriginalContentViewController = originalVC
  74. mainWindow = window
  75. // Show main window immediately with splash content.
  76. window.contentViewController = splashVC
  77. window.isRestorable = false
  78. window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
  79. window.center()
  80. NSApp.activate(ignoringOtherApps: true)
  81. window.makeKeyAndOrderFront(nil)
  82. Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
  83. guard let self else { return }
  84. self.restoreMainContentAfterSplash()
  85. }
  86. }
  87. @discardableResult
  88. private func openMainWindowIfNeededAndReturnWindow() -> (NSWindow?, NSViewController?) {
  89. if let existing = windowHostingViewController() {
  90. existing.isRestorable = false
  91. existing.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
  92. existing.center()
  93. existing.makeKeyAndOrderFront(nil)
  94. return (existing, existing.contentViewController)
  95. }
  96. let storyboard = NSStoryboard(name: "Main", bundle: nil)
  97. if let controller = storyboard.instantiateInitialController() as? NSWindowController {
  98. mainWindowController = controller
  99. controller.showWindow(nil)
  100. if let window = controller.window {
  101. window.isRestorable = false
  102. window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
  103. window.center()
  104. window.makeKeyAndOrderFront(nil)
  105. return (window, window.contentViewController)
  106. }
  107. }
  108. return (nil, nil)
  109. }
  110. private func restoreMainContentAfterSplash() {
  111. guard let window = mainWindow else { return }
  112. // If we captured the original VC (from storyboard), restore it.
  113. if let original = mainOriginalContentViewController {
  114. window.contentViewController = original
  115. }
  116. window.isRestorable = false
  117. window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
  118. window.center()
  119. window.makeKeyAndOrderFront(nil)
  120. mainOriginalContentViewController = nil
  121. mainWindow = nil
  122. }
  123. private func configureStatusItem() {
  124. let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
  125. if let button = item.button {
  126. let symbolConfig = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
  127. if let base = NSImage(systemSymbolName: "video.fill", accessibilityDescription: "Zoom App"),
  128. let image = base.withSymbolConfiguration(symbolConfig) {
  129. image.isTemplate = true
  130. button.image = image
  131. button.imagePosition = .imageOnly
  132. button.toolTip = "Zoom App"
  133. } else {
  134. button.title = "Z"
  135. }
  136. }
  137. let menu = NSMenu()
  138. menu.delegate = self
  139. item.menu = menu
  140. statusItem = item
  141. statusMenu = menu
  142. rebuildStatusMenu()
  143. }
  144. func menuWillOpen(_ menu: NSMenu) {
  145. rebuildStatusMenu()
  146. }
  147. private func rebuildStatusMenu() {
  148. guard let menu = statusMenu else { return }
  149. menu.removeAllItems()
  150. appendMenuItem("Open Home", tag: .openHome, to: menu)
  151. appendMenuItem("Open Meetings", tag: .openMeetings, to: menu)
  152. appendMenuItem("Open Scheduler", tag: .openScheduler, to: menu)
  153. appendMenuItem("Open Widgets", tag: .openWidgets, to: menu)
  154. appendMenuItem("Open Settings", tag: .openSettings, to: menu)
  155. menu.addItem(.separator())
  156. appendMenuItem("New Meeting", tag: .newMeeting, to: menu)
  157. appendMenuItem("Join Meeting", tag: .joinMeeting, to: menu)
  158. appendMenuItem("Schedule Meeting", tag: .scheduleMeeting, to: menu)
  159. if MeetingRecordingManager.shared.isRecording {
  160. menu.addItem(.separator())
  161. appendMenuItem("Stop Recording", tag: .stopRecording, to: menu)
  162. }
  163. menu.addItem(.separator())
  164. let authTitle = isUserLoggedInForMenu() ? "Sign Out" : "Sign In"
  165. appendMenuItem(authTitle, tag: .auth, to: menu)
  166. menu.addItem(.separator())
  167. appendMenuItem("Quit", tag: .quit, to: menu)
  168. }
  169. private func appendMenuItem(_ title: String, tag: MenuItemTag, to menu: NSMenu) {
  170. let item = NSMenuItem(title: title, action: #selector(handleStatusMenuAction(_:)), keyEquivalent: "")
  171. item.target = self
  172. item.tag = tag.rawValue
  173. menu.addItem(item)
  174. }
  175. @objc
  176. private func handleStatusMenuAction(_ sender: NSMenuItem) {
  177. guard let action = MenuItemTag(rawValue: sender.tag) else { return }
  178. if action == .quit {
  179. NSApp.terminate(nil)
  180. return
  181. }
  182. focusMainAppWindow()
  183. guard let controller = mainViewController() else { return }
  184. switch action {
  185. case .openHome:
  186. controller.menuBarShowSection(.home)
  187. case .openMeetings:
  188. controller.menuBarShowSection(.meetings)
  189. case .openScheduler:
  190. controller.menuBarShowSection(.scheduler)
  191. case .openWidgets:
  192. controller.menuBarShowSection(.widgets)
  193. case .openSettings:
  194. controller.menuBarShowSection(.settings)
  195. case .newMeeting:
  196. controller.menuBarTriggerNewMeeting()
  197. case .joinMeeting:
  198. controller.menuBarTriggerJoinMeeting()
  199. case .scheduleMeeting:
  200. controller.menuBarTriggerScheduleMeeting()
  201. case .stopRecording:
  202. Task { @MainActor in
  203. do {
  204. try await MeetingRecordingManager.shared.stopRecording()
  205. } catch {
  206. let alert = NSAlert()
  207. alert.messageText = "Could not save recording"
  208. alert.informativeText = error.localizedDescription
  209. alert.alertStyle = .warning
  210. alert.runModal()
  211. }
  212. }
  213. case .auth:
  214. controller.menuBarTriggerAuthAction()
  215. case .quit:
  216. break
  217. }
  218. }
  219. private func focusMainAppWindow() {
  220. NSApp.activate(ignoringOtherApps: true)
  221. if let window = windowHostingViewController() {
  222. window.makeKeyAndOrderFront(nil)
  223. } else {
  224. NSApp.windows.first?.makeKeyAndOrderFront(nil)
  225. }
  226. }
  227. private func windowHostingViewController() -> NSWindow? {
  228. NSApp.windows.first(where: { $0.contentViewController is ViewController })
  229. }
  230. private func mainViewController() -> ViewController? {
  231. if let controller = NSApp.keyWindow?.contentViewController as? ViewController {
  232. return controller
  233. }
  234. if let controller = NSApp.mainWindow?.contentViewController as? ViewController {
  235. return controller
  236. }
  237. if let controller = windowHostingViewController()?.contentViewController as? ViewController {
  238. return controller
  239. }
  240. return nil
  241. }
  242. private func isUserLoggedInForMenu() -> Bool {
  243. if let controller = mainViewController() {
  244. return controller.menuBarIsUserLoggedIn()
  245. }
  246. return UserDefaults.standard.bool(forKey: loginStateKey)
  247. }
  248. }
  249. extension AppDelegate: UNUserNotificationCenterDelegate {
  250. func userNotificationCenter(
  251. _ center: UNUserNotificationCenter,
  252. didReceive response: UNNotificationResponse,
  253. withCompletionHandler completionHandler: @escaping () -> Void
  254. ) {
  255. NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
  256. NSApp.activate(ignoringOtherApps: true)
  257. completionHandler()
  258. }
  259. func userNotificationCenter(
  260. _ center: UNUserNotificationCenter,
  261. willPresent notification: UNNotification,
  262. withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  263. ) {
  264. completionHandler([.banner, .sound])
  265. }
  266. }
  267. private final class SplashViewController: NSViewController {
  268. private struct Theme {
  269. let isDark: Bool
  270. let backgroundTop: NSColor
  271. let backgroundBottom: NSColor
  272. let titleColor: NSColor
  273. let subtitleColor: NSColor
  274. let statusColor: NSColor
  275. let trackColor: NSColor
  276. let fillColor: NSColor
  277. let iconBadgeFill: NSColor
  278. }
  279. private final class GradientBackgroundView: NSView {
  280. let gradientLayer = CAGradientLayer()
  281. override init(frame frameRect: NSRect) {
  282. super.init(frame: frameRect)
  283. wantsLayer = true
  284. layer = CALayer()
  285. gradientLayer.startPoint = CGPoint(x: 0.1, y: 1.0)
  286. gradientLayer.endPoint = CGPoint(x: 0.9, y: 0.0)
  287. gradientLayer.locations = [0.0, 1.0]
  288. layer?.addSublayer(gradientLayer)
  289. }
  290. @available(*, unavailable)
  291. required init?(coder: NSCoder) { nil }
  292. override func layout() {
  293. super.layout()
  294. gradientLayer.frame = bounds
  295. }
  296. }
  297. private var theme: Theme = SplashViewController.makeTheme(isDark: true)
  298. private var dotsTimer: Timer?
  299. private var dotsStep: Int = 0
  300. private var progressTimer: Timer?
  301. private var progressValue: Double = 0
  302. private weak var rootBackground: GradientBackgroundView?
  303. private weak var iconBadgeView: NSView?
  304. private weak var iconView: NSImageView?
  305. private weak var titleLabel: NSTextField?
  306. private weak var subtitleLabel: NSTextField?
  307. private weak var contentStack: NSStackView?
  308. private weak var statusLabel: NSTextField?
  309. private weak var percentLabel: NSTextField?
  310. private weak var progressIndicator: NSProgressIndicator?
  311. override func viewDidLoad() {
  312. super.viewDidLoad()
  313. applyTheme()
  314. }
  315. override func viewDidAppear() {
  316. super.viewDidAppear()
  317. startAnimations()
  318. startDots()
  319. startProgress()
  320. }
  321. override func viewWillDisappear() {
  322. super.viewWillDisappear()
  323. dotsTimer?.invalidate()
  324. dotsTimer = nil
  325. progressTimer?.invalidate()
  326. progressTimer = nil
  327. }
  328. override func loadView() {
  329. let background = GradientBackgroundView()
  330. let stack = NSStackView()
  331. stack.orientation = .vertical
  332. stack.alignment = .centerX
  333. stack.spacing = 10
  334. stack.translatesAutoresizingMaskIntoConstraints = false
  335. let iconBadge = NSView()
  336. iconBadge.translatesAutoresizingMaskIntoConstraints = false
  337. iconBadge.wantsLayer = true
  338. iconBadge.layer?.cornerRadius = 26
  339. iconBadge.layer?.masksToBounds = true
  340. let iconView = NSImageView()
  341. iconView.translatesAutoresizingMaskIntoConstraints = false
  342. iconView.imageScaling = .scaleProportionallyUpOrDown
  343. iconView.image = NSApp.applicationIconImage
  344. iconView.wantsLayer = true
  345. let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Zoom App"
  346. let title = NSTextField(labelWithString: appName)
  347. title.font = .systemFont(ofSize: 30, weight: .semibold)
  348. let subtitle = NSTextField(labelWithString: "Preparing your meetings workspace.")
  349. subtitle.font = .systemFont(ofSize: 14, weight: .regular)
  350. subtitle.alignment = .center
  351. subtitle.maximumNumberOfLines = 2
  352. subtitle.lineBreakMode = .byWordWrapping
  353. let progressRow = NSStackView()
  354. progressRow.orientation = .horizontal
  355. progressRow.alignment = .centerY
  356. progressRow.spacing = 10
  357. progressRow.translatesAutoresizingMaskIntoConstraints = false
  358. let progress = NSProgressIndicator()
  359. progress.translatesAutoresizingMaskIntoConstraints = false
  360. progress.isIndeterminate = false
  361. progress.minValue = 0
  362. progress.maxValue = 100
  363. progress.doubleValue = 10
  364. progress.controlSize = .small
  365. progress.style = .bar
  366. let percent = NSTextField(labelWithString: "10%")
  367. percent.font = .systemFont(ofSize: 12, weight: .medium)
  368. percent.alignment = .right
  369. progressRow.addArrangedSubview(progress)
  370. progressRow.addArrangedSubview(percent)
  371. let status = NSTextField(labelWithString: "CONNECTING SERVICES")
  372. status.font = .systemFont(ofSize: 10.5, weight: .semibold)
  373. status.alignment = .center
  374. status.usesSingleLineMode = true
  375. status.setAccessibilityLabel("Splash status")
  376. iconBadge.addSubview(iconView)
  377. NSLayoutConstraint.activate([
  378. iconView.centerXAnchor.constraint(equalTo: iconBadge.centerXAnchor),
  379. iconView.centerYAnchor.constraint(equalTo: iconBadge.centerYAnchor),
  380. iconView.widthAnchor.constraint(equalToConstant: 78),
  381. iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor)
  382. ])
  383. stack.addArrangedSubview(iconBadge)
  384. stack.addArrangedSubview(title)
  385. stack.addArrangedSubview(subtitle)
  386. stack.addArrangedSubview(progressRow)
  387. stack.addArrangedSubview(status)
  388. background.addSubview(stack)
  389. NSLayoutConstraint.activate([
  390. iconBadge.widthAnchor.constraint(equalToConstant: 104),
  391. iconBadge.heightAnchor.constraint(equalToConstant: 104),
  392. stack.centerXAnchor.constraint(equalTo: background.centerXAnchor),
  393. stack.centerYAnchor.constraint(equalTo: background.centerYAnchor),
  394. stack.leadingAnchor.constraint(greaterThanOrEqualTo: background.leadingAnchor, constant: 28),
  395. stack.trailingAnchor.constraint(lessThanOrEqualTo: background.trailingAnchor, constant: -28),
  396. progress.widthAnchor.constraint(equalToConstant: 260),
  397. progress.heightAnchor.constraint(equalToConstant: 6),
  398. percent.widthAnchor.constraint(equalToConstant: 44)
  399. ])
  400. // Initial state for animation.
  401. stack.alphaValue = 0
  402. stack.setFrameOrigin(NSPoint(x: stack.frame.origin.x, y: stack.frame.origin.y - 8))
  403. self.rootBackground = background
  404. self.iconBadgeView = iconBadge
  405. self.iconView = iconView
  406. self.titleLabel = title
  407. self.subtitleLabel = subtitle
  408. self.contentStack = stack
  409. self.statusLabel = status
  410. self.percentLabel = percent
  411. self.progressIndicator = progress
  412. view = background
  413. }
  414. private func startAnimations() {
  415. guard let stack = contentStack, let iconView else { return }
  416. // Entrance animation: fade + slight rise + gentle icon scale.
  417. iconView.layer?.transform = CATransform3DMakeScale(0.92, 0.92, 1)
  418. NSAnimationContext.runAnimationGroup { context in
  419. context.duration = 0.55
  420. context.timingFunction = CAMediaTimingFunction(name: .easeOut)
  421. stack.animator().alphaValue = 1
  422. }
  423. let scale = CABasicAnimation(keyPath: "transform")
  424. scale.fromValue = CATransform3DMakeScale(0.92, 0.92, 1)
  425. scale.toValue = CATransform3DIdentity
  426. scale.duration = 0.55
  427. scale.timingFunction = CAMediaTimingFunction(name: .easeOut)
  428. iconView.layer?.add(scale, forKey: "splash.scaleIn")
  429. iconView.layer?.transform = CATransform3DIdentity
  430. // Subtle breathing pulse after entrance.
  431. DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { [weak self] in
  432. guard let self, let iconLayer = self.iconView?.layer else { return }
  433. let pulse = CABasicAnimation(keyPath: "transform")
  434. pulse.fromValue = CATransform3DIdentity
  435. pulse.toValue = CATransform3DMakeScale(1.03, 1.03, 1)
  436. pulse.duration = 0.9
  437. pulse.autoreverses = true
  438. pulse.repeatCount = .infinity
  439. pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  440. iconLayer.add(pulse, forKey: "splash.pulse")
  441. }
  442. }
  443. private func startDots() {
  444. dotsTimer?.invalidate()
  445. dotsStep = 0
  446. dotsTimer = Timer.scheduledTimer(withTimeInterval: 0.45, repeats: true) { [weak self] _ in
  447. guard let self, let subtitleLabel else { return }
  448. self.dotsStep = (self.dotsStep + 1) % 4
  449. let dots = String(repeating: ".", count: self.dotsStep)
  450. // Keep the main subtitle stable; add subtle activity via ellipsis.
  451. subtitleLabel.stringValue = "Preparing your meetings workspace" + dots
  452. }
  453. }
  454. private func startProgress() {
  455. progressTimer?.invalidate()
  456. progressValue = 10
  457. setProgressUI(value: progressValue)
  458. // Animate progress smoothly for the 3s splash duration.
  459. let tick: TimeInterval = 0.06
  460. let target: Double = 100
  461. let duration: Double = 3.0
  462. let step = (target - progressValue) / (duration / tick)
  463. progressTimer = Timer.scheduledTimer(withTimeInterval: tick, repeats: true) { [weak self] timer in
  464. guard let self else { timer.invalidate(); return }
  465. self.progressValue = min(target, self.progressValue + step)
  466. self.setProgressUI(value: self.progressValue)
  467. if self.progressValue >= target - 0.01 {
  468. timer.invalidate()
  469. }
  470. }
  471. }
  472. private func setProgressUI(value: Double) {
  473. progressIndicator?.doubleValue = value
  474. percentLabel?.stringValue = "\(Int(round(value)))%"
  475. }
  476. private func applyTheme() {
  477. let isDark = view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
  478. theme = Self.makeTheme(isDark: isDark)
  479. rootBackground?.gradientLayer.colors = [theme.backgroundTop.cgColor, theme.backgroundBottom.cgColor]
  480. // Icon badge styling.
  481. if let badge = iconBadgeView {
  482. badge.layer?.backgroundColor = theme.iconBadgeFill.cgColor
  483. }
  484. titleLabel?.textColor = theme.titleColor
  485. subtitleLabel?.textColor = theme.subtitleColor
  486. statusLabel?.textColor = theme.statusColor
  487. percentLabel?.textColor = theme.subtitleColor
  488. if let progress = progressIndicator {
  489. progress.appearance = NSAppearance(named: theme.isDark ? .darkAqua : .aqua)
  490. // Prefer tinting when available; fall back gracefully if not.
  491. if #available(macOS 10.14, *) {
  492. progress.contentFilters = []
  493. }
  494. }
  495. }
  496. private static func makeTheme(isDark: Bool) -> Theme {
  497. if isDark {
  498. return Theme(
  499. isDark: true,
  500. backgroundTop: NSColor(calibratedRed: 9 / 255, green: 9 / 255, blue: 10 / 255, alpha: 1),
  501. backgroundBottom: NSColor(calibratedRed: 16 / 255, green: 16 / 255, blue: 18 / 255, alpha: 1),
  502. titleColor: NSColor(calibratedWhite: 0.98, alpha: 1),
  503. subtitleColor: NSColor(calibratedWhite: 0.78, alpha: 1),
  504. statusColor: NSColor(calibratedWhite: 0.66, alpha: 1),
  505. trackColor: NSColor.white.withAlphaComponent(0.10),
  506. fillColor: NSColor(calibratedRed: 96 / 255, green: 215 / 255, blue: 255 / 255, alpha: 1),
  507. iconBadgeFill: NSColor.white.withAlphaComponent(0.08)
  508. )
  509. }
  510. return Theme(
  511. isDark: false,
  512. backgroundTop: NSColor(calibratedWhite: 0.98, alpha: 1),
  513. backgroundBottom: NSColor(calibratedWhite: 0.92, alpha: 1),
  514. titleColor: NSColor(calibratedWhite: 0.10, alpha: 1),
  515. subtitleColor: NSColor(calibratedWhite: 0.34, alpha: 1),
  516. statusColor: NSColor(calibratedWhite: 0.42, alpha: 1),
  517. trackColor: NSColor.black.withAlphaComponent(0.10),
  518. fillColor: NSColor(calibratedRed: 0.18, green: 0.52, blue: 0.96, alpha: 1),
  519. iconBadgeFill: NSColor.black.withAlphaComponent(0.04)
  520. )
  521. }
  522. }