Bars widget: app-driven "Track Countdown" with zero config (KC-20)

- Task long-press menu gains Track/Stop Tracking Countdown, storing the
  tracked event id + tracking date in the App Group (CountdownTracker)
  and reloading widget timelines.
- The widget reads the tracked event directly and bypasses its config:
  Mode A (one-off) = tracking date -> event date; Mode B (recurring) =
  previous occurrence -> next occurrence via the task's recurrence rule.
- WidgetTask decodes isRecurring/recurrenceLabel from the shared JSON.
- With nothing tracked, the configured modes remain the fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-11 23:05:33 +03:00
parent a7639f6454
commit b6eaba78ba
4 changed files with 122 additions and 3 deletions

View File

@@ -606,3 +606,41 @@ All widgets should match the brand orange theme of the logo exactly.
- Lock-screen ring tints → brand accent (renders in color on StandBy/home).
Files: `WidgetViews.swift`, `TaskLiveActivity.swift`.
---
## KC-20 — "Track Countdown": zero-config bars widget driven from the app
**Status:** In Progress (implemented & build-verified, pending device build)
**Reported by:** User
**Area:** Widgets / Tasks
### Description
The bars widget should not need manual event entry: track any KisaniCal event
from the app and the widget displays it automatically (title, date, time left,
progress bars).
### Fix
- **App:** new `CountdownTracker` + a "Track Countdown" / "Stop Tracking
Countdown" item in the shared task long-press menu. Stores
`kisani.widget.trackedEvent` (+ `trackedSince`) in the App Group and reloads
widget timelines.
- **Widget:** the tracked event bypasses the widget configuration entirely.
- **Mode A** (one-off): progress = elapsed/(event tracking date).
- **Mode B** (recurring birthdays/anniversaries): progress across the
occurrence span containing now (previous → next occurrence), derived from
the task's recurrence rule (`recurrenceSpan`).
- `WidgetTask` now decodes `isRecurring`/`recurrenceLabel` from the shared JSON.
- No tracked event → existing configuration (upcoming queue / picked / custom)
remains the fallback, exactly as before.
- Bar rendering already maps progress → filled (accent) vs remaining (dimmed)
bars; the screenshot showing static dim bars was the pre-KC-18 build.
Files: `TaskContextMenu.swift`, `WidgetData.swift`, `EventCountdownWidget.swift`.
### How to test (device)
1. Long-press any task with a due date → "Track Countdown".
2. The bars widget switches to that event without touching its config.
3. A yearly birthday shows Mode B: bar spans last year's → this year's date.
4. Long-press the task again → "Stop Tracking Countdown" → widget falls back
to its configured mode.

View File

@@ -1,4 +1,27 @@
import SwiftUI
import WidgetKit
/// Tracks one event for the Event Countdown (Bars) widget via App Group storage.
/// The widget reads these keys directly no manual widget configuration needed.
enum CountdownTracker {
static let idKey = "kisani.widget.trackedEvent"
static let sinceKey = "kisani.widget.trackedSince"
static func isTracked(_ task: TaskItem) -> Bool {
UserDefaults.kisani.string(forKey: idKey) == task.id.uuidString
}
static func toggle(_ task: TaskItem) {
if isTracked(task) {
UserDefaults.kisani.removeObject(forKey: idKey)
UserDefaults.kisani.removeObject(forKey: sinceKey)
} else {
UserDefaults.kisani.set(task.id.uuidString, forKey: idKey)
UserDefaults.kisani.set(Date(), forKey: sinceKey) // Mode A anchor
}
WidgetCenter.shared.reloadAllTimelines()
}
}
/// The unified long-press menu for a task, shared across Today, Matrix, and Calendar.
/// Pass only the closures a given screen needs; the rest default to no-ops.
@@ -67,6 +90,11 @@ struct TaskMenuItems: View {
Label("Add to Live Activity", systemImage: "pin.circle")
}
Button { CountdownTracker.toggle(task) } label: {
Label(CountdownTracker.isTracked(task) ? "Stop Tracking Countdown" : "Track Countdown",
systemImage: "timer")
}
if let onEdit {
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
}