289 lines
10 KiB
Swift
289 lines
10 KiB
Swift
//
|
|
// ClockViewModel.swift
|
|
// TheNoiseClock
|
|
//
|
|
// Created by Matt Bruce on 9/7/25.
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
import Observation
|
|
import AudioPlaybackKit
|
|
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// ViewModel for clock display and management
|
|
@Observable
|
|
class ClockViewModel {
|
|
|
|
// MARK: - Properties
|
|
private(set) var currentTime = Date()
|
|
private(set) var style = ClockStyle()
|
|
private(set) var isDisplayMode = false
|
|
|
|
// Wake lock service
|
|
private let wakeLockService = WakeLockService.shared
|
|
|
|
// Ambient light service
|
|
private let ambientLightService = AmbientLightService.shared
|
|
|
|
// Timer management
|
|
private var secondTimer: Timer.TimerPublisher?
|
|
private var minuteTimer: Timer.TimerPublisher?
|
|
private var secondCancellable: AnyCancellable?
|
|
private var minuteCancellable: AnyCancellable?
|
|
private var styleObserver: NSObjectProtocol?
|
|
|
|
// Persistence
|
|
private var persistenceWorkItem: DispatchWorkItem?
|
|
private var styleJSON: Data {
|
|
get {
|
|
UserDefaults.standard.data(forKey: ClockStyle.appStorageKey) ?? {
|
|
let def = ClockStyle()
|
|
return (try? JSONEncoder().encode(def)) ?? Data()
|
|
}()
|
|
}
|
|
set {
|
|
UserDefaults.standard.set(newValue, forKey: ClockStyle.appStorageKey)
|
|
}
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
init() {
|
|
loadStyle()
|
|
setupTimers()
|
|
startAmbientLightMonitoring()
|
|
observeStyleUpdates()
|
|
}
|
|
|
|
deinit {
|
|
stopTimers()
|
|
stopAmbientLightMonitoring()
|
|
if let styleObserver {
|
|
NotificationCenter.default.removeObserver(styleObserver)
|
|
}
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
func toggleDisplayMode() {
|
|
let oldValue = isDisplayMode
|
|
withAnimation(Design.Animation.spring(bounce: Design.Animation.springBounce)) {
|
|
isDisplayMode.toggle()
|
|
}
|
|
Design.debugLog("[ClockViewModel] toggleDisplayMode: \(oldValue) -> \(isDisplayMode)")
|
|
|
|
// Manage wake lock based on display mode and keep awake setting
|
|
updateWakeLockState()
|
|
if isDisplayMode {
|
|
requestKeepAwakePromptIfNeeded()
|
|
}
|
|
}
|
|
|
|
func setDisplayMode(_ enabled: Bool) {
|
|
guard isDisplayMode != enabled else {
|
|
Design.debugLog("[ClockViewModel] setDisplayMode(\(enabled)) - already at this value, skipping")
|
|
return
|
|
}
|
|
Design.debugLog("[ClockViewModel] setDisplayMode: \(isDisplayMode) -> \(enabled)")
|
|
withAnimation(Design.Animation.spring(bounce: Design.Animation.springBounce)) {
|
|
isDisplayMode = enabled
|
|
}
|
|
updateWakeLockState()
|
|
if enabled {
|
|
requestKeepAwakePromptIfNeeded()
|
|
}
|
|
}
|
|
|
|
func updateStyle(_ newStyle: ClockStyle) {
|
|
|
|
// Update properties of the existing style object instead of replacing it
|
|
// This preserves the @Observable chain
|
|
style.use24Hour = newStyle.use24Hour
|
|
style.showSeconds = newStyle.showSeconds
|
|
style.showAmPm = newStyle.showAmPm
|
|
style.forceHorizontalMode = newStyle.forceHorizontalMode
|
|
style.digitColorHex = newStyle.digitColorHex
|
|
style.glowIntensity = newStyle.glowIntensity
|
|
style.clockOpacity = newStyle.clockOpacity
|
|
style.fontFamily = newStyle.fontFamily
|
|
style.fontWeight = newStyle.fontWeight
|
|
style.fontDesign = newStyle.fontDesign
|
|
style.showBattery = newStyle.showBattery
|
|
style.showDate = newStyle.showDate
|
|
style.overlayOpacity = newStyle.overlayOpacity
|
|
style.backgroundHex = newStyle.backgroundHex
|
|
style.keepAwake = newStyle.keepAwake
|
|
style.randomizeColor = newStyle.randomizeColor
|
|
style.selectedColorTheme = newStyle.selectedColorTheme
|
|
style.nightModeEnabled = newStyle.nightModeEnabled
|
|
style.autoNightMode = newStyle.autoNightMode
|
|
style.scheduledNightMode = newStyle.scheduledNightMode
|
|
style.nightModeStartTime = newStyle.nightModeStartTime
|
|
style.nightModeEndTime = newStyle.nightModeEndTime
|
|
style.ambientLightThreshold = newStyle.ambientLightThreshold
|
|
style.autoBrightness = newStyle.autoBrightness
|
|
style.digitAnimationStyle = newStyle.digitAnimationStyle
|
|
style.dateFormat = newStyle.dateFormat
|
|
style.respectFocusModes = newStyle.respectFocusModes
|
|
style.liveActivitiesEnabled = newStyle.liveActivitiesEnabled
|
|
|
|
|
|
saveStyle()
|
|
updateTimersIfNeeded()
|
|
updateWakeLockState()
|
|
updateBrightness() // Update brightness when style changes
|
|
}
|
|
|
|
func setKeepAwakeEnabled(_ enabled: Bool) {
|
|
style.keepAwake = enabled
|
|
saveStyle()
|
|
updateWakeLockState()
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
private func loadStyle() {
|
|
if let decoded = try? JSONDecoder().decode(ClockStyle.self, from: styleJSON) {
|
|
style = decoded
|
|
} else {
|
|
style = ClockStyle()
|
|
saveStyle()
|
|
}
|
|
}
|
|
|
|
private func observeStyleUpdates() {
|
|
styleObserver = NotificationCenter.default.addObserver(
|
|
forName: .clockStyleDidUpdate,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
self?.loadStyle()
|
|
self?.updateTimersIfNeeded()
|
|
self?.updateWakeLockState()
|
|
self?.updateBrightness()
|
|
}
|
|
}
|
|
|
|
func saveStyle() {
|
|
persistenceWorkItem?.cancel()
|
|
|
|
let work = DispatchWorkItem {
|
|
if let data = try? JSONEncoder().encode(self.style) {
|
|
self.styleJSON = data
|
|
}
|
|
}
|
|
persistenceWorkItem = work
|
|
|
|
DispatchQueue.main.asyncAfter(
|
|
deadline: .now() + AppConstants.PersistenceDelays.clockStyle,
|
|
execute: work
|
|
)
|
|
}
|
|
|
|
private func setupTimers() {
|
|
// Always need minute timer for color randomization
|
|
if minuteTimer == nil {
|
|
minuteTimer = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common)
|
|
minuteCancellable = minuteTimer?.autoconnect().sink { _ in
|
|
if self.style.randomizeColor {
|
|
self.style.digitColorHex = Color.randomBrightColorHex()
|
|
self.saveStyle()
|
|
self.updateBrightness() // Update brightness when color changes
|
|
}
|
|
|
|
// Check for night mode state changes (scheduled night mode)
|
|
// Force a UI update by updating currentTime slightly
|
|
self.currentTime = Date()
|
|
|
|
// Update brightness if night mode is active
|
|
self.updateBrightness()
|
|
}
|
|
}
|
|
|
|
// Only create second timer if seconds are shown
|
|
if style.showSeconds && secondTimer == nil {
|
|
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common)
|
|
secondCancellable = secondTimer?.autoconnect().sink { now in
|
|
self.currentTime = now
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stopTimers() {
|
|
secondCancellable?.cancel()
|
|
minuteCancellable?.cancel()
|
|
secondCancellable = nil
|
|
minuteCancellable = nil
|
|
secondTimer = nil
|
|
minuteTimer = nil
|
|
}
|
|
|
|
private func updateTimersIfNeeded() {
|
|
if style.showSeconds && secondTimer == nil {
|
|
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common)
|
|
secondCancellable = secondTimer?.autoconnect().sink { now in
|
|
self.currentTime = now
|
|
}
|
|
} else if !style.showSeconds && secondTimer != nil {
|
|
secondCancellable?.cancel()
|
|
secondCancellable = nil
|
|
secondTimer = nil
|
|
}
|
|
}
|
|
|
|
private func requestKeepAwakePromptIfNeeded() {
|
|
guard !style.keepAwake else { return }
|
|
NotificationCenter.default.post(name: .keepAwakePromptRequested, object: nil)
|
|
}
|
|
|
|
/// Update wake lock state based on current settings
|
|
private func updateWakeLockState() {
|
|
// Enable wake lock if in display mode and keep awake is enabled
|
|
if isDisplayMode && style.keepAwake {
|
|
wakeLockService.enableWakeLock()
|
|
} else {
|
|
wakeLockService.disableWakeLock()
|
|
}
|
|
}
|
|
|
|
/// Start ambient light monitoring
|
|
private func startAmbientLightMonitoring() {
|
|
ambientLightService.startMonitoring()
|
|
|
|
// Set up callback to respond to brightness changes
|
|
ambientLightService.onBrightnessChange = { [weak self] in
|
|
//Design.debugLog("[brightness] ClockViewModel: Received brightness change notification")
|
|
self?.updateBrightness()
|
|
}
|
|
}
|
|
|
|
/// Stop ambient light monitoring
|
|
private func stopAmbientLightMonitoring() {
|
|
ambientLightService.stopMonitoring()
|
|
}
|
|
|
|
/// Update brightness based on color theme and night mode settings
|
|
private func updateBrightness() {
|
|
if style.autoBrightness {
|
|
let targetBrightness = style.effectiveBrightness
|
|
let currentScreenBrightness = UIScreen.main.brightness
|
|
let isNightMode = style.isNightModeActive
|
|
|
|
// Design.debugLog("[brightness] Auto Brightness Debug:")
|
|
// Design.debugLog("[brightness] - Auto brightness enabled: \(style.autoBrightness)")
|
|
// Design.debugLog("[brightness] - Current screen brightness: \(String(format: "%.2f", currentScreenBrightness))")
|
|
// Design.debugLog("[brightness] - Target brightness: \(String(format: "%.2f", targetBrightness))")
|
|
// Design.debugLog("[brightness] - Night mode active: \(isNightMode)")
|
|
// Design.debugLog("[brightness] - Color theme: \(style.selectedColorTheme)")
|
|
// Design.debugLog("[brightness] - Ambient light threshold: \(String(format: "%.2f", style.ambientLightThreshold))")
|
|
|
|
ambientLightService.setBrightness(targetBrightness)
|
|
|
|
// Design.debugLog("[brightness] - Brightness set to: \(String(format: "%.2f", targetBrightness))")
|
|
// Design.debugLog("[brightness] - Actual screen brightness now: \(String(format: "%.2f", UIScreen.main.brightness))")
|
|
// Design.debugLog("[brightness] ---")
|
|
// } else {
|
|
// Design.debugLog("[brightness] Auto Brightness: DISABLED")
|
|
}
|
|
}
|
|
}
|