From b133d8a4e968c837f5830fe168736517cb3fdcb7 Mon Sep 17 00:00:00 2001 From: leo12025 Date: Sat, 27 Dec 2025 22:42:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=80=9A=E7=9F=A5):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=99=A8=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=92=8C=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当检测到外部显示器连接时自动启用保持亮屏功能 当外部显示器断开时发送通知询问是否关闭功能 添加通知代理处理用户响应 --- PowerBar/AppState.swift | 122 +++++++++++++++++++++++++++++++++++++ PowerBar/PowerBarApp.swift | 25 ++++++++ temp_dmg/Applications | 1 + 3 files changed, 148 insertions(+) create mode 120000 temp_dmg/Applications diff --git a/PowerBar/AppState.swift b/PowerBar/AppState.swift index 4902c27..2522023 100644 --- a/PowerBar/AppState.swift +++ b/PowerBar/AppState.swift @@ -3,6 +3,7 @@ import Combine import IOKit.pwr_mgt import ServiceManagement import AppKit +import UserNotifications // MARK: - App State Manager class AppState: ObservableObject { @@ -16,11 +17,20 @@ class AppState: ObservableObject { // 内部属性 private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现 + private var displayCountObserver: AnyCancellable? // 监听显示器数量变化 + private var wasAutomaticallyEnabled: Bool = false // 记录是否是自动启用的 + private var currentDisplayCount: Int = NSScreen.screens.count // 当前显示器数量 // 初始化方法 init() { // 检查并设置初始的自动启动状态 checkLaunchAtLoginStatus() + + // 请求通知权限 + requestNotificationPermission() + + // 设置显示器数量变化监听 + setupDisplayCountObserver() } // MARK: - Public Methods @@ -192,10 +202,122 @@ class AppState: ObservableObject { } } + // MARK: - Display Monitoring and Auto-enable + + /// 请求通知权限 + private func requestNotificationPermission() { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + if granted { + print("通知权限已授予") + } else if let error = error { + print("请求通知权限失败: \(error)") + } + } + } + + /// 设置显示器数量变化监听 + private func setupDisplayCountObserver() { + // 使用 Combine 监听 NSScreen.screens.count 的变化 + // 这里使用定时器定期检查,因为没有直接的通知可以监听 + displayCountObserver = Timer.publish(every: 5, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.checkDisplayCount() + } + } + + /// 检查显示器数量变化 + private func checkDisplayCount() { + let newDisplayCount = NSScreen.screens.count + + // 如果显示器数量变化 + if newDisplayCount != currentDisplayCount { + if newDisplayCount > 1 { + // 外部显示器连接 + handleExternalDisplayConnected() + } else { + // 外部显示器断开 + handleExternalDisplayDisconnected() + } + currentDisplayCount = newDisplayCount + } + } + + /// 处理外部显示器连接 + private func handleExternalDisplayConnected() { + if !isFunctionEnabled { + // 自动启用保持亮屏 + wasAutomaticallyEnabled = true + toggleFunction() + + // 发送通知 + sendNotification(title: "PowerBar", body: "检测到外部显示器,已自动打开保持亮屏功能") + } + } + + /// 处理外部显示器断开 + private func handleExternalDisplayDisconnected() { + if wasAutomaticallyEnabled && isFunctionEnabled { + // 发送通知询问是否关闭保持亮屏 + sendNotificationWithAction(title: "PowerBar", body: "外部显示器已断开,是否关闭保持亮屏功能?") + } + } + + /// 发送普通通知 + private func sendNotification(title: String, body: String) { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = UNNotificationSound.default + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("发送通知失败: \(error)") + } + } + } + + /// 发送带操作的通知 + private func sendNotificationWithAction(title: String, body: String) { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = UNNotificationSound.default + content.categoryIdentifier = "displayDisconnected" + + // 注册通知类别和操作 + let cancelAction = UNNotificationAction(identifier: "cancel", title: "取消", options: []) + let disableAction = UNNotificationAction(identifier: "disable", title: "关闭", options: []) + let category = UNNotificationCategory(identifier: "displayDisconnected", actions: [disableAction, cancelAction], intentIdentifiers: []) + UNUserNotificationCenter.current().setNotificationCategories([category]) + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("发送通知失败: \(error)") + } + } + } + + /// 处理通知操作 + func handleNotificationResponse(identifier: String) { + if identifier == "disable" { + // 用户选择关闭保持亮屏 + wasAutomaticallyEnabled = false + toggleFunction() + } else if identifier == "cancel" { + // 用户选择取消,保持当前状态 + wasAutomaticallyEnabled = false + } + } + // MARK: - Deinitializer deinit { // 确保 App 退出时清理资源 stopCaffeinate() + displayCountObserver?.cancel() } } diff --git a/PowerBar/PowerBarApp.swift b/PowerBar/PowerBarApp.swift index e2c6b26..c92bf5d 100644 --- a/PowerBar/PowerBarApp.swift +++ b/PowerBar/PowerBarApp.swift @@ -1,5 +1,6 @@ import SwiftUI import AppKit +import UserNotifications @main struct PowerBarApp: App { @@ -26,6 +27,30 @@ struct PowerBarApp: App { .foregroundColor(appState.isFunctionEnabled ? .green : .gray) } } + .onChange(of: ScenePhase.active) { + // 当应用变为活动状态时,设置通知中心代理 + UNUserNotificationCenter.current().delegate = NotificationDelegate.shared + NotificationDelegate.shared.appState = appState + } + } +} + +// 通知代理,处理通知响应 +class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + static let shared = NotificationDelegate() + weak var appState: AppState? + + private override init() { super.init() } + + // 当通知在前台收到时调用 + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + return [.banner, .sound] + } + + // 当用户点击通知操作时调用 + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + let actionIdentifier = response.actionIdentifier + appState?.handleNotificationResponse(identifier: actionIdentifier) } } diff --git a/temp_dmg/Applications b/temp_dmg/Applications new file mode 120000 index 0000000..bd2d47f --- /dev/null +++ b/temp_dmg/Applications @@ -0,0 +1 @@ +/Applications \ No newline at end of file