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:
@@ -511,7 +511,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.7;
|
MARKETING_VERSION = 2.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
||||||
PRODUCT_NAME = "Calendarr iOS";
|
PRODUCT_NAME = "Calendarr iOS";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -555,7 +555,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.7;
|
MARKETING_VERSION = 2.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
||||||
PRODUCT_NAME = "Calendarr iOS";
|
PRODUCT_NAME = "Calendarr iOS";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
|||||||
@@ -438,12 +438,31 @@ class CalendarStore {
|
|||||||
/// covers the worst-case month grid (6 rows × 7 cols) for the calendar
|
/// covers the worst-case month grid (6 rows × 7 cols) for the calendar
|
||||||
/// widget. Also asks the system to refresh the widget timeline.
|
/// widget. Also asks the system to refresh the widget timeline.
|
||||||
private func publishWidgetSnapshot() {
|
private func publishWidgetSnapshot() {
|
||||||
|
// Never write group-view data into the widget; it would show other
|
||||||
|
// people's calendar colours and decorated event titles.
|
||||||
|
guard activeGroup == nil else { return }
|
||||||
|
|
||||||
let cal = userCalendar
|
let cal = userCalendar
|
||||||
let now = Date()
|
let now = Date()
|
||||||
// Include the week before today so widgets that show the current week
|
// Include the week before today so widgets that show the current week
|
||||||
// (e.g. "This Week", "Up Next + Calendar") have data for Monday–today.
|
// (e.g. "This Week", "Up Next + Calendar") have data for Monday–today.
|
||||||
let from = cal.date(byAdding: .day, value: -7, to: cal.startOfDay(for: now)) ?? now
|
let from = cal.date(byAdding: .day, value: -7, to: cal.startOfDay(for: now)) ?? now
|
||||||
let to = cal.date(byAdding: .day, value: 42, to: cal.startOfDay(for: now)) ?? from
|
let to = cal.date(byAdding: .day, value: 42, to: cal.startOfDay(for: now)) ?? from
|
||||||
|
|
||||||
|
// Build the calendar list from all cached events (not just the window)
|
||||||
|
// so every calendar appears in the widget configuration picker.
|
||||||
|
var calendarMap: [String: WidgetCalendar] = [:]
|
||||||
|
for ev in allCachedEvents {
|
||||||
|
let key = Self.calendarKey(source: ev.source, calendarId: ev.calendarId)
|
||||||
|
guard !banishedCalendarKeys.contains(key) else { continue }
|
||||||
|
if calendarMap[key] == nil {
|
||||||
|
calendarMap[key] = WidgetCalendar(id: key,
|
||||||
|
name: ev.calendarName.isEmpty ? ev.calendarId : ev.calendarName,
|
||||||
|
colorHex: ev.calendarColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WidgetStore.writeCalendars(Array(calendarMap.values).sorted { $0.name < $1.name })
|
||||||
|
|
||||||
let visible = allCachedEvents
|
let visible = allCachedEvents
|
||||||
.filter { ev in
|
.filter { ev in
|
||||||
let key = Self.calendarKey(source: ev.source, calendarId: ev.calendarId)
|
let key = Self.calendarKey(source: ev.source, calendarId: ev.calendarId)
|
||||||
@@ -460,7 +479,8 @@ class CalendarStore {
|
|||||||
end: ev.endDate,
|
end: ev.endDate,
|
||||||
isAllDay: ev.isAllDay,
|
isAllDay: ev.isAllDay,
|
||||||
colorHex: ev.effectiveColor,
|
colorHex: ev.effectiveColor,
|
||||||
location: ev.location)
|
location: ev.location,
|
||||||
|
calendarKey: Self.calendarKey(source: ev.source, calendarId: ev.calendarId))
|
||||||
}
|
}
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
let snap = WidgetSnapshot(
|
let snap = WidgetSnapshot(
|
||||||
|
|||||||
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 WidgetKit
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
struct CalendarrEntry: TimelineEntry {
|
struct CalendarrEntry: TimelineEntry {
|
||||||
let date: Date
|
let date: Date
|
||||||
let snapshot: WidgetSnapshot?
|
let snapshot: WidgetSnapshot?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CalendarrTimelineProvider: TimelineProvider {
|
struct CalendarrTimelineProvider: AppIntentTimelineProvider {
|
||||||
|
typealias Entry = CalendarrEntry
|
||||||
|
typealias Intent = CalendarSelectionIntent
|
||||||
|
|
||||||
func placeholder(in context: Context) -> CalendarrEntry {
|
func placeholder(in context: Context) -> CalendarrEntry {
|
||||||
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
|
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (CalendarrEntry) -> Void) {
|
func snapshot(for configuration: CalendarSelectionIntent, in context: Context) async -> CalendarrEntry {
|
||||||
completion(CalendarrEntry(date: .now, snapshot: WidgetStore.read()))
|
let raw = WidgetStore.read()
|
||||||
|
return CalendarrEntry(date: .now, snapshot: filtered(raw, by: configuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<CalendarrEntry>) -> Void) {
|
func timeline(for configuration: CalendarSelectionIntent, in context: Context) async -> Timeline<CalendarrEntry> {
|
||||||
let snapshot = WidgetStore.read()
|
let raw = WidgetStore.read()
|
||||||
|
let snap = filtered(raw, by: configuration)
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
// Provide one entry per hour for the next 24h so the widget keeps
|
// One entry per hour for 24 h so the widget re-renders as time advances.
|
||||||
// re-rendering as time progresses (past events drop off, "now" advances).
|
|
||||||
var entries: [CalendarrEntry] = []
|
var entries: [CalendarrEntry] = []
|
||||||
for h in 0..<24 {
|
for h in 0..<24 {
|
||||||
let date = Calendar.current.date(byAdding: .hour, value: h, to: now) ?? now
|
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
|
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"
|
let kind: String = "TodayWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
TodayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.today_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.today_title", "system"))
|
||||||
@@ -62,7 +62,7 @@ struct TwoDaysWidget: Widget {
|
|||||||
let kind: String = "TwoDaysWidget"
|
let kind: String = "TwoDaysWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
TwoDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.days_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.days_title", "system"))
|
||||||
@@ -77,7 +77,7 @@ struct ThreeDaysWidget: Widget {
|
|||||||
let kind: String = "ThreeDaysWidget"
|
let kind: String = "ThreeDaysWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
ThreeDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.threedays_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.threedays_title", "system"))
|
||||||
@@ -92,7 +92,7 @@ struct ThisWeekWidget: Widget {
|
|||||||
let kind: String = "ThisWeekWidget"
|
let kind: String = "ThisWeekWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
ThisWeekWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.thisweek_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.thisweek_title", "system"))
|
||||||
@@ -107,7 +107,7 @@ struct TwoWeeksWidget: Widget {
|
|||||||
let kind: String = "TwoWeeksWidget"
|
let kind: String = "TwoWeeksWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
TwoWeeksWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.twoweeks_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.twoweeks_title", "system"))
|
||||||
@@ -122,7 +122,7 @@ struct UpcomingWidget: Widget {
|
|||||||
let kind: String = "UpcomingWidget"
|
let kind: String = "UpcomingWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
UpcomingWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.upcoming_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.upcoming_title", "system"))
|
||||||
@@ -137,7 +137,7 @@ struct UpNextWidget: Widget {
|
|||||||
let kind: String = "UpNextWidget"
|
let kind: String = "UpNextWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
UpNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.upnext_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.upnext_title", "system"))
|
||||||
@@ -152,7 +152,7 @@ struct CalendarDayWidget: Widget {
|
|||||||
let kind: String = "CalendarDayWidget"
|
let kind: String = "CalendarDayWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
CalendarDayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.calday_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.calday_title", "system"))
|
||||||
@@ -167,7 +167,7 @@ struct TwoMonthWidget: Widget {
|
|||||||
let kind: String = "TwoMonthWidget"
|
let kind: String = "TwoMonthWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
TwoMonthWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.twomonth_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.twomonth_title", "system"))
|
||||||
@@ -182,7 +182,7 @@ struct NowNextEventsWidget: Widget {
|
|||||||
let kind: String = "NowNextEventsWidget"
|
let kind: String = "NowNextEventsWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
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)
|
NowNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
|
||||||
}
|
}
|
||||||
.configurationDisplayName(WidgetL10n.t("widget.display.nownext_title", "system"))
|
.configurationDisplayName(WidgetL10n.t("widget.display.nownext_title", "system"))
|
||||||
@@ -197,7 +197,7 @@ struct LockScreenWidget: Widget {
|
|||||||
let kind: String = "LockScreenWidget"
|
let kind: String = "LockScreenWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||||
LockScreenWidgetView(entry: entry)
|
LockScreenWidgetView(entry: entry)
|
||||||
.containerBackground(for: .widget) { Color.clear }
|
.containerBackground(for: .widget) { Color.clear }
|
||||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||||
@@ -214,7 +214,7 @@ struct LockScreenCountWidget: Widget {
|
|||||||
let kind: String = "LockScreenCountWidget"
|
let kind: String = "LockScreenCountWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||||
LockScreenCountWidgetView(entry: entry)
|
LockScreenCountWidgetView(entry: entry)
|
||||||
.containerBackground(for: .widget) { Color.clear }
|
.containerBackground(for: .widget) { Color.clear }
|
||||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||||
@@ -231,7 +231,7 @@ struct LockScreenCountdownWidget: Widget {
|
|||||||
let kind: String = "LockScreenCountdownWidget"
|
let kind: String = "LockScreenCountdownWidget"
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
|
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
|
||||||
LockScreenCountdownWidgetView(entry: entry)
|
LockScreenCountdownWidgetView(entry: entry)
|
||||||
.containerBackground(for: .widget) { Color.clear }
|
.containerBackground(for: .widget) { Color.clear }
|
||||||
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import WidgetKit
|
|||||||
/// and the App Group ID registered in the Apple Developer Portal.
|
/// and the App Group ID registered in the Apple Developer Portal.
|
||||||
let widgetAppGroupID = "group.com.scarriffleservices.calendarr"
|
let widgetAppGroupID = "group.com.scarriffleservices.calendarr"
|
||||||
|
|
||||||
|
/// Lightweight calendar descriptor stored alongside the event cache so the
|
||||||
|
/// widget configuration intent can offer calendar options without a network call.
|
||||||
|
struct WidgetCalendar: Codable, Identifiable, Hashable {
|
||||||
|
let id: String // calendarKey ("source-calendarId")
|
||||||
|
let name: String
|
||||||
|
let colorHex: String
|
||||||
|
}
|
||||||
|
|
||||||
/// Lightweight event representation that lives inside the widget cache.
|
/// Lightweight event representation that lives inside the widget cache.
|
||||||
/// We strip everything the widget doesn't need (notes, calendar IDs, URLs).
|
/// We strip everything the widget doesn't need (notes, calendar IDs, URLs).
|
||||||
struct WidgetEvent: Codable, Hashable, Identifiable {
|
struct WidgetEvent: Codable, Hashable, Identifiable {
|
||||||
@@ -18,6 +26,27 @@ struct WidgetEvent: Codable, Hashable, Identifiable {
|
|||||||
let isAllDay: Bool
|
let isAllDay: Bool
|
||||||
let colorHex: String
|
let colorHex: String
|
||||||
let location: String
|
let location: String
|
||||||
|
let calendarKey: String // used for per-calendar filtering in widget config
|
||||||
|
|
||||||
|
init(id: String, title: String, start: Date, end: Date,
|
||||||
|
isAllDay: Bool, colorHex: String, location: String, calendarKey: String) {
|
||||||
|
self.id = id; self.title = title; self.start = start; self.end = end
|
||||||
|
self.isAllDay = isAllDay; self.colorHex = colorHex
|
||||||
|
self.location = location; self.calendarKey = calendarKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible decoder: old caches have no calendarKey field.
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
title = try c.decode(String.self, forKey: .title)
|
||||||
|
start = try c.decode(Date.self, forKey: .start)
|
||||||
|
end = try c.decode(Date.self, forKey: .end)
|
||||||
|
isAllDay = try c.decode(Bool.self, forKey: .isAllDay)
|
||||||
|
colorHex = try c.decode(String.self, forKey: .colorHex)
|
||||||
|
location = try c.decode(String.self, forKey: .location)
|
||||||
|
calendarKey = try c.decodeIfPresent(String.self, forKey: .calendarKey) ?? ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot blob the app writes to the App-Group container and the widget reads.
|
/// Snapshot blob the app writes to the App-Group container and the widget reads.
|
||||||
@@ -75,7 +104,8 @@ struct WidgetSnapshot: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WidgetStore {
|
enum WidgetStore {
|
||||||
private static let cacheFilename = "widget-cache.json"
|
private static let cacheFilename = "widget-cache.json"
|
||||||
|
private static let calendarsFilename = "widget-calendars.json"
|
||||||
|
|
||||||
private static var containerURL: URL? {
|
private static var containerURL: URL? {
|
||||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: widgetAppGroupID)
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: widgetAppGroupID)
|
||||||
@@ -85,6 +115,10 @@ enum WidgetStore {
|
|||||||
containerURL?.appendingPathComponent(cacheFilename)
|
containerURL?.appendingPathComponent(cacheFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static var calendarsURL: URL? {
|
||||||
|
containerURL?.appendingPathComponent(calendarsFilename)
|
||||||
|
}
|
||||||
|
|
||||||
/// Called by the app whenever the event cache changes.
|
/// Called by the app whenever the event cache changes.
|
||||||
static func write(_ snapshot: WidgetSnapshot) {
|
static func write(_ snapshot: WidgetSnapshot) {
|
||||||
guard let url = cacheURL else { return }
|
guard let url = cacheURL else { return }
|
||||||
@@ -103,6 +137,21 @@ enum WidgetStore {
|
|||||||
return try? decoder.decode(WidgetSnapshot.self, from: data)
|
return try? decoder.decode(WidgetSnapshot.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the list of available calendars so the widget intent can offer
|
||||||
|
/// calendar options for filtering without a network call.
|
||||||
|
static func writeCalendars(_ calendars: [WidgetCalendar]) {
|
||||||
|
guard let url = calendarsURL else { return }
|
||||||
|
if let data = try? JSONEncoder().encode(calendars) {
|
||||||
|
try? data.write(to: url, options: .atomic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the calendar list written by the main app.
|
||||||
|
static func readCalendars() -> [WidgetCalendar] {
|
||||||
|
guard let url = calendarsURL, let data = try? Data(contentsOf: url) else { return [] }
|
||||||
|
return (try? JSONDecoder().decode([WidgetCalendar].self, from: data)) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
/// Rewrite the existing snapshot with the latest colour / language values
|
/// Rewrite the existing snapshot with the latest colour / language values
|
||||||
/// from UserDefaults. Used when the user tweaks an appearance setting and
|
/// from UserDefaults. Used when the user tweaks an appearance setting and
|
||||||
/// we want the widgets to refresh immediately, without needing a new event
|
/// we want the widgets to refresh immediately, without needing a new event
|
||||||
|
|||||||
Reference in New Issue
Block a user