|
|
@@ -245,6 +245,13 @@ final class ViewController: NSViewController {
|
|
245
|
245
|
private let typography = Typography()
|
|
246
|
246
|
private let launchContentSize = NSSize(width: 920, height: 690)
|
|
247
|
247
|
private let launchMinContentSize = NSSize(width: 760, height: 600)
|
|
|
248
|
+ private let launchSplashMinimumVisibleDuration: TimeInterval = 2
|
|
|
249
|
+ private let launchSplashTimeout: TimeInterval = 12
|
|
|
250
|
+ private var launchSplashView: LaunchSplashView?
|
|
|
251
|
+ private var launchSplashTimeoutWorkItem: DispatchWorkItem?
|
|
|
252
|
+ private var launchSplashMinimumDelayWorkItem: DispatchWorkItem?
|
|
|
253
|
+ private var launchSplashShownAt: Date?
|
|
|
254
|
+ private var hasDismissedLaunchSplash = false
|
|
248
|
255
|
|
|
249
|
256
|
private var mainContentHost: NSView?
|
|
250
|
257
|
/// Pin constraints for the current page inside `mainContentHost`; deactivated before each swap so relayout never stacks duplicates.
|
|
|
@@ -431,6 +438,7 @@ final class ViewController: NSViewController {
|
|
431
|
438
|
observeAppLifecycleForUsageTrackingIfNeeded()
|
|
432
|
439
|
setupRootView()
|
|
433
|
440
|
buildMainLayout()
|
|
|
441
|
+ showLaunchSplashIfNeeded()
|
|
434
|
442
|
startStoreKit()
|
|
435
|
443
|
}
|
|
436
|
444
|
|
|
|
@@ -438,6 +446,7 @@ final class ViewController: NSViewController {
|
|
438
|
446
|
super.viewDidAppear()
|
|
439
|
447
|
hasViewAppearedOnce = true
|
|
440
|
448
|
presentLaunchPaywallIfNeeded()
|
|
|
449
|
+ dismissLaunchSplashIfReady()
|
|
441
|
450
|
applyWindowTitle(for: selectedSidebarPage)
|
|
442
|
451
|
guard let window = view.window else { return }
|
|
443
|
452
|
configureMainWindowChrome(window)
|
|
|
@@ -473,6 +482,8 @@ final class ViewController: NSViewController {
|
|
473
|
482
|
deinit {
|
|
474
|
483
|
premiumUpgradeRatingPromptWorkItem?.cancel()
|
|
475
|
484
|
endUsageTrackingSession()
|
|
|
485
|
+ launchSplashTimeoutWorkItem?.cancel()
|
|
|
486
|
+ launchSplashMinimumDelayWorkItem?.cancel()
|
|
476
|
487
|
if hasObservedAppLifecycleForUsage {
|
|
477
|
488
|
NotificationCenter.default.removeObserver(self)
|
|
478
|
489
|
}
|
|
|
@@ -492,6 +503,97 @@ private extension ViewController {
|
|
492
|
503
|
view.layer?.backgroundColor = palette.pageBackground.cgColor
|
|
493
|
504
|
}
|
|
494
|
505
|
|
|
|
506
|
+ private func makeLaunchSplashTheme() -> LaunchSplashView.Theme {
|
|
|
507
|
+ let borderAlpha: CGFloat = darkModeEnabled ? 0.9 : 0.45
|
|
|
508
|
+ let cardBackground = darkModeEnabled ? palette.sectionCard : palette.tabBarBackground
|
|
|
509
|
+ let cardBorder = darkModeEnabled ? palette.inputBorder : palette.separator
|
|
|
510
|
+ return LaunchSplashView.Theme(
|
|
|
511
|
+ background: palette.pageBackground,
|
|
|
512
|
+ cardBackground: cardBackground,
|
|
|
513
|
+ cardBorder: cardBorder.withAlphaComponent(borderAlpha),
|
|
|
514
|
+ titleText: palette.textPrimary,
|
|
|
515
|
+ subtitleText: palette.textSecondary,
|
|
|
516
|
+ accent: palette.primaryBlue
|
|
|
517
|
+ )
|
|
|
518
|
+ }
|
|
|
519
|
+
|
|
|
520
|
+ private func showLaunchSplashIfNeeded() {
|
|
|
521
|
+ guard launchSplashView == nil else { return }
|
|
|
522
|
+ let splash = LaunchSplashView(frame: .zero)
|
|
|
523
|
+ splash.alphaValue = 1
|
|
|
524
|
+ let displayName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String)?
|
|
|
525
|
+ .trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
526
|
+ let fallbackName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
|
|
|
527
|
+ let resolvedName = (displayName?.isEmpty == false ? displayName : fallbackName) ?? "Assistant for Google Meet"
|
|
|
528
|
+ splash.configure(appName: resolvedName, appIcon: NSApp.applicationIconImage, theme: makeLaunchSplashTheme())
|
|
|
529
|
+
|
|
|
530
|
+ view.addSubview(splash)
|
|
|
531
|
+ NSLayoutConstraint.activate([
|
|
|
532
|
+ splash.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
533
|
+ splash.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
534
|
+ splash.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
535
|
+ splash.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
|
536
|
+ ])
|
|
|
537
|
+ view.layoutSubtreeIfNeeded()
|
|
|
538
|
+ launchSplashView = splash
|
|
|
539
|
+ launchSplashShownAt = Date()
|
|
|
540
|
+
|
|
|
541
|
+ launchSplashTimeoutWorkItem?.cancel()
|
|
|
542
|
+ let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
|
|
543
|
+ self?.dismissLaunchSplash(force: true)
|
|
|
544
|
+ }
|
|
|
545
|
+ launchSplashTimeoutWorkItem = timeoutWorkItem
|
|
|
546
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + launchSplashTimeout, execute: timeoutWorkItem)
|
|
|
547
|
+ }
|
|
|
548
|
+
|
|
|
549
|
+ private func dismissLaunchSplashIfReady() {
|
|
|
550
|
+ guard hasCompletedInitialStoreKitSync else { return }
|
|
|
551
|
+ guard !hasDismissedLaunchSplash else { return }
|
|
|
552
|
+ if let shownAt = launchSplashShownAt {
|
|
|
553
|
+ let elapsed = Date().timeIntervalSince(shownAt)
|
|
|
554
|
+ let remaining = launchSplashMinimumVisibleDuration - elapsed
|
|
|
555
|
+ if remaining > 0 {
|
|
|
556
|
+ launchSplashMinimumDelayWorkItem?.cancel()
|
|
|
557
|
+ let minDelayWorkItem = DispatchWorkItem { [weak self] in
|
|
|
558
|
+ self?.dismissLaunchSplash(force: false)
|
|
|
559
|
+ }
|
|
|
560
|
+ launchSplashMinimumDelayWorkItem = minDelayWorkItem
|
|
|
561
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + remaining, execute: minDelayWorkItem)
|
|
|
562
|
+ return
|
|
|
563
|
+ }
|
|
|
564
|
+ }
|
|
|
565
|
+ dismissLaunchSplash(force: false)
|
|
|
566
|
+ }
|
|
|
567
|
+
|
|
|
568
|
+ private func dismissLaunchSplash(force: Bool) {
|
|
|
569
|
+ guard !hasDismissedLaunchSplash else { return }
|
|
|
570
|
+ guard let splash = launchSplashView else { return }
|
|
|
571
|
+ if !force && !hasCompletedInitialStoreKitSync { return }
|
|
|
572
|
+
|
|
|
573
|
+ hasDismissedLaunchSplash = true
|
|
|
574
|
+ launchSplashTimeoutWorkItem?.cancel()
|
|
|
575
|
+ launchSplashTimeoutWorkItem = nil
|
|
|
576
|
+ launchSplashMinimumDelayWorkItem?.cancel()
|
|
|
577
|
+ launchSplashMinimumDelayWorkItem = nil
|
|
|
578
|
+
|
|
|
579
|
+ let removeSplash: () -> Void = { [weak self] in
|
|
|
580
|
+ splash.removeFromSuperview()
|
|
|
581
|
+ self?.launchSplashView = nil
|
|
|
582
|
+ self?.launchSplashShownAt = nil
|
|
|
583
|
+ }
|
|
|
584
|
+
|
|
|
585
|
+ if NSWorkspace.shared.accessibilityDisplayShouldReduceMotion {
|
|
|
586
|
+ removeSplash()
|
|
|
587
|
+ return
|
|
|
588
|
+ }
|
|
|
589
|
+
|
|
|
590
|
+ NSAnimationContext.runAnimationGroup({ context in
|
|
|
591
|
+ context.duration = 0.24
|
|
|
592
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
593
|
+ splash.animator().alphaValue = 0
|
|
|
594
|
+ }, completionHandler: removeSplash)
|
|
|
595
|
+ }
|
|
|
596
|
+
|
|
495
|
597
|
func systemPrefersDarkMode() -> Bool {
|
|
496
|
598
|
// Use the system-wide appearance setting (not app/window effective appearance).
|
|
497
|
599
|
// When the key is missing, macOS is in Light mode.
|
|
|
@@ -861,6 +963,7 @@ private extension ViewController {
|
|
861
|
963
|
NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
|
|
862
|
964
|
view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
|
|
863
|
965
|
palette = Palette(isDarkMode: enabled)
|
|
|
966
|
+ launchSplashView?.apply(theme: makeLaunchSplashTheme())
|
|
864
|
967
|
reloadTheme()
|
|
865
|
968
|
}
|
|
866
|
969
|
|
|
|
@@ -1282,6 +1385,7 @@ private extension ViewController {
|
|
1282
|
1385
|
self.hasCompletedInitialStoreKitSync = true
|
|
1283
|
1386
|
self.refreshPaywallStoreUI()
|
|
1284
|
1387
|
self.presentLaunchPaywallIfNeeded()
|
|
|
1388
|
+ self.dismissLaunchSplashIfReady()
|
|
1285
|
1389
|
}
|
|
1286
|
1390
|
}
|
|
1287
|
1391
|
|