Compare commits

...

3 Commits

Author SHA1 Message Date
d5ccfd9651 feat: 添加 PowerBar 的构建脚本用于打包和导出应用 2025-12-27 23:08:10 +08:00
57d605674e feat: 升级版本至1.0.1并添加外部显示器支持
- 更新版本号至1.0.1
- 添加外部显示器检测功能
- 更新README文档说明新特性
- 移除临时Applications链接
2025-12-27 22:46:41 +08:00
b133d8a4e9 feat(通知): 添加显示器连接状态检测和通知功能
当检测到外部显示器连接时自动启用保持亮屏功能
当外部显示器断开时发送通知询问是否关闭功能
添加通知代理处理用户响应
2025-12-27 22:42:43 +08:00
6 changed files with 207 additions and 5 deletions

View File

@@ -266,7 +266,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar; PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -300,7 +300,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar; PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;

View File

@@ -3,6 +3,7 @@ import Combine
import IOKit.pwr_mgt import IOKit.pwr_mgt
import ServiceManagement import ServiceManagement
import AppKit import AppKit
import UserNotifications
// MARK: - App State Manager // MARK: - App State Manager
class AppState: ObservableObject { class AppState: ObservableObject {
@@ -16,11 +17,20 @@ class AppState: ObservableObject {
// //
private var caffeinateProcess: Process? // 使Process private var caffeinateProcess: Process? // 使Process
private var displayCountObserver: AnyCancellable? //
private var wasAutomaticallyEnabled: Bool = false //
private var currentDisplayCount: Int = NSScreen.screens.count //
// //
init() { init() {
// //
checkLaunchAtLoginStatus() checkLaunchAtLoginStatus()
//
requestNotificationPermission()
//
setupDisplayCountObserver()
} }
// MARK: - Public Methods // 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 // MARK: - Deinitializer
deinit { deinit {
// App 退 // App 退
stopCaffeinate() stopCaffeinate()
displayCountObserver?.cancel()
} }
} }

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import UserNotifications
@main @main
struct PowerBarApp: App { struct PowerBarApp: App {
@@ -26,6 +27,30 @@ struct PowerBarApp: App {
.foregroundColor(appState.isFunctionEnabled ? .green : .gray) .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)
} }
} }

View File

@@ -1,11 +1,17 @@
# PowerBar # PowerBar
一个智能的macOS菜单栏应用根据用户活动状态自动管理系统睡眠。 一个智能的macOS菜单栏应用根据用户活动状态和外部显示器连接状态自动管理系统睡眠。
## 版本
1.0.1
## 功能特性 ## 功能特性
- 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠 - 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠
- 🔍 **空闲检测**5分钟无操作自动视为空闲停止阻止睡眠 - 🖥️ **外部显示器检测**自动检测外部显示器连接状态
- 🚀 **自动控制**:外部显示器连接时自动开启保持亮屏
- 📢 **通知提醒**:外部显示器连接/断开时发送通知
- 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态 - 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态
- 🎨 **现代化UI**:简洁美观的菜单界面,易于操作 - 🎨 **现代化UI**:简洁美观的菜单界面,易于操作
-**低资源消耗**:轻量级设计,几乎不占用系统资源 -**低资源消耗**:轻量级设计,几乎不占用系统资源

49
archive_export.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# PowerBar 构建脚本
# 使用 archive + export 方式构建应用
set -e
echo "=== PowerBar 构建脚本 ==="
echo "1. 清理之前的构建产物..."
# 清理之前的构建产物
rm -rf build
rm -rf archive
rm -rf PowerBar.app
rm -rf PowerBar-*.dmg
echo "2. 创建构建目录..."
mkdir -p build
mkdir -p archive
echo "3. 归档项目..."
xcodebuild archive \
-project PowerBar.xcodeproj \
-scheme PowerBar \
-archivePath archive/PowerBar.xcarchive \
-destination 'generic/platform=macOS' \
-configuration Release
echo "4. 导出归档..."
xcodebuild -exportArchive -archivePath archive/PowerBar.xcarchive -exportPath build -exportOptionsPlist ExportOptions.plist
echo "5. 复制应用到当前目录..."
cp -R build/PowerBar.app .
echo "6. 创建DMG安装包..."
# 获取当前版本号
VERSION=$(xcodebuild -project PowerBar.xcodeproj -showBuildSettings | grep MARKETING_VERSION | head -1 | awk -F'=' '{print $2}' | xargs)
# 创建DMG
hdiutil create -volname "PowerBar" \
-srcfolder PowerBar.app \
-ov \
-format UDZO \
PowerBar-$VERSION.dmg
echo "7. 构建完成!"
echo "应用:$(pwd)/PowerBar.app"
echo "DMG$(pwd)/PowerBar-$VERSION.dmg"
echo "=== 构建脚本执行结束 ==="

View File

@@ -7,7 +7,7 @@
PROJECT_NAME="PowerBar" PROJECT_NAME="PowerBar"
SCHEME="PowerBar" SCHEME="PowerBar"
CONFIGURATION="Release" CONFIGURATION="Release"
VERSION="1.0.0" VERSION="1.0.1"
echo "=== PowerBar 打包脚本 v$VERSION ===" echo "=== PowerBar 打包脚本 v$VERSION ==="