Event widget: upcoming-queue auto-select + cleaner config & preview

- Add "Upcoming queue" toggle to the Event Countdown widget. When on, it
  auto-tracks the Nth nearest upcoming event (Nearest / 2nd … 5th, new
  AutoSelectRank enum). When off, track a picked or fully custom event.
- parameterSummary now shows only the relevant fields per mode (toggle +
  auto-select, or event/name/date), so the date input only appears for
  custom events.
- Replace the dense 380-day placeholder with a ~90-day window so the
  gallery preview shows a legible dot grid.

(The interactive task-checkbox widget already exists as "My Tasks".)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-07 12:43:02 +03:00
parent 72f4566d91
commit 7b98be1d8d
4 changed files with 109 additions and 33 deletions

View File

@@ -10,12 +10,32 @@ import AppIntents
// persisted per-instance by WidgetKit, the widget is fully self-contained and does
// not depend on App Group data sharing.
// Which upcoming event to auto-track when the queue is on.
enum AutoSelectRank: Int, AppEnum {
case first = 1, second, third, fourth, fifth
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Auto Select"
static var caseDisplayRepresentations: [AutoSelectRank: DisplayRepresentation] = [
.first: "Nearest to finish",
.second: "2nd nearest to finish",
.third: "3rd nearest to finish",
.fourth: "4th nearest to finish",
.fifth: "5th nearest to finish",
]
}
struct EventConfigIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Event Countdown"
static var description = IntentDescription("Count down to any upcoming event with a dot grid.")
static var description = IntentDescription("Count down to an event with a dot grid.")
// When on, the widget auto-tracks one of your upcoming events. When off, you
// track a specific event you pick, or a fully custom name + date.
@Parameter(title: "Upcoming queue", default: true)
var upcomingQueue: Bool
@Parameter(title: "Auto select", default: .first)
var autoSelect: AutoSelectRank
// Pick one of your existing events (tasks with a due date). When set, it
// overrides the manual name/date fields below.
@Parameter(title: "Track event")
var event: EventEntity?
@@ -27,6 +47,25 @@ struct EventConfigIntent: WidgetConfigurationIntent {
@Parameter(title: "Counting from", default: Date.now)
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.
static var parameterSummary: some ParameterSummary {
When(\.$upcomingQueue, .equalTo, true) {
Summary("Track upcoming event") {
\.$upcomingQueue
\.$autoSelect
}
} otherwise: {
Summary("Track a custom event") {
\.$upcomingQueue
\.$event
\.$eventName
\.$targetDate
\.$startDate
}
}
}
}
// MARK: - Event picker (reads your tasks from the App Group)
@@ -141,10 +180,11 @@ struct EventEntry: TimelineEntry {
struct EventProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> EventEntry {
// ~90-day window, ~60% elapsed a clear, legible dot grid in the gallery.
EventEntry(date: .now,
eventName: "Mum's Birthday",
start: Date().addingTimeInterval(-86400 * 320),
target: Date().addingTimeInterval(86400 * 60))
eventName: "IELTS",
start: Date().addingTimeInterval(-86400 * 54),
target: Date().addingTimeInterval(86400 * 36))
}
func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry {
@@ -158,19 +198,43 @@ struct EventProvider: AppIntentTimelineProvider {
return Timeline(entries: [entry], policy: .after(nextMidnight))
}
/// Upcoming events (incomplete tasks with a future due date), nearest first.
private func upcomingEvents() -> [EventEntity] {
loadWidgetTasks()
.filter { !$0.isComplete && ($0.dueDate ?? .distantPast) > Date() }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate) }
}
private func entry(for config: EventConfigIntent) -> EventEntry {
let name: String
let target: Date
if let ev = config.event, let due = ev.dueDate {
// A picked event supplies its own name + date.
var name: String
var target: Date
if config.upcomingQueue {
// Auto-track the Nth nearest upcoming event.
let upcoming = upcomingEvents()
let idx = max(0, config.autoSelect.rawValue - 1)
if idx < upcoming.count, let due = upcoming[idx].dueDate {
name = upcoming[idx].title
target = due
} else {
name = "No upcoming event"
target = Date().addingTimeInterval(86400)
}
} else if let ev = config.event, let due = ev.dueDate {
// A specifically picked event supplies its own name + date.
name = ev.title
target = due
} else {
// Fully custom event.
name = config.eventName.isEmpty ? "My Event" : config.eventName
target = config.targetDate
}
// Never let the start sit after the target.
let start = min(config.startDate, target)
// 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)
let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey)
return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode)
}