macsync/@packages/ireminders/Sources/IReminderSync/Reader.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)
}
}