Bars widget: fix empty progress bar + dead-feeling date pickers (KC-18)

- Persist a per-event "first tracked" anchor in the App Group instead of
  resetting the progress start to now on every refresh (auto mode), so
  the bar actually fills toward the event. Explicit "Counting from" wins
  when it's before the event date.
- Make Event date / Counting from optional ("Choose") — the same-second
  defaults produced a zero-length span (always 0%) and looked broken.
- Pre-render timeline entries every 30 min (12h horizon) so the bar and
  time-left label move between refreshes instead of only at midnight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-11 18:16:30 +03:00
parent 49b22b5f2f
commit 0606d9c3aa
2 changed files with 79 additions and 13 deletions

View File

@@ -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<EventEntry> {
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)