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>
This commit is contained in:
42
CalendarrWidgets/CalendarIntent.swift
Normal file
42
CalendarrWidgets/CalendarIntent.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
/// An individual calendar option shown in the widget configuration picker.
|
||||
struct CalendarAppEntity: AppEntity, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let colorHex: String
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Kalender"
|
||||
static var defaultQuery = CalendarEntityQuery()
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads available calendars from the App Group container so the widget
|
||||
/// extension can offer options without a network call.
|
||||
struct CalendarEntityQuery: EntityQuery {
|
||||
func entities(for identifiers: [String]) async throws -> [CalendarAppEntity] {
|
||||
let ids = Set(identifiers)
|
||||
return WidgetStore.readCalendars()
|
||||
.filter { ids.contains($0.id) }
|
||||
.map { CalendarAppEntity(id: $0.id, name: $0.name, colorHex: $0.colorHex) }
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [CalendarAppEntity] {
|
||||
WidgetStore.readCalendars()
|
||||
.map { CalendarAppEntity(id: $0.id, name: $0.name, colorHex: $0.colorHex) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget configuration intent: lets the user pick which calendars to show.
|
||||
/// An empty selection means "show all calendars" (the default).
|
||||
struct CalendarSelectionIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource = "Kalender auswählen"
|
||||
static var description = IntentDescription("Wähle welche Kalender im Widget angezeigt werden. Leer = alle Kalender.")
|
||||
|
||||
@Parameter(title: "Kalender")
|
||||
var selectedCalendars: [CalendarAppEntity]
|
||||
}
|
||||
@@ -1,33 +1,57 @@
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
struct CalendarrEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let snapshot: WidgetSnapshot?
|
||||
}
|
||||
|
||||
struct CalendarrTimelineProvider: TimelineProvider {
|
||||
struct CalendarrTimelineProvider: AppIntentTimelineProvider {
|
||||
typealias Entry = CalendarrEntry
|
||||
typealias Intent = CalendarSelectionIntent
|
||||
|
||||
func placeholder(in context: Context) -> CalendarrEntry {
|
||||
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (CalendarrEntry) -> Void) {
|
||||
completion(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 getTimeline(in context: Context, completion: @escaping (Timeline<CalendarrEntry>) -> Void) {
|
||||
let snapshot = WidgetStore.read()
|
||||
func timeline(for configuration: CalendarSelectionIntent, in context: Context) async -> Timeline<CalendarrEntry> {
|
||||
let raw = WidgetStore.read()
|
||||
let snap = filtered(raw, by: configuration)
|
||||
let now = Date()
|
||||
|
||||
// Provide one entry per hour for the next 24h so the widget keeps
|
||||
// re-rendering as time progresses (past events drop off, "now" advances).
|
||||
// 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: snapshot))
|
||||
entries.append(CalendarrEntry(date: date, snapshot: snap))
|
||||
}
|
||||
// Ask iOS to refresh in 30 min to pick up any new data the app wrote.
|
||||
let refreshAt = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
|
||||
completion(Timeline(entries: entries, policy: .after(refreshAt)))
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ struct TodayWidget: Widget {
|
||||
let kind: String = "TodayWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
TodayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.today_title", "system"))
|
||||
@@ -62,7 +62,7 @@ struct TwoDaysWidget: Widget {
|
||||
let kind: String = "TwoDaysWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
TwoDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.days_title", "system"))
|
||||
@@ -77,7 +77,7 @@ struct ThreeDaysWidget: Widget {
|
||||
let kind: String = "ThreeDaysWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
ThreeDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.threedays_title", "system"))
|
||||
@@ -92,7 +92,7 @@ struct ThisWeekWidget: Widget {
|
||||
let kind: String = "ThisWeekWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
ThisWeekWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.thisweek_title", "system"))
|
||||
@@ -107,7 +107,7 @@ struct TwoWeeksWidget: Widget {
|
||||
let kind: String = "TwoWeeksWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
TwoWeeksWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.twoweeks_title", "system"))
|
||||
@@ -122,7 +122,7 @@ struct UpcomingWidget: Widget {
|
||||
let kind: String = "UpcomingWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
UpcomingWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.upcoming_title", "system"))
|
||||
@@ -137,7 +137,7 @@ struct UpNextWidget: Widget {
|
||||
let kind: String = "UpNextWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
UpNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.upnext_title", "system"))
|
||||
@@ -152,7 +152,7 @@ struct CalendarDayWidget: Widget {
|
||||
let kind: String = "CalendarDayWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
CalendarDayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.calday_title", "system"))
|
||||
@@ -167,7 +167,7 @@ struct TwoMonthWidget: Widget {
|
||||
let kind: String = "TwoMonthWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
TwoMonthWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.twomonth_title", "system"))
|
||||
@@ -182,7 +182,7 @@ struct NowNextEventsWidget: Widget {
|
||||
let kind: String = "NowNextEventsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
NowNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||
}
|
||||
.configurationDisplayName(WidgetL10n.t("widget.display.nownext_title", "system"))
|
||||
@@ -197,7 +197,7 @@ struct LockScreenWidget: Widget {
|
||||
let kind: String = "LockScreenWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
LockScreenWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) { Color.clear }
|
||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||
@@ -214,7 +214,7 @@ struct LockScreenCountWidget: Widget {
|
||||
let kind: String = "LockScreenCountWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
LockScreenCountWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) { Color.clear }
|
||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||
@@ -231,7 +231,7 @@ struct LockScreenCountdownWidget: Widget {
|
||||
let kind: String = "LockScreenCountdownWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
||||
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||
LockScreenCountdownWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) { Color.clear }
|
||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||
|
||||
Reference in New Issue
Block a user