diff --git a/src/client/MacSyncApp.swift b/src/client/MacSyncApp.swift index 7b11713..23ca572 100644 --- a/src/client/MacSyncApp.swift +++ b/src/client/MacSyncApp.swift @@ -4,7 +4,9 @@ import Foundation import ICalSync import IMailSync import IMessageSync +import INoteSync import IPhotoSync +import IReminderSync import MacSyncShared import Photos @@ -27,11 +29,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private let iPhotoSync = IPhotoSync.SyncManager.shared private let iMailSync = IMailSync.SyncManager.shared private let iCalSync = ICalSync.SyncManager.shared + private let iReminderSync = IReminderSync.SyncManager.shared + private let iNoteSync = INoteSync.SyncManager.shared private let webServer = LocalWebServer() private var syncStatusItem: NSMenuItem? private var iphotoStatusItem: NSMenuItem? private var imailStatusItem: NSMenuItem? private var icalStatusItem: NSMenuItem? + private var ireminderStatusItem: NSMenuItem? + private var inoteStatusItem: NSMenuItem? private var contactRenderStatusItem: NSMenuItem? func applicationDidFinishLaunching(_ notification: Notification) { @@ -62,6 +68,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { iPhotoSync.startSync() iMailSync.startSync() iCalSync.startSync() + iReminderSync.startSync() + iNoteSync.startSync() ContactRenderPoller.shared.start() } else { let existingDeviceId = try? macSyncSharedKeychain.loadString(key: "deviceId") @@ -96,6 +104,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { self.iPhotoSync.startSync() self.iMailSync.startSync() self.iCalSync.startSync() + self.iReminderSync.startSync() + self.iNoteSync.startSync() ContactRenderPoller.shared.start() } } catch { @@ -134,6 +144,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(iCalItem) icalStatusItem = iCalItem + let iReminderItem = NSMenuItem(title: "Reminders: idle", action: nil, keyEquivalent: "") + iReminderItem.isEnabled = false + menu.addItem(iReminderItem) + ireminderStatusItem = iReminderItem + + let iNoteItem = NSMenuItem(title: "Notes: idle", action: nil, keyEquivalent: "") + iNoteItem.isEnabled = false + menu.addItem(iNoteItem) + inoteStatusItem = iNoteItem + let contactRenderItem = NSMenuItem(title: "Contacts: idle", action: nil, keyEquivalent: "") contactRenderItem.isEnabled = false menu.addItem(contactRenderItem) @@ -223,6 +243,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate { icalStatusItem?.title = "iCal: idle" } + // Reminders + if iReminderSync.isSyncing { + ireminderStatusItem?.title = "Reminders: \(iReminderSync.currentOperation)" + } else if let last = iReminderSync.lastSyncCompletedAt { + let total = iReminderSync.stats.reminderCount + let suffix = total > 0 ? " (\(total) reminders)" : "" + ireminderStatusItem?.title = "Reminders: \(relFormatter.localizedString(for: last, relativeTo: Date()))\(suffix)" + } else if iReminderSync.syncError != .none { + ireminderStatusItem?.title = "Reminders: \(iReminderSync.syncError.message)" + } else { + ireminderStatusItem?.title = "Reminders: idle" + } + + // Notes + if iNoteSync.isSyncing { + inoteStatusItem?.title = "Notes: \(iNoteSync.currentOperation)" + } else if let last = iNoteSync.lastSyncCompletedAt { + let total = iNoteSync.stats.noteCount + let suffix = total > 0 ? " (\(total) notes)" : "" + inoteStatusItem?.title = "Notes: \(relFormatter.localizedString(for: last, relativeTo: Date()))\(suffix)" + } else if iNoteSync.syncError != .none { + inoteStatusItem?.title = "Notes: \(iNoteSync.syncError.message)" + } else { + inoteStatusItem?.title = "Notes: idle" + } + // Contact render let poller = ContactRenderPoller.shared if !poller.lastError.isEmpty { @@ -242,6 +288,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { iPhotoSync.syncNow() iMailSync.syncNow() iCalSync.syncNow() + iReminderSync.syncNow() + iNoteSync.syncNow() } @objc private func openSettings() { @@ -280,7 +328,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { calendars.target = self submenu.addItem(calendars) - let automation = NSMenuItem(title: "Grant Messages + Mail Automation…", action: #selector(requestAutomation), keyEquivalent: "") + let reminders = NSMenuItem(title: "Grant Reminders…", action: #selector(requestReminders), keyEquivalent: "") + reminders.target = self + submenu.addItem(reminders) + + let automation = NSMenuItem(title: "Grant Messages + Mail + Notes Automation…", action: #selector(requestAutomation), keyEquivalent: "") automation.target = self submenu.addItem(automation) @@ -372,13 +424,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + @objc private func requestReminders() { + NSLog("MacSync: requesting reminders permission") + Task { + let granted = await iReminderSync.reader.requestAuthorization() + if !granted { + await MainActor.run { + NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders")! + ) + } + } + } + } + @objc private func requestAutomation() { - NSLog("MacSync: priming Messages + Mail automation TCC") + NSLog("MacSync: priming Messages + Mail + Notes automation TCC") // Sending a harmless AppleScript to each app registers MacSync in the Automation // pane and surfaces the first-time "Allow" prompt. let probe = """ tell application \"Messages\" to get name tell application \"Mail\" to get name + tell application \"Notes\" to get name """ var errorInfo: NSDictionary? _ = NSAppleScript(source: probe)?.executeAndReturnError(&errorInfo) diff --git a/src/client/Resources/Info.plist.template b/src/client/Resources/Info.plist.template index 38ccb10..17b957c 100644 --- a/src/client/Resources/Info.plist.template +++ b/src/client/Resources/Info.plist.template @@ -19,10 +19,10 @@ NSContactsUsageDescription MacSync syncs your contacts to your personal server. NSCalendarsUsageDescription - MacSync syncs your calendar events to your personal server. + MacSync reads and writes calendar events so changes made on the web sync back to Calendar.app. NSRemindersUsageDescription - MacSync syncs your reminders to your personal server. + MacSync reads and writes Reminders so changes made on the web sync back to Reminders.app. NSAppleEventsUsageDescription - MacSync reads your Mail.app messages to sync email to your personal server. + MacSync controls Messages.app, Mail.app, and Notes.app via AppleScript to sync messages, email, and notes to and from your personal server.