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:
@@ -8,6 +8,14 @@ import WidgetKit
|
||||
/// and the App Group ID registered in the Apple Developer Portal.
|
||||
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.
|
||||
/// We strip everything the widget doesn't need (notes, calendar IDs, URLs).
|
||||
struct WidgetEvent: Codable, Hashable, Identifiable {
|
||||
@@ -18,6 +26,27 @@ struct WidgetEvent: Codable, Hashable, Identifiable {
|
||||
let isAllDay: Bool
|
||||
let colorHex: 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.
|
||||
@@ -75,7 +104,8 @@ struct WidgetSnapshot: Codable {
|
||||
}
|
||||
|
||||
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? {
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: widgetAppGroupID)
|
||||
@@ -85,6 +115,10 @@ enum WidgetStore {
|
||||
containerURL?.appendingPathComponent(cacheFilename)
|
||||
}
|
||||
|
||||
private static var calendarsURL: URL? {
|
||||
containerURL?.appendingPathComponent(calendarsFilename)
|
||||
}
|
||||
|
||||
/// Called by the app whenever the event cache changes.
|
||||
static func write(_ snapshot: WidgetSnapshot) {
|
||||
guard let url = cacheURL else { return }
|
||||
@@ -103,6 +137,21 @@ enum WidgetStore {
|
||||
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
|
||||
/// from UserDefaults. Used when the user tweaks an appearance setting and
|
||||
/// we want the widgets to refresh immediately, without needing a new event
|
||||
|
||||
Reference in New Issue
Block a user