Files
Calendarr-IOS/CalendarrWidgets/CalendarrTimelineProvider.swift
Scarriffle d1da8de06a Read and write the widget snapshot through CalendarrCore
The snapshot format was an internal detail shared between the app and its own
widget extension. A second Mac app needs the same data, and the way to give it
that is a documented contract rather than a format it reverse-engineers and then
drifts from. Both targets now link CalendarrCore and go through SnapshotStore.

Shared/WidgetData.swift becomes a thin facade. The typealiases and the flat
colour accessors exist so the ~70 existing call sites across the app and the
widget views compile unchanged; they are a migration convenience, not a design.

The snapshot now carries what a reader outside this app actually needs:

coverageStart / coverageEnd, because the published window is ~7 days back and
~42 ahead. Outside it the snapshot holds no information, which is not the same
as holding no events — and only the writer knows where that edge is. Without it
a consumer renders a convincingly empty March and is simply wrong.

isLoggedIn plus a session record, so a reader can say "sign in to Calendarr"
rather than "open Calendarr once". The events are deleted on sign-out, so the
absence of a cache alone cannot tell those two apart.

writerVersion, purely so a mismatch between the two apps is diagnosable.

The coverage constants move to SnapshotCoverage, so the writer and the code that
reconstructs the window for older files can no longer disagree about it.

CalendarStore needs an explicit `import CalendarrCore` because the app target
builds with SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY, which requires the
defining module to be imported directly rather than picked up transitively.

Verified: builds for iOS and Mac Catalyst; CalendarrKit's 11 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:00 +02:00

79 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 } // nothing selected = show all
let keep = Set(ids)
// Filtering narrows the events but not the window: the snapshot still
// speaks for the same range, it just has fewer calendars in it.
return WidgetSnapshot(
writtenAt: snapshot.writtenAt,
coverageStart: snapshot.coverageStart,
coverageEnd: snapshot.coverageEnd,
isLoggedIn: snapshot.isLoggedIn,
writerVersion: snapshot.writerVersion,
events: snapshot.events.filter { keep.contains($0.calendarKey) },
theme: snapshot.theme,
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 }
}
}