Initial MultiTimer macOS app
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user