// // DynamicIslandViewCoordinator.swift // DynamicIsland // // Created by Alexander on 2024-11-20. // import Combine import Defaults import SwiftUI enum SneakContentType { case brightness case volume case backlight case music case mic case battery case download case timer case reminder case recording case doNotDisturb case bluetoothAudio case privacy case lockScreen case capsLock } struct sneakPeek { var show: Bool = false var type: SneakContentType = .music var value: CGFloat = 0 var icon: String = "" } enum BrowserType { case chromium case safari } struct ExpandedItem { var show: Bool = false var type: SneakContentType = .battery var value: CGFloat = 0 var browser: BrowserType = .chromium } class DynamicIslandViewCoordinator: ObservableObject { static let shared = DynamicIslandViewCoordinator() private var cancellables = Set() @Published var currentView: NotchViews = .home { didSet { if Defaults[.enableMinimalisticUI] && currentView != .home { currentView = .home return } handleStatsTabTransition(from: oldValue, to: currentView) } } @Published var statsSecondRowExpansion: CGFloat = 1 private var statsSecondRowWorkItem: DispatchWorkItem? private let statsSecondRowRevealDelay: TimeInterval = 0.5 private let statsSecondRowAnimationDuration: TimeInterval = 0.3 @Published var notesLayoutState: NotesLayoutState = .list @AppStorage("firstLaunch") var firstLaunch: Bool = true @AppStorage("showWhatsNew") var showWhatsNew: Bool = true @AppStorage("musicLiveActivityEnabled") var musicLiveActivityEnabled: Bool = true @AppStorage("timerLiveActivityEnabled") var timerLiveActivityEnabled: Bool = true @Default(.enableTimerFeature) private var enableTimerFeature @Default(.timerDisplayMode) private var timerDisplayMode @AppStorage("alwaysShowTabs") var alwaysShowTabs: Bool = true { didSet { if !alwaysShowTabs { openLastTabByDefault = false if TrayDrop.shared.isEmpty || !Defaults[.openShelfByDefault] { currentView = .home } } } } @AppStorage("openLastTabByDefault") var openLastTabByDefault: Bool = false { didSet { if openLastTabByDefault { alwaysShowTabs = true } } } @AppStorage("hudReplacement") var hudReplacement: Bool = true @AppStorage("preferred_screen_name") var preferredScreen = NSScreen.main?.localizedName ?? "Unknown" { didSet { selectedScreen = preferredScreen NotificationCenter.default.post(name: Notification.Name.selectedScreenChanged, object: nil) } } @Published var selectedScreen: String = NSScreen.main?.localizedName ?? "Unknown" @Published var optionKeyPressed: Bool = true private init() { selectedScreen = preferredScreen Defaults.publisher(.timerDisplayMode) .receive(on: DispatchQueue.main) .sink { [weak self] change in self?.handleTimerDisplayModeChange(change.newValue) } .store(in: &cancellables) Defaults.publisher(.enableTimerFeature) .receive(on: DispatchQueue.main) .sink { [weak self] change in self?.handleTimerFeatureToggle(change.newValue) } .store(in: &cancellables) Defaults.publisher(.enableMinimalisticUI) .receive(on: DispatchQueue.main) .sink { [weak self] change in self?.handleMinimalisticModeChange(change.newValue) } .store(in: &cancellables) } private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) { guard oldValue != newValue else { return } statsSecondRowWorkItem?.cancel() if newValue == .stats && Defaults[.enableStatsFeature] { statsSecondRowExpansion = 0 let workItem = DispatchWorkItem { [weak self] in guard let self else { return } withAnimation(.easeInOut(duration: self.statsSecondRowAnimationDuration)) { self.statsSecondRowExpansion = 1 } self.statsSecondRowWorkItem = nil } statsSecondRowWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + statsSecondRowRevealDelay, execute: workItem) } else { withAnimation(.easeInOut(duration: 0.2)) { statsSecondRowExpansion = 0 } } } private func handleTimerDisplayModeChange(_ mode: TimerDisplayMode) { guard mode == .popover, currentView == .timer else { return } withAnimation(.smooth) { currentView = .home } } private func handleTimerFeatureToggle(_ isEnabled: Bool) { guard !isEnabled, currentView == .timer else { return } withAnimation(.smooth) { currentView = .home } } private func handleMinimalisticModeChange(_ isEnabled: Bool) { guard isEnabled else { return } if currentView != .home { withAnimation(.smooth) { currentView = .home } } } func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { let resolvedDuration: TimeInterval switch type { case .timer: resolvedDuration = 10 case .reminder: resolvedDuration = Defaults[.reminderSneakPeekDuration] default: resolvedDuration = duration } sneakPeekDuration = resolvedDuration let bypassedTypes: [SneakContentType] = [.music, .timer, .reminder, .bluetoothAudio] if !bypassedTypes.contains(type) && !Defaults[.enableSystemHUD] { return } DispatchQueue.main.async { withAnimation(.smooth(duration: 0.3)) { self.sneakPeek.show = status self.sneakPeek.type = type self.sneakPeek.value = value self.sneakPeek.icon = icon } } } private var sneakPeekDuration: TimeInterval = 1.5 private var sneakPeekTask: Task? // Helper function to manage sneakPeek timer using Swift Concurrency private func scheduleSneakPeekHide(after duration: TimeInterval) { sneakPeekTask?.cancel() // Don't schedule auto-hide if duration is infinite (for persistent indicators like Caps Lock) guard duration.isFinite else { return } sneakPeekTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) guard let self = self, !Task.isCancelled else { return } await MainActor.run { withAnimation { // Hide the sneak peek with the correct type that was showing self.toggleSneakPeek(status: false, type: self.sneakPeek.type) self.sneakPeekDuration = 1.5 } } } } @Published var sneakPeek: sneakPeek = .init() { didSet { if sneakPeek.show { scheduleSneakPeekHide(after: sneakPeekDuration) } else { sneakPeekTask?.cancel() } } } func toggleExpandingView( status: Bool, type: SneakContentType, value: CGFloat = 0, browser: BrowserType = .chromium ) { Task { @MainActor in withAnimation(.smooth) { self.expandingView.show = status self.expandingView.type = type self.expandingView.value = value self.expandingView.browser = browser } } } private var expandingViewTask: Task? @Published var expandingView: ExpandedItem = .init() { didSet { if expandingView.show { expandingViewTask?.cancel() // Only auto-hide for battery, not for downloads (DownloadManager handles that) if expandingView.type != .download { let duration: TimeInterval = 3 expandingViewTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) guard let self = self, !Task.isCancelled else { return } self.toggleExpandingView(status: false, type: .battery) } } } else { expandingViewTask?.cancel() } } } func showEmpty() { currentView = .home } // MARK: - Clipboard Management @Published var shouldToggleClipboardPopover: Bool = false func toggleClipboardPopover() { // Toggle the published property to trigger UI updates shouldToggleClipboardPopover.toggle() } }