Atoll/DynamicIsland/DynamicIslandViewCoordinator.swift
2025-12-04 21:03:12 +01:00

231 lines
7.1 KiB
Swift

//
// 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
}
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()
@Published var currentView: NotchViews = .home {
didSet {
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
@AppStorage("firstLaunch") var firstLaunch: Bool = true
@AppStorage("showWhatsNew") var showWhatsNew: Bool = true
@AppStorage("musicLiveActivityEnabled") var musicLiveActivityEnabled: Bool = true
@AppStorage("timerLiveActivityEnabled") var timerLiveActivityEnabled: Bool = true
@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
}
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
}
}
}
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
if type != .music && type != .timer && type != .reminder {
// close()
if !Defaults[.enableSystemHUD] {
return
}
}
DispatchQueue.main.async {
withAnimation(.smooth) {
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<Void, Never>?
// Helper function to manage sneakPeek timer using Swift Concurrency
private func scheduleSneakPeekHide(after duration: TimeInterval) {
sneakPeekTask?.cancel()
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()
}
}