mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working Timer, need to enable fulscreen mode
This commit is contained in:
parent
628d76b41c
commit
19b1977fa8
13 changed files with 937 additions and 7 deletions
320
DynamicIsland/managers/TimerManager.swift
Normal file
320
DynamicIsland/managers/TimerManager.swift
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
//
|
||||
// TimerManager.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Timer management for the Dynamic Island
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import AppKit
|
||||
|
||||
class TimerManager: ObservableObject {
|
||||
// MARK: - Properties
|
||||
static let shared = TimerManager()
|
||||
|
||||
@Published var isTimerActive: Bool = false
|
||||
@Published var timerName: String = "Timer"
|
||||
@Published var totalDuration: TimeInterval = 0
|
||||
@Published var remainingTime: TimeInterval = 0
|
||||
@Published var elapsedTime: TimeInterval = 0
|
||||
@Published var isPaused: Bool = false
|
||||
@Published var isFinished: Bool = false
|
||||
@Published var isOvertime: Bool = false // Timer has gone past 0 and is counting negative
|
||||
@Published var lastUpdated: Date = .distantPast
|
||||
|
||||
// Timer progress (0.0 to 1.0, or >1.0 for overtime)
|
||||
var progress: Double {
|
||||
guard totalDuration > 0 else { return 0.0 }
|
||||
if isOvertime {
|
||||
// For overtime, progress goes beyond 1.0
|
||||
return 1.0 + min(1.0, abs(remainingTime) / totalDuration)
|
||||
} else {
|
||||
return min(1.0, max(0.0, elapsedTime / totalDuration))
|
||||
}
|
||||
}
|
||||
|
||||
// Color based on progress: green -> yellow -> red -> flashing red for overtime
|
||||
var timerColor: Color {
|
||||
if isOvertime {
|
||||
// Flashing red for overtime
|
||||
return .red
|
||||
}
|
||||
|
||||
let p = progress
|
||||
if p < 0.6 {
|
||||
// Green phase (0-60%)
|
||||
return .green
|
||||
} else if p < 0.9 {
|
||||
// Yellow phase (60-90%)
|
||||
let yellowProgress = (p - 0.6) / 0.3
|
||||
return Color(
|
||||
red: 1.0 - (1.0 - yellowProgress) * 0.5, // Gradually more red
|
||||
green: 1.0,
|
||||
blue: 0.0
|
||||
)
|
||||
} else {
|
||||
// Red phase (90-100%)
|
||||
return .red
|
||||
}
|
||||
}
|
||||
|
||||
// Computed properties for UI
|
||||
var isRunning: Bool {
|
||||
return isTimerActive && !isPaused
|
||||
}
|
||||
|
||||
var currentColor: Color {
|
||||
return timerColor
|
||||
}
|
||||
|
||||
var formattedTimeRemaining: String {
|
||||
return formattedRemainingTime()
|
||||
}
|
||||
|
||||
var statusText: String {
|
||||
if isOvertime {
|
||||
return "Overtime"
|
||||
} else if isPaused {
|
||||
return "Paused"
|
||||
} else if isTimerActive {
|
||||
return "Running"
|
||||
} else {
|
||||
return "Ready"
|
||||
}
|
||||
}
|
||||
|
||||
// NSColor version for compatibility
|
||||
var timerNSColor: NSColor {
|
||||
if isOvertime {
|
||||
return NSColor.systemRed
|
||||
}
|
||||
|
||||
let p = progress
|
||||
if p < 0.6 {
|
||||
return NSColor.systemGreen
|
||||
} else if p < 0.9 {
|
||||
let yellowProgress = (p - 0.6) / 0.3
|
||||
return NSColor(
|
||||
red: 1.0 - (1.0 - yellowProgress) * 0.5,
|
||||
green: 1.0,
|
||||
blue: 0.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
} else {
|
||||
return NSColor.systemRed
|
||||
}
|
||||
}
|
||||
|
||||
private var timerInstance: Timer?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var soundPlayer: AVAudioPlayer?
|
||||
|
||||
// MARK: - Initialization
|
||||
private init() {
|
||||
// Simple initialization
|
||||
}
|
||||
|
||||
deinit {
|
||||
timerInstance?.invalidate()
|
||||
soundPlayer?.stop()
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Timer Methods
|
||||
func startTimer(duration: TimeInterval, name: String = "Timer") {
|
||||
// Stop any existing timer
|
||||
timerInstance?.invalidate()
|
||||
|
||||
// Start new timer
|
||||
isTimerActive = true
|
||||
isFinished = false
|
||||
isOvertime = false
|
||||
timerName = name
|
||||
totalDuration = duration
|
||||
remainingTime = duration
|
||||
elapsedTime = 0
|
||||
isPaused = false
|
||||
lastUpdated = Date()
|
||||
|
||||
triggerTimerSneakPeek()
|
||||
|
||||
// Start countdown timer
|
||||
timerInstance = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
if !self.isPaused {
|
||||
if self.remainingTime > 0 {
|
||||
// Normal countdown
|
||||
self.remainingTime -= 1
|
||||
self.elapsedTime = self.totalDuration - self.remainingTime
|
||||
self.lastUpdated = Date()
|
||||
} else if self.remainingTime == 0 {
|
||||
// Timer just finished - play sound and start overtime
|
||||
self.isFinished = true
|
||||
self.isOvertime = true
|
||||
self.playTimerSound()
|
||||
self.remainingTime = -1
|
||||
self.lastUpdated = Date()
|
||||
} else {
|
||||
// Overtime - count negative
|
||||
self.remainingTime -= 1
|
||||
self.lastUpdated = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startDemoTimer(duration: TimeInterval) {
|
||||
startTimer(duration: duration, name: "Demo Timer")
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timerInstance?.invalidate()
|
||||
timerInstance = nil
|
||||
soundPlayer?.stop()
|
||||
|
||||
// Smooth close animation for live activity
|
||||
if isTimerActive {
|
||||
scheduleSmoothClose()
|
||||
}
|
||||
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
func forceStopTimer() {
|
||||
// Immediate stop for user action (stop button)
|
||||
timerInstance?.invalidate()
|
||||
timerInstance = nil
|
||||
soundPlayer?.stop()
|
||||
isTimerActive = false
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
func pauseTimer() {
|
||||
guard isTimerActive && !isPaused else { return }
|
||||
isPaused = true
|
||||
timerInstance?.invalidate()
|
||||
timerInstance = nil
|
||||
}
|
||||
|
||||
func resumeTimer() {
|
||||
guard isTimerActive && isPaused else { return }
|
||||
isPaused = false
|
||||
lastUpdated = Date()
|
||||
|
||||
// Resume countdown timer with same logic as start timer
|
||||
timerInstance = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
if !self.isPaused {
|
||||
if self.remainingTime > 0 {
|
||||
// Normal countdown
|
||||
self.remainingTime -= 1
|
||||
self.elapsedTime = self.totalDuration - self.remainingTime
|
||||
self.lastUpdated = Date()
|
||||
} else if self.remainingTime == 0 {
|
||||
// Timer just finished - play sound and start overtime
|
||||
self.isFinished = true
|
||||
self.isOvertime = true
|
||||
self.playTimerSound()
|
||||
self.remainingTime = -1
|
||||
self.lastUpdated = Date()
|
||||
} else {
|
||||
// Overtime - count negative
|
||||
self.remainingTime -= 1
|
||||
self.lastUpdated = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetTimer() {
|
||||
isTimerActive = false
|
||||
timerName = "Timer"
|
||||
totalDuration = 0
|
||||
remainingTime = 0
|
||||
elapsedTime = 0
|
||||
isPaused = false
|
||||
isFinished = false
|
||||
isOvertime = false
|
||||
}
|
||||
|
||||
private func scheduleSmoothClose() {
|
||||
// Wait 3 seconds then smoothly close the live activity
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
withAnimation(.easeInOut(duration: 1.0)) {
|
||||
self.isTimerActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playTimerSound() {
|
||||
// Play default macOS timer sound
|
||||
guard let soundURL = Bundle.main.url(forResource: "dynamic", withExtension: "m4a") else {
|
||||
// Fallback to system sound
|
||||
NSSound.beep()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
soundPlayer = try AVAudioPlayer(contentsOf: soundURL)
|
||||
soundPlayer?.numberOfLoops = -1 // Loop indefinitely
|
||||
soundPlayer?.play()
|
||||
} catch {
|
||||
// Fallback to system sound
|
||||
NSSound.beep()
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerTimerSneakPeek() {
|
||||
let coordinator = DynamicIslandViewCoordinator.shared
|
||||
|
||||
DispatchQueue.main.async {
|
||||
coordinator.sneakPeek.show = true
|
||||
coordinator.sneakPeek.type = .timer
|
||||
coordinator.sneakPeek.value = CGFloat(self.progress) // Use timer progress (0.0 to 1.0)
|
||||
|
||||
// Hide sneak peek after delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
coordinator.sneakPeek.show = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Formatted Time Strings
|
||||
func formattedRemainingTime() -> String {
|
||||
if isOvertime && remainingTime < 0 {
|
||||
return "-" + timeString(from: abs(remainingTime))
|
||||
} else {
|
||||
return timeString(from: remainingTime)
|
||||
}
|
||||
}
|
||||
|
||||
func formattedElapsedTime() -> String {
|
||||
return timeString(from: elapsedTime)
|
||||
}
|
||||
|
||||
func formattedTotalDuration() -> String {
|
||||
return timeString(from: totalDuration)
|
||||
}
|
||||
|
||||
private func timeString(from seconds: TimeInterval) -> String {
|
||||
let totalMinutes = Int(seconds) / 60
|
||||
let remainingSeconds = Int(seconds) % 60
|
||||
let hours = totalMinutes / 60
|
||||
let minutes = totalMinutes % 60
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%d:%02d:%02d", hours, minutes, remainingSeconds)
|
||||
} else {
|
||||
return String(format: "%d:%02d", minutes, remainingSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue