Atoll/DynamicIsland/DynamicIslandViewCoordinator.swift
2026-01-15 23:31:07 +05:30

342 lines
11 KiB
Swift

//
// DynamicIslandViewCoordinator.swift
// DynamicIsland
//
// Created by Alexander on 2024-11-20.
//
import Combine
import Defaults
import SwiftUI
enum SneakContentType: Equatable {
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
case extensionLiveActivity(bundleID: String, activityID: String)
}
extension SneakContentType {
static func == (lhs: SneakContentType, rhs: SneakContentType) -> Bool {
switch (lhs, rhs) {
case (.brightness, .brightness),
(.volume, .volume),
(.backlight, .backlight),
(.music, .music),
(.mic, .mic),
(.battery, .battery),
(.download, .download),
(.timer, .timer),
(.reminder, .reminder),
(.recording, .recording),
(.doNotDisturb, .doNotDisturb),
(.bluetoothAudio, .bluetoothAudio),
(.privacy, .privacy),
(.lockScreen, .lockScreen),
(.capsLock, .capsLock):
return true
case let (.extensionLiveActivity(lb, la), .extensionLiveActivity(rb, ra)):
return lb == rb && la == ra
default:
return false
}
}
}
struct sneakPeek {
var show: Bool = false
var type: SneakContentType = .music
var value: CGFloat = 0
var icon: String = ""
var title: String = ""
var subtitle: String = ""
var accentColor: Color?
var styleOverride: SneakPeekStyle? = nil
}
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<AnyCancellable>()
@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 = "",
title: String = "",
subtitle: String = "",
accentColor: Color? = nil,
styleOverride: SneakPeekStyle? = nil
) {
let resolvedDuration: TimeInterval
switch type {
case .timer:
resolvedDuration = 10
case .reminder:
resolvedDuration = Defaults[.reminderSneakPeekDuration]
case .extensionLiveActivity:
resolvedDuration = duration
default:
resolvedDuration = duration
}
sneakPeekDuration = resolvedDuration
let bypassedTypes: [SneakContentType] = [.music, .timer, .reminder, .bluetoothAudio]
// Check if it's an extension type
let isExtensionType: Bool
if case .extensionLiveActivity = type {
isExtensionType = true
} else {
isExtensionType = false
}
if !isExtensionType && !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
self.sneakPeek.title = title
self.sneakPeek.subtitle = subtitle
self.sneakPeek.accentColor = accentColor
self.sneakPeek.styleOverride = styleOverride
}
}
}
private var sneakPeekDuration: TimeInterval = 1.5
private var sneakPeekTask: Task<Void, Never>?
// 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<Void, Never>?
@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()
}
}