213 lines
6.4 KiB
Swift
213 lines
6.4 KiB
Swift
import Foundation
|
|
import Combine
|
|
import AppKit
|
|
|
|
@MainActor
|
|
final class TimerStore: ObservableObject {
|
|
@Published var timers: [TimerItem] = [] {
|
|
didSet {
|
|
save()
|
|
}
|
|
}
|
|
|
|
private var ticker: AnyCancellable?
|
|
private let storageKey = "multiTimerApp.timers.v2"
|
|
|
|
init() {
|
|
load()
|
|
|
|
if timers.isEmpty {
|
|
timers = (1...10).map {
|
|
TimerItem(name: "Timer \($0)", durationSeconds: 10 * 60)
|
|
}
|
|
}
|
|
|
|
// Beim Start prüfen: Falls Timer vor App-Ende lief, endDate verwenden.
|
|
reconcileRunningTimers()
|
|
|
|
ticker = Timer
|
|
.publish(every: 0.5, on: .main, in: .common)
|
|
.autoconnect()
|
|
.sink { [weak self] _ in
|
|
Task { @MainActor in
|
|
self?.tick()
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
forName: NSWorkspace.didWakeNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
Task { @MainActor in
|
|
self?.tick()
|
|
}
|
|
}
|
|
}
|
|
|
|
func addTimer() {
|
|
guard timers.count < 15 else { return }
|
|
|
|
timers.append(
|
|
TimerItem(
|
|
name: "Timer \(timers.count + 1)",
|
|
durationSeconds: 10 * 60
|
|
)
|
|
)
|
|
}
|
|
|
|
func removeTimer(_ timer: TimerItem) {
|
|
AlarmWindowManager.shared.closeWindow(timerID: timer.id)
|
|
timers.removeAll { $0.id == timer.id }
|
|
}
|
|
|
|
func start(_ timer: TimerItem) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timer.id }) else { return }
|
|
|
|
if timers[index].remainingSeconds <= 0 {
|
|
timers[index].remainingSeconds = timers[index].durationSeconds
|
|
}
|
|
|
|
timers[index].endDate = Date().addingTimeInterval(TimeInterval(timers[index].remainingSeconds))
|
|
timers[index].isRunning = true
|
|
}
|
|
|
|
func pause(_ timer: TimerItem) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timer.id }) else { return }
|
|
|
|
if let endDate = timers[index].endDate {
|
|
timers[index].remainingSeconds = max(0, Int(ceil(endDate.timeIntervalSinceNow)))
|
|
}
|
|
|
|
timers[index].isRunning = false
|
|
timers[index].endDate = nil
|
|
}
|
|
|
|
func reset(_ timer: TimerItem) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timer.id }) else { return }
|
|
|
|
timers[index].isRunning = false
|
|
timers[index].endDate = nil
|
|
timers[index].remainingSeconds = timers[index].durationSeconds
|
|
|
|
AlarmWindowManager.shared.closeWindow(timerID: timer.id)
|
|
}
|
|
|
|
func repeatTimer(timerID: UUID) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timerID }) else { return }
|
|
|
|
timers[index].remainingSeconds = timers[index].durationSeconds
|
|
timers[index].endDate = Date().addingTimeInterval(TimeInterval(timers[index].durationSeconds))
|
|
timers[index].isRunning = true
|
|
}
|
|
|
|
func finishTimer(timerID: UUID) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timerID }) else { return }
|
|
|
|
timers[index].isRunning = false
|
|
timers[index].endDate = nil
|
|
timers[index].remainingSeconds = timers[index].durationSeconds
|
|
}
|
|
|
|
func durationChanged(for timerID: UUID, newDurationSeconds: Int) {
|
|
guard let index = timers.firstIndex(where: { $0.id == timerID }) else { return }
|
|
|
|
let safeSeconds = max(60, min(24 * 3600, newDurationSeconds))
|
|
timers[index].durationSeconds = safeSeconds
|
|
|
|
if !timers[index].isRunning {
|
|
timers[index].remainingSeconds = safeSeconds
|
|
timers[index].endDate = nil
|
|
}
|
|
}
|
|
|
|
private func tick() {
|
|
var changed = false
|
|
|
|
for index in timers.indices where timers[index].isRunning {
|
|
guard let endDate = timers[index].endDate else {
|
|
timers[index].endDate = Date().addingTimeInterval(TimeInterval(timers[index].remainingSeconds))
|
|
changed = true
|
|
continue
|
|
}
|
|
|
|
let secondsLeft = max(0, Int(ceil(endDate.timeIntervalSinceNow)))
|
|
|
|
if timers[index].remainingSeconds != secondsLeft {
|
|
timers[index].remainingSeconds = secondsLeft
|
|
changed = true
|
|
}
|
|
|
|
if secondsLeft <= 0 {
|
|
handleExpiredTimer(at: index)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
if changed {
|
|
objectWillChange.send()
|
|
save()
|
|
}
|
|
}
|
|
|
|
private func handleExpiredTimer(at index: Int) {
|
|
timers[index].remainingSeconds = 0
|
|
timers[index].isRunning = false
|
|
timers[index].endDate = nil
|
|
timers[index].completedCount += 1
|
|
|
|
AlarmManager.shared.playAlarm()
|
|
|
|
let timerID = timers[index].id
|
|
let timerName = timers[index].name
|
|
|
|
if timers[index].autoRepeat {
|
|
timers[index].remainingSeconds = timers[index].durationSeconds
|
|
timers[index].endDate = Date().addingTimeInterval(TimeInterval(timers[index].durationSeconds))
|
|
timers[index].isRunning = true
|
|
} else {
|
|
AlarmWindowManager.shared.showAlarm(
|
|
timerID: timerID,
|
|
timerName: timerName,
|
|
onRepeat: { [weak self] in
|
|
self?.repeatTimer(timerID: timerID)
|
|
},
|
|
onFinish: { [weak self] in
|
|
self?.finishTimer(timerID: timerID)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private func reconcileRunningTimers() {
|
|
for index in timers.indices where timers[index].isRunning {
|
|
if let endDate = timers[index].endDate {
|
|
timers[index].remainingSeconds = max(0, Int(ceil(endDate.timeIntervalSinceNow)))
|
|
} else {
|
|
timers[index].endDate = Date().addingTimeInterval(TimeInterval(timers[index].remainingSeconds))
|
|
}
|
|
|
|
if timers[index].remainingSeconds <= 0 {
|
|
timers[index].isRunning = false
|
|
timers[index].remainingSeconds = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
guard let data = try? JSONEncoder().encode(timers) else { return }
|
|
UserDefaults.standard.set(data, forKey: storageKey)
|
|
}
|
|
|
|
private func load() {
|
|
guard
|
|
let data = UserDefaults.standard.data(forKey: storageKey),
|
|
let decoded = try? JSONDecoder().decode([TimerItem].self, from: data)
|
|
else {
|
|
return
|
|
}
|
|
|
|
timers = decoded
|
|
}
|
|
}
|