158 lines
5.0 KiB
Swift
158 lines
5.0 KiB
Swift
import Foundation
|
|
import Observation
|
|
import SwiftData
|
|
#if os(iOS)
|
|
import ActivityKit
|
|
#endif
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class TaskViewModel {
|
|
private(set) var tasks: [TaskItem] = []
|
|
private(set) var activeTimerStartDates: [UUID: Date] = [:]
|
|
private(set) var elapsedSeconds: [UUID: Int] = [:]
|
|
|
|
private var context: ModelContext?
|
|
private var timerTasks: [UUID: Task<Void, Never>] = [:]
|
|
#if os(iOS)
|
|
private var liveActivities: [UUID: Activity<TimerActivityAttributes>] = [:]
|
|
#endif
|
|
|
|
func setup(context: ModelContext) {
|
|
self.context = context
|
|
fetchTasks()
|
|
}
|
|
|
|
func fetchTasks() {
|
|
guard let context else { return }
|
|
let descriptor = FetchDescriptor<TaskItem>(sortBy: [SortDescriptor(\.name)])
|
|
tasks = (try? context.fetch(descriptor)) ?? []
|
|
}
|
|
|
|
func addTask(name: String, icon: String, type: TaskType, category: Category? = nil) {
|
|
guard let context else { return }
|
|
let item = TaskItem(name: name, icon: icon, type: type, category: category)
|
|
context.insert(item)
|
|
try? context.save()
|
|
fetchTasks()
|
|
}
|
|
|
|
func deleteTask(_ task: TaskItem) {
|
|
stopTimer(for: task)
|
|
guard let context else { return }
|
|
context.delete(task)
|
|
try? context.save()
|
|
fetchTasks()
|
|
}
|
|
|
|
func addCount(to task: TaskItem) {
|
|
guard let context else { return }
|
|
let log = TaskLog(date: .now, duration: 0, count: 1)
|
|
log.task = task
|
|
context.insert(log)
|
|
try? context.save()
|
|
}
|
|
|
|
func toggleTimer(for task: TaskItem) {
|
|
if activeTimerStartDates[task.id] != nil {
|
|
stopTimer(for: task)
|
|
} else {
|
|
startTimer(for: task)
|
|
}
|
|
}
|
|
|
|
func isTimerRunning(for taskID: UUID) -> Bool {
|
|
activeTimerStartDates[taskID] != nil
|
|
}
|
|
|
|
func formattedElapsed(for taskID: UUID) -> String {
|
|
let s = elapsedSeconds[taskID] ?? 0
|
|
let h = s / 3600
|
|
let m = (s % 3600) / 60
|
|
let sec = s % 60
|
|
return h > 0
|
|
? String(format: "%d:%02d:%02d", h, m, sec)
|
|
: String(format: "%02d:%02d", m, sec)
|
|
}
|
|
|
|
func todayCount(for task: TaskItem) -> Int {
|
|
task.logs
|
|
.filter { Calendar.current.isDateInToday($0.date) }
|
|
.reduce(0) { $0 + $1.count }
|
|
}
|
|
|
|
// MARK: - Timer
|
|
|
|
private func startTimer(for task: TaskItem) {
|
|
let startDate = Date.now
|
|
activeTimerStartDates[task.id] = startDate
|
|
elapsedSeconds[task.id] = 0
|
|
startLiveActivity(for: task, startDate: startDate)
|
|
|
|
let taskID = task.id
|
|
timerTasks[taskID] = Task { [weak self] in
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(for: .seconds(1))
|
|
guard !Task.isCancelled, let self else { break }
|
|
let elapsed = Int(Date.now.timeIntervalSince(startDate))
|
|
self.elapsedSeconds[taskID] = elapsed
|
|
await self.updateLiveActivity(taskID: taskID, elapsed: elapsed, startDate: startDate)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stopTimer(for task: TaskItem) {
|
|
guard let startDate = activeTimerStartDates[task.id] else { return }
|
|
|
|
timerTasks[task.id]?.cancel()
|
|
timerTasks.removeValue(forKey: task.id)
|
|
|
|
let duration = Date.now.timeIntervalSince(startDate)
|
|
activeTimerStartDates.removeValue(forKey: task.id)
|
|
elapsedSeconds.removeValue(forKey: task.id)
|
|
|
|
endLiveActivity(for: task)
|
|
|
|
guard let context else { return }
|
|
let log = TaskLog(date: .now, duration: duration, count: 0)
|
|
log.task = task
|
|
context.insert(log)
|
|
try? context.save()
|
|
}
|
|
|
|
// MARK: - ActivityKit
|
|
|
|
private func startLiveActivity(for task: TaskItem, startDate: Date) {
|
|
#if os(iOS)
|
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
let attrs = TimerActivityAttributes(taskName: task.name, taskIcon: task.icon)
|
|
let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: 0)
|
|
let content = ActivityContent(state: state, staleDate: nil)
|
|
do {
|
|
let activity = try Activity.request(attributes: attrs, content: content)
|
|
liveActivities[task.id] = activity
|
|
} catch {
|
|
// Live Activity unavailable (simulator, denied, or OS < 16.2)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private func updateLiveActivity(taskID: UUID, elapsed: Int, startDate: Date) async {
|
|
#if os(iOS)
|
|
guard let activity = liveActivities[taskID] else { return }
|
|
let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: elapsed)
|
|
await activity.update(ActivityContent(state: state, staleDate: nil))
|
|
#endif
|
|
}
|
|
|
|
private func endLiveActivity(for task: TaskItem) {
|
|
#if os(iOS)
|
|
guard let activity = liveActivities[task.id] else { return }
|
|
liveActivities.removeValue(forKey: task.id)
|
|
Task {
|
|
await activity.end(nil, dismissalPolicy: .immediate)
|
|
}
|
|
#endif
|
|
}
|
|
}
|