mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
342 lines
11 KiB
Swift
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()
|
|
}
|
|
}
|