171 lines
5.4 KiB
Swift
171 lines
5.4 KiB
Swift
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)
|
|
}
|
|
}
|