2026-04-27 12:21:12 +00:00
|
|
|
//
|
|
|
|
|
// NotificationCenter.swift
|
|
|
|
|
// 摸鱼侠 plugin v0.1
|
|
|
|
|
//
|
|
|
|
|
// Thin wrapper around UNUserNotificationCenter. Plugin bundles inherit
|
|
|
|
|
// the host app's bundle identifier for notification authorization, so
|
|
|
|
|
// the host (Mio Island) needs to have notifications allowed in System
|
|
|
|
|
// Settings. We request once on activate; if it's denied we fall back
|
|
|
|
|
// to the in-panel red-dot pulse pattern.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
@preconcurrency import UserNotifications
|
|
|
|
|
import AppKit
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class WorkerNotificationCenter {
|
|
|
|
|
static let shared = WorkerNotificationCenter()
|
|
|
|
|
|
|
|
|
|
private(set) var isAuthorized: Bool = false
|
|
|
|
|
private var didRequest: Bool = false
|
|
|
|
|
|
|
|
|
|
private init() {}
|
|
|
|
|
|
|
|
|
|
/// Asks the user once. Subsequent calls are no-ops.
|
|
|
|
|
func requestAuthorizationIfNeeded() {
|
|
|
|
|
guard !didRequest else { return }
|
|
|
|
|
didRequest = true
|
|
|
|
|
let center = UNUserNotificationCenter.current()
|
|
|
|
|
center.getNotificationSettings { settings in
|
|
|
|
|
let status = settings.authorizationStatus
|
|
|
|
|
switch status {
|
|
|
|
|
case .authorized, .provisional, .ephemeral:
|
2026-05-19 09:54:28 +00:00
|
|
|
Task { @MainActor in Self.applyAuthorized(true) }
|
2026-04-27 12:21:12 +00:00
|
|
|
case .denied:
|
2026-05-19 09:54:28 +00:00
|
|
|
Task { @MainActor in Self.applyAuthorized(false) }
|
2026-04-27 12:21:12 +00:00
|
|
|
WorkerDebugLog.write("notifications denied — falling back to in-panel dot")
|
|
|
|
|
case .notDetermined:
|
|
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
|
|
|
|
|
if let error = error {
|
|
|
|
|
WorkerDebugLog.write("notif auth error: \(error)")
|
|
|
|
|
}
|
2026-05-19 09:54:28 +00:00
|
|
|
Task { @MainActor in Self.applyAuthorized(granted) }
|
2026-04-27 12:21:12 +00:00
|
|
|
WorkerDebugLog.write("notif auth granted=\(granted)")
|
|
|
|
|
}
|
|
|
|
|
@unknown default:
|
2026-05-19 09:54:28 +00:00
|
|
|
Task { @MainActor in Self.applyAuthorized(false) }
|
2026-04-27 12:21:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 09:54:28 +00:00
|
|
|
/// P0 fix (2026-05-19 review): prior code set
|
|
|
|
|
/// `WorkerNotificationCenter.shared.isAuthorized` but never
|
|
|
|
|
/// propagated to `WorkerStore.notificationsAuthorized`. Result: the
|
|
|
|
|
/// top-bar notif status dot stayed dim even after the user authorized.
|
|
|
|
|
/// Always write both so observers see consistent state.
|
|
|
|
|
@MainActor
|
|
|
|
|
private static func applyAuthorized(_ value: Bool) {
|
|
|
|
|
WorkerNotificationCenter.shared.isAuthorized = value
|
|
|
|
|
WorkerStore.shared.notificationsAuthorized = value
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 12:21:12 +00:00
|
|
|
/// Fire-and-forget local notification. Returns true if scheduled
|
|
|
|
|
/// (best effort — auth status may flip between scheduling and firing).
|
|
|
|
|
@discardableResult
|
|
|
|
|
func notify(title: String, body: String, identifier: String = UUID().uuidString) -> Bool {
|
|
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
|
content.title = title
|
|
|
|
|
content.body = body
|
|
|
|
|
content.sound = .default
|
|
|
|
|
|
|
|
|
|
// Trigger immediately. nil means deliver right away. We use a
|
|
|
|
|
// 0.1s threshold trigger because some macOS builds are flaky
|
|
|
|
|
// with truly-instant local notifications from plugin bundles.
|
|
|
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
|
|
|
|
let req = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
|
|
|
|
UNUserNotificationCenter.current().add(req) { err in
|
|
|
|
|
if let err = err {
|
|
|
|
|
WorkerDebugLog.write("notif schedule error: \(err)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always bounce the dock as a backup signal — works even when
|
|
|
|
|
// notifications are denied.
|
|
|
|
|
NSApp.requestUserAttention(.criticalRequest)
|
|
|
|
|
return isAuthorized
|
|
|
|
|
}
|
|
|
|
|
}
|