feat: My Tasks widget, event countdown redesign, completed section
- My Tasks widget: today's tasks with interactive check-off (ToggleTaskIntent) and a "+" that opens the app's add-task sheet; writes preserve all task fields and sync via the App Group. - Event Countdown: AZURE/AWS-style header (accent name + countdown label + percent); tap the label to cycle days → weeks → time remaining. - Today: collapsible "Completed" archive section below Upcoming. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
// Minimal task model shared between widget and main app via App Group
|
||||
struct WidgetTask: Codable, Identifiable {
|
||||
@@ -84,6 +85,59 @@ func nextDueTask(from tasks: [WidgetTask]) -> WidgetTask? {
|
||||
.first
|
||||
}
|
||||
|
||||
private func activeTasksKey() -> String {
|
||||
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
|
||||
return "kisani.tasks.v2.\(uid)"
|
||||
}
|
||||
|
||||
// Tasks due today (incomplete first, then by due time) for the interactive list widget.
|
||||
func todayWidgetTasks() -> [WidgetTask] {
|
||||
let cal = Calendar.current
|
||||
let start = cal.startOfDay(for: Date())
|
||||
let end = cal.date(byAdding: .day, value: 1, to: start)!
|
||||
return loadWidgetTasks()
|
||||
.filter { t in
|
||||
guard let due = t.dueDate else { return false }
|
||||
return due >= start && due < end
|
||||
}
|
||||
.sorted {
|
||||
if $0.isComplete != $1.isComplete { return !$0.isComplete }
|
||||
return ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle a task's completion from the widget. Uses JSONSerialization so ALL task
|
||||
// fields are preserved on write (the widget only models a subset of TaskItem).
|
||||
func toggleWidgetTaskComplete(id: String) {
|
||||
let key = activeTasksKey()
|
||||
guard let data = UserDefaults.kisani.data(forKey: key),
|
||||
var arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]]
|
||||
else { return }
|
||||
for i in arr.indices where (arr[i]["id"] as? String) == id {
|
||||
let done = (arr[i]["isComplete"] as? Bool) ?? false
|
||||
arr[i]["isComplete"] = !done
|
||||
if !done {
|
||||
// TaskItem uses the default (deferredToDate) Date strategy → seconds since reference date.
|
||||
arr[i]["completedAt"] = Date().timeIntervalSinceReferenceDate
|
||||
} else {
|
||||
arr[i].removeValue(forKey: "completedAt")
|
||||
}
|
||||
}
|
||||
if let out = try? JSONSerialization.data(withJSONObject: arr) {
|
||||
UserDefaults.kisani.set(out, forKey: key)
|
||||
CloudSyncManager_widgetPush(out, key)
|
||||
}
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
// The widget extension can't see CloudSyncManager; mirror to iCloud KV store directly.
|
||||
private func CloudSyncManager_widgetPush(_ data: Data, _ key: String) {
|
||||
guard data.count < 900_000 else { return }
|
||||
let kv = NSUbiquitousKeyValueStore.default
|
||||
kv.set(data, forKey: key)
|
||||
kv.synchronize()
|
||||
}
|
||||
|
||||
// Today's task completion: (done, total) for tasks due today.
|
||||
func todayTaskCompletion() -> (done: Int, total: Int) {
|
||||
let cal = Calendar.current
|
||||
|
||||
Reference in New Issue
Block a user