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:
@@ -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 (0…1).
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user