diff --git a/KisaniCal/ISSUES.md b/KisaniCal/ISSUES.md index ff3dc56..86722c1 100644 --- a/KisaniCal/ISSUES.md +++ b/KisaniCal/ISSUES.md @@ -550,3 +550,37 @@ to standard and removed entirely from the gallery. Files modified: `KisaniCalWidgets.swift`, `EventCountdownWidget.swift`, `WidgetViews.swift`, `WidgetData.swift`. No files deleted (shared code remains in EventCountdownWidget.swift). No app-side settings referenced these widgets. + +--- + +## KC-18 — Bars widget: progress bar never fills; date pickers felt broken + +**Status:** In Progress (implemented & build-verified, pending device build) +**Reported by:** User +**Area:** Widgets + +### Symptoms +The Event Countdown (Bars) widget showed all bars dim regardless of time left, +and the Event date / Counting from rows in the config appeared not to work. + +### Root causes +1. Auto (upcoming-queue) mode anchored progress to `Date()` on **every** refresh, + so elapsed time was always ~0 → empty bar forever. +2. Custom mode defaulted both `Event date` and `Counting from` to the same + instant (`Date.now` at config time) → zero-length span → progress 0. The raw + second-precision defaults also made the picker rows look broken. +3. The timeline only refreshed at midnight, so even a correct bar wouldn't move + during an "X hr left" countdown. + +### Fix +- Persistent per-event anchor (`kisani.widget.anchor.` in the App Group): + progress fills from when an event was first tracked and resets naturally when + the tracked event changes. An explicit "Counting from" before the event wins. +- `targetDate`/`startDate` are now optional — rows read "Choose" until set, and a + missing custom date falls back to a 2-day stub window. +- Timeline pre-renders an entry every 30 minutes (12 h horizon) so the bar and + time-left label keep moving. + +Files: `KisaniCalWidgets/EventCountdownWidget.swift`. +Note: existing widget instances re-read their config; dates must be re-picked +once (params changed to optional). diff --git a/KisaniCalWidgets/EventCountdownWidget.swift b/KisaniCalWidgets/EventCountdownWidget.swift index e3e52a9..eab5df7 100644 --- a/KisaniCalWidgets/EventCountdownWidget.swift +++ b/KisaniCalWidgets/EventCountdownWidget.swift @@ -42,11 +42,13 @@ struct EventConfigIntent: WidgetConfigurationIntent { @Parameter(title: "Or event name", default: "My Event") var eventName: String - @Parameter(title: "Event date", default: Date.now) - var targetDate: Date + // Optional so the rows read "Choose" until the user actually picks a date — + // a same-second default for both made the bar span zero and looked broken. + @Parameter(title: "Event date") + var targetDate: Date? - @Parameter(title: "Counting from", default: Date.now) - var startDate: Date + @Parameter(title: "Counting from") + var startDate: Date? // Show only the relevant fields depending on the toggle — like a clean, // single-purpose config screen instead of every field at once. @@ -204,10 +206,18 @@ struct EventProvider: AppIntentTimelineProvider { } func timeline(for configuration: EventConfigIntent, in context: Context) async -> Timeline { - let entry = entry(for: configuration) - // Recompute at the next midnight so the grid advances one dot per day. - let nextMidnight = Calendar.current.startOfDay(for: Date().addingTimeInterval(86400)) - return Timeline(entries: [entry], policy: .after(nextMidnight)) + let base = entry(for: configuration) + // Live progress: pre-render an entry every 30 minutes for the next 12 hours + // so the bar and time-left label keep moving between refreshes. + let entries: [EventEntry] = (0..<24).map { step in + EventEntry(date: Date().addingTimeInterval(Double(step) * 1800), + eventName: base.eventName, + start: base.start, + target: base.target, + unitMode: base.unitMode) + } + let next = Date().addingTimeInterval(24 * 1800) + return Timeline(entries: entries, policy: .after(next)) } /// Upcoming events (incomplete tasks with a future due date), nearest first. @@ -221,6 +231,7 @@ struct EventProvider: AppIntentTimelineProvider { private func entry(for config: EventConfigIntent) -> EventEntry { var name: String var target: Date + var anchorKey: String if config.upcomingQueue { // Auto-track the Nth nearest upcoming event. @@ -229,27 +240,48 @@ struct EventProvider: AppIntentTimelineProvider { if idx < upcoming.count, let due = upcoming[idx].dueDate { name = upcoming[idx].title target = due + anchorKey = upcoming[idx].id } else { name = "No upcoming event" target = Date().addingTimeInterval(86400) + anchorKey = "none" } } else if let ev = config.event, let due = ev.dueDate { // A specifically picked event supplies its own name + date. name = ev.title target = due + anchorKey = ev.id } else { - // Fully custom event. + // Fully custom event. No date picked yet → count to tomorrow as a stub. name = config.eventName.isEmpty ? "My Event" : config.eventName target = config.targetDate + ?? Calendar.current.startOfDay(for: Date()).addingTimeInterval(2 * 86400) + anchorKey = "custom.\(name).\(Int(target.timeIntervalSince1970))" } - // Counting anchor: custom mode honors the picker; auto mode counts from - // "now" so the grid fills toward the event. Never start after the target. - let anchor = config.upcomingQueue ? Date() : config.startDate - let start = min(anchor, target) + // Counting anchor: an explicit "Counting from" wins when it's before the + // event; otherwise remember when this event was first tracked so the bar + // fills steadily instead of resetting to "now" on every refresh. + let start: Date + if let s = config.startDate, s < target { + start = s + } else { + start = Self.storedAnchor(key: anchorKey, target: target) + } let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey) return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode) } + + /// First-seen date for a tracked event, persisted in the App Group so progress + /// is stable across refreshes (and resets naturally when the event changes). + private static func storedAnchor(key: String, target: Date) -> Date { + let ud = UserDefaults.kisani + let k = "kisani.widget.anchor.\(key)" + if let saved = ud.object(forKey: k) as? Date, saved < target { return saved } + let now = Date() + ud.set(now, forKey: k) + return now + } } // MARK: - Event Countdown (Bars)