Files
Calendarr-IOS/CalendarrWidgets/CalendarrTimelineProvider.swift
Scarriffle 4e9ae83299 feat(widget): calendar filter config + fix group-view data leak
Bug fix: publishWidgetSnapshot() now guards against activeGroup != nil,
so group view events/colors never contaminate the widget cache.

Feature: widgets can now be configured via long-press → Edit Widget.
- WidgetCalendar struct + writeCalendars/readCalendars in WidgetData.swift
- calendarKey added to WidgetEvent (backward-compatible decoder)
- CalendarIntent.swift: CalendarAppEntity + CalendarEntityQuery + CalendarSelectionIntent
- CalendarrTimelineProvider migrated from TimelineProvider to AppIntentTimelineProvider
- All 13 StaticConfiguration widgets changed to AppIntentConfiguration
- publishWidgetSnapshot() builds + writes the calendar list for the intent

An empty selection (default) shows all calendars; selecting specific
calendars filters the widget events accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:41:43 +02:00

78 lines
3.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import WidgetKit
import AppIntents
struct CalendarrEntry: TimelineEntry {
let date: Date
let snapshot: WidgetSnapshot?
}
struct CalendarrTimelineProvider: AppIntentTimelineProvider {
typealias Entry = CalendarrEntry
typealias Intent = CalendarSelectionIntent
func placeholder(in context: Context) -> CalendarrEntry {
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
}
func snapshot(for configuration: CalendarSelectionIntent, in context: Context) async -> CalendarrEntry {
let raw = WidgetStore.read()
return CalendarrEntry(date: .now, snapshot: filtered(raw, by: configuration))
}
func timeline(for configuration: CalendarSelectionIntent, in context: Context) async -> Timeline<CalendarrEntry> {
let raw = WidgetStore.read()
let snap = filtered(raw, by: configuration)
let now = Date()
// One entry per hour for 24 h so the widget re-renders as time advances.
var entries: [CalendarrEntry] = []
for h in 0..<24 {
let date = Calendar.current.date(byAdding: .hour, value: h, to: now) ?? now
entries.append(CalendarrEntry(date: date, snapshot: snap))
}
let refreshAt = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
return Timeline(entries: entries, policy: .after(refreshAt))
}
// MARK: Filtering
private func filtered(_ snapshot: WidgetSnapshot?, by config: CalendarSelectionIntent) -> WidgetSnapshot? {
guard let snapshot else { return nil }
let ids = config.selectedCalendars.map { $0.id }
guard !ids.isEmpty else { return snapshot } // empty = show all
let keep = Set(ids)
return WidgetSnapshot(
writtenAt: snapshot.writtenAt,
events: snapshot.events.filter { keep.contains($0.calendarKey) },
todayColorHex: snapshot.todayColorHex,
textColorHex: snapshot.textColorHex,
backgroundColorHex: snapshot.backgroundColorHex,
lineColorHex: snapshot.lineColorHex,
primaryColorHex: snapshot.primaryColorHex,
accentColorHex: snapshot.accentColorHex,
language: snapshot.language
)
}
}
// MARK: Shared helpers used by all widget views
enum WidgetHelpers {
static func events(for day: Date, in snapshot: WidgetSnapshot) -> [WidgetEvent] {
let cal = Calendar.current
let dayStart = cal.startOfDay(for: day)
let dayEnd = cal.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart
return snapshot.events
.filter { $0.start < dayEnd && $0.end > dayStart }
.sorted { $0.start < $1.start }
}
static func upcoming(from now: Date, daysAhead: Int, in snapshot: WidgetSnapshot) -> [WidgetEvent] {
let cal = Calendar.current
let end = cal.date(byAdding: .day, value: daysAhead, to: cal.startOfDay(for: now)) ?? now
return snapshot.events
.filter { $0.end > now && $0.start < end }
.sorted { $0.start < $1.start }
}
}