- watchOS: add WatchSessionManager (WCSession bridge), WatchNotificationHandler (snooze/dismiss actions), recommendations() for AppIntentTimelineProvider - watchOS: WatchTimerStore tick loop with cascade haptics, App Group sync - macOS: CreateTimerSheet (countdown/alarm/pomodoro), launch-at-login toggle - macOS: MenuBarState showCreateSheet, MacSettingsView data tab - Xcode project updated with new file references
132 lines
4.4 KiB
Swift
132 lines
4.4 KiB
Swift
// Local notification handler for watchOS timer alerts
|
|
|
|
import Foundation
|
|
import UserNotifications
|
|
import os
|
|
|
|
@MainActor
|
|
final class WatchNotificationHandler: NSObject, ObservableObject {
|
|
static let shared = WatchNotificationHandler()
|
|
|
|
private let logger = Logger(subsystem: "com.chronomind.watch", category: "Notifications")
|
|
private let center = UNUserNotificationCenter.current()
|
|
|
|
static let timerFiredCategory = "CHRONOMIND_TIMER_FIRED"
|
|
static let snoozeAction = "SNOOZE_ACTION"
|
|
static let dismissAction = "DISMISS_ACTION"
|
|
|
|
private override init() {
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Setup
|
|
|
|
func requestAuthorization() {
|
|
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
|
Task { @MainActor in
|
|
if let error = error {
|
|
self.logger.error("Notification auth error: \(error.localizedDescription)")
|
|
return
|
|
}
|
|
self.logger.info("Notification auth granted: \(granted)")
|
|
self.registerCategories()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func registerCategories() {
|
|
let snooze = UNNotificationAction(
|
|
identifier: Self.snoozeAction,
|
|
title: "Snooze 5m",
|
|
options: []
|
|
)
|
|
let dismiss = UNNotificationAction(
|
|
identifier: Self.dismissAction,
|
|
title: "Dismiss",
|
|
options: [.destructive]
|
|
)
|
|
|
|
let category = UNNotificationCategory(
|
|
identifier: Self.timerFiredCategory,
|
|
actions: [snooze, dismiss],
|
|
intentIdentifiers: [],
|
|
options: []
|
|
)
|
|
|
|
center.setNotificationCategories([category])
|
|
center.delegate = self
|
|
logger.info("Notification categories registered")
|
|
}
|
|
|
|
// MARK: - Schedule
|
|
|
|
func scheduleTimerNotification(id: String, label: String, targetTime: Date, urgency: UrgencyLevel) {
|
|
let interval = targetTime.timeIntervalSinceNow
|
|
guard interval > 0 else { return }
|
|
|
|
let content = UNMutableNotificationContent()
|
|
content.title = "Timer Fired"
|
|
content.body = label
|
|
content.sound = urgency == .critical ? .defaultCritical : .default
|
|
content.categoryIdentifier = Self.timerFiredCategory
|
|
content.userInfo = ["timerId": id, "urgency": urgency.rawValue]
|
|
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: false)
|
|
let request = UNNotificationRequest(identifier: "timer-\(id)", content: content, trigger: trigger)
|
|
|
|
center.add(request) { error in
|
|
Task { @MainActor in
|
|
if let error = error {
|
|
self.logger.error("Failed to schedule notification: \(error.localizedDescription)")
|
|
} else {
|
|
self.logger.info("Scheduled notification for timer \(id) in \(Int(interval))s")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func cancelNotification(for timerId: String) {
|
|
center.removePendingNotificationRequests(withIdentifiers: ["timer-\(timerId)"])
|
|
}
|
|
}
|
|
|
|
// MARK: - UNUserNotificationCenterDelegate
|
|
|
|
extension WatchNotificationHandler: UNUserNotificationCenterDelegate {
|
|
nonisolated func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
didReceive response: UNNotificationResponse,
|
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
) {
|
|
let userInfo = response.notification.request.content.userInfo
|
|
guard let timerId = userInfo["timerId"] as? String else {
|
|
completionHandler()
|
|
return
|
|
}
|
|
|
|
Task { @MainActor in
|
|
let sessionManager = WatchSessionManager.shared
|
|
switch response.actionIdentifier {
|
|
case Self.snoozeAction:
|
|
sessionManager.sendCommand(.snooze(id: timerId, minutes: 5))
|
|
logger.info("Snooze action for timer \(timerId)")
|
|
case Self.dismissAction:
|
|
sessionManager.sendCommand(.dismiss(id: timerId))
|
|
logger.info("Dismiss action for timer \(timerId)")
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
completionHandler()
|
|
}
|
|
|
|
nonisolated func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
willPresent notification: UNNotification,
|
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
) {
|
|
completionHandler([.banner, .sound])
|
|
}
|
|
}
|