import EventKit import Foundation import LilithLogging private let log = AppLogger.logger(for: "IReminder.Reader") // MARK: - Models public struct ReminderCalendarEntry: Sendable, Equatable { public let calendarIdentifier: String public let title: String public let source: String public let color: String public init( calendarIdentifier: String, title: String, source: String, color: String ) { self.calendarIdentifier = calendarIdentifier self.title = title self.source = source self.color = color } } public struct ReminderEntry: Sendable, Equatable { public let reminderIdentifier: String public let calendarIdentifier: String public let title: String? public let notes: String? public let dueDate: Date? public let priority: Int public let isCompleted: Bool public let creationDate: Date? public let lastModifiedDate: Date? public init( reminderIdentifier: String, calendarIdentifier: String, title: String?, notes: String?, dueDate: Date?, priority: Int, isCompleted: Bool, creationDate: Date?, lastModifiedDate: Date? ) { self.reminderIdentifier = reminderIdentifier self.calendarIdentifier = calendarIdentifier self.title = title self.notes = notes self.dueDate = dueDate self.priority = priority self.isCompleted = isCompleted self.creationDate = creationDate self.lastModifiedDate = lastModifiedDate } } // MARK: - Reader /// Reads reminder lists and reminders from the system EventKit store /// (`EKEntityType.reminder`). public final class ReminderReader: @unchecked Sendable { public static let shared = ReminderReader() private let store = EKEventStore() private var authorized = false private init() {} /// Exposed for the Sender, which needs to call `save`/`remove` on the same /// authorized store the Reader uses. public var eventStore: EKEventStore { store } // MARK: - Authorization public func requestAuthorization() async -> Bool { if #available(macOS 14.0, *) { do { let granted = try await store.requestFullAccessToReminders() authorized = granted } catch { NSLog("ReminderReader: requestFullAccessToReminders failed: \(error)") authorized = false } } else { authorized = await withCheckedContinuation { continuation in store.requestAccess(to: .reminder) { granted, _ in continuation.resume(returning: granted) } } } NSLog("ReminderReader: auth granted=\(authorized)") return authorized } public var isAuthorized: Bool { if #available(macOS 14.0, *) { EKEventStore.authorizationStatus(for: .reminder) == .fullAccess } else { EKEventStore.authorizationStatus(for: .reminder) == .authorized } } // MARK: - Fetch public func fetchCalendars() -> [ReminderCalendarEntry] { store.calendars(for: .reminder).map { cal in ReminderCalendarEntry( calendarIdentifier: cal.calendarIdentifier, title: cal.title, source: cal.source?.title ?? "", color: hexColor(from: cal.cgColor) ) } } /// Fetch all reminders (across all reminder lists). If `since` is supplied, /// reminders whose `lastModifiedDate` is strictly older than `since` are /// dropped (matches the calendar reader's delta semantics). public func fetchReminders(since: Date? = nil) async -> [ReminderEntry] { let predicate = store.predicateForReminders(in: nil) let raw: [EKReminder] = await withCheckedContinuation { continuation in store.fetchReminders(matching: predicate) { reminders in continuation.resume(returning: reminders ?? []) } } NSLog("ReminderReader: fetchReminders raw=\(raw.count)") let filtered: [EKReminder] if let since { filtered = raw.filter { r in (r.lastModifiedDate ?? r.creationDate ?? Date.distantPast) >= since } } else { filtered = raw } NSLog("ReminderReader: fetchReminders filtered=\(filtered.count)") return filtered.map { mapReminder($0) } } // MARK: - Helpers private func mapReminder(_ r: EKReminder) -> ReminderEntry { ReminderEntry( reminderIdentifier: r.calendarItemIdentifier, calendarIdentifier: r.calendar?.calendarIdentifier ?? "", title: r.title, notes: r.notes, dueDate: r.dueDateComponents?.date, priority: r.priority, isCompleted: r.isCompleted, creationDate: r.creationDate, lastModifiedDate: r.lastModifiedDate ) } private func hexColor(from cgColor: CGColor?) -> String { guard let cgColor, let components = cgColor.components, components.count >= 3 else { return "#888888" } let r = Int(components[0] * 255) let g = Int(components[1] * 255) let b = Int(components[2] * 255) return String(format: "#%02X%02X%02X", r, g, b) } }