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:
Robin Kutesa
2026-06-03 13:31:04 +03:00
parent 8b094691f5
commit 646cdf6a9b
10 changed files with 386 additions and 44 deletions

View File

@@ -67,11 +67,25 @@ struct EventQuery: EntityQuery {
// MARK: - Entry
// Tapping the countdown label cycles this unit (shared by all event widgets).
let countdownUnitKey = "kisani.widget.countdownUnit" // 0 = days, 1 = weeks, 2 = time
struct CycleCountdownUnitIntent: AppIntent {
static var title: LocalizedStringResource = "Change Countdown Unit"
func perform() async throws -> some IntentResult {
let cur = UserDefaults.kisani.integer(forKey: countdownUnitKey)
UserDefaults.kisani.set((cur + 1) % 3, forKey: countdownUnitKey)
return .result()
}
}
struct EventEntry: TimelineEntry {
let date: Date
let eventName: String
let start: Date
let target: Date
var unitMode: Int = 0 // 0 days, 1 weeks, 2 time
/// Fraction of the countdown window that has elapsed (01).
var progress: Double {
@@ -97,6 +111,30 @@ struct EventEntry: TimelineEntry {
to: cal.startOfDay(for: target)).day ?? 0
return max(0, d)
}
/// Countdown from the current date. Tapping the label cycles the unit:
/// days weeks time remaining.
var timeLeftLabel: String {
if target <= date { return "Today" }
let cal = Calendar.current
switch unitMode {
case 1: // weeks
let days = cal.dateComponents([.day], from: cal.startOfDay(for: date),
to: cal.startOfDay(for: target)).day ?? 0
if days < 7 { return "< 1 week left" }
let weeks = days / 7
return "\(weeks) week\(weeks == 1 ? "" : "s") left"
case 2: // time remaining (detailed)
let c = cal.dateComponents([.day, .hour], from: date, to: target)
let days = c.day ?? 0, hours = c.hour ?? 0
if days == 0 { return hours <= 0 ? "Due now" : "\(hours) hr left" }
return hours > 0 ? "\(days)d, \(hours) hr left" : "\(days) day\(days == 1 ? "" : "s") left"
default: // days
let days = cal.dateComponents([.day], from: cal.startOfDay(for: date),
to: cal.startOfDay(for: target)).day ?? 0
return "\(days) day\(days == 1 ? "" : "s") left"
}
}
}
// MARK: - Provider
@@ -133,7 +171,8 @@ struct EventProvider: AppIntentTimelineProvider {
}
// Never let the start sit after the target.
let start = min(config.startDate, target)
return EventEntry(date: .now, eventName: name, start: start, target: target)
let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey)
return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode)
}
}