Compare commits
8 Commits
6b645dedf0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
d5ccfd9651
|
|||
|
57d605674e
|
|||
|
b133d8a4e9
|
|||
|
aef0f74616
|
|||
|
886b460d5e
|
|||
|
27e98a527f
|
|||
|
230a2836a6
|
|||
|
3514f450c9
|
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -11,24 +12,25 @@ class AppState: ObservableObject {
|
|||||||
// 功能的总开关
|
// 功能的总开关
|
||||||
@Published var isFunctionEnabled: Bool = false
|
@Published var isFunctionEnabled: Bool = false
|
||||||
|
|
||||||
// 用户当前是否空闲
|
|
||||||
@Published var isUserIdle: Bool = false
|
|
||||||
|
|
||||||
// 自动启动设置
|
// 自动启动设置
|
||||||
@Published var isLaunchAtLogin: Bool = false
|
@Published var isLaunchAtLogin: Bool = false
|
||||||
|
|
||||||
// 内部属性
|
// 内部属性
|
||||||
private var monitorTimer: Timer?
|
|
||||||
private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现
|
private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现
|
||||||
|
private var displayCountObserver: AnyCancellable? // 监听显示器数量变化
|
||||||
// 配置参数
|
private var wasAutomaticallyEnabled: Bool = false // 记录是否是自动启用的
|
||||||
private let idleThreshold: TimeInterval = 300 // 5分钟无操作视为空闲
|
private var currentDisplayCount: Int = NSScreen.screens.count // 当前显示器数量
|
||||||
private let checkInterval: TimeInterval = 10 // 每10秒检查一次
|
|
||||||
|
|
||||||
// 初始化方法
|
// 初始化方法
|
||||||
init() {
|
init() {
|
||||||
// 检查并设置初始的自动启动状态
|
// 检查并设置初始的自动启动状态
|
||||||
checkLaunchAtLoginStatus()
|
checkLaunchAtLoginStatus()
|
||||||
|
|
||||||
|
// 请求通知权限
|
||||||
|
requestNotificationPermission()
|
||||||
|
|
||||||
|
// 设置显示器数量变化监听
|
||||||
|
setupDisplayCountObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
@@ -37,52 +39,11 @@ class AppState: ObservableObject {
|
|||||||
func toggleFunction() {
|
func toggleFunction() {
|
||||||
isFunctionEnabled.toggle()
|
isFunctionEnabled.toggle()
|
||||||
if isFunctionEnabled {
|
if isFunctionEnabled {
|
||||||
// 开启功能,立即开始一轮检查,并设置定时器
|
// 开启功能,直接启动caffeinate,不检查用户活动
|
||||||
performCheck()
|
startCaffeinate()
|
||||||
startMonitoringTimer()
|
|
||||||
} else {
|
} else {
|
||||||
// 关闭功能,停止一切
|
// 关闭功能,停止caffeinate
|
||||||
stopMonitoringTimer()
|
|
||||||
stopCaffeinate()
|
stopCaffeinate()
|
||||||
updateUserIdleStatus(false) // 重置状态
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Monitoring Logic
|
|
||||||
|
|
||||||
private func startMonitoringTimer() {
|
|
||||||
guard monitorTimer == nil else { return }
|
|
||||||
print("开始监控...")
|
|
||||||
// 使用 weak self 避免循环引用
|
|
||||||
monitorTimer = Timer.scheduledTimer(withTimeInterval: checkInterval, repeats: true) { [weak self] _ in
|
|
||||||
self?.performCheck()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopMonitoringTimer() {
|
|
||||||
monitorTimer?.invalidate()
|
|
||||||
monitorTimer = nil
|
|
||||||
print("监控已停止。")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performCheck() {
|
|
||||||
// 1. 获取系统空闲时间
|
|
||||||
let idleTime = getSystemIdleTime()
|
|
||||||
|
|
||||||
// 2. 判断是否空闲
|
|
||||||
let isIdleNow = (idleTime >= idleThreshold)
|
|
||||||
|
|
||||||
// 3. 如果用户状态发生了变化,则采取行动
|
|
||||||
if isIdleNow != isUserIdle {
|
|
||||||
updateUserIdleStatus(isIdleNow)
|
|
||||||
|
|
||||||
if isIdleNow {
|
|
||||||
// 从“活动中”变为“空闲”
|
|
||||||
stopCaffeinate()
|
|
||||||
} else {
|
|
||||||
// 从“空闲”变为“活动中”
|
|
||||||
startCaffeinate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +86,7 @@ class AppState: ObservableObject {
|
|||||||
// 简化实现,直接启动 caffeinate 进程
|
// 简化实现,直接启动 caffeinate 进程
|
||||||
caffeinateProcess = Process()
|
caffeinateProcess = Process()
|
||||||
caffeinateProcess?.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")
|
caffeinateProcess?.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")
|
||||||
caffeinateProcess?.arguments = ["-d"] // -d 指示系统不要关闭显示屏
|
caffeinateProcess?.arguments = ["-i", "-s", "-d"] // -i 防止用户不活动导致睡眠,-s 防止系统进入睡眠,-d 防止显示屏关闭
|
||||||
|
|
||||||
// 配置输出,避免产生不必要的输出
|
// 配置输出,避免产生不必要的输出
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
@@ -155,14 +116,7 @@ class AppState: ObservableObject {
|
|||||||
print("已重置 caffeinate 进程引用")
|
print("已重置 caffeinate 进程引用")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UI State Update
|
|
||||||
|
|
||||||
/// 在主线程更新用户空闲状态
|
|
||||||
private func updateUserIdleStatus(_ isIdle: Bool) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isUserIdle = isIdle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Launch at Login Management
|
// MARK: - Launch at Login Management
|
||||||
|
|
||||||
@@ -248,11 +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 退出时清理资源
|
||||||
stopMonitoringTimer()
|
|
||||||
stopCaffeinate()
|
stopCaffeinate()
|
||||||
|
displayCountObserver?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,61 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_16x16.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_16x16@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_32x32.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_32x32@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_128x128.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_128x128@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_256x256.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_256x256@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_512x512.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon_512x512@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
|
|||||||
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 465 B |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
PowerBar/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +108,6 @@ struct MenuContent: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.transition(.opacity)
|
|
||||||
.animation(.easeInOut(duration: 0.3), value: appState.isFunctionEnabled)
|
|
||||||
|
|
||||||
// 状态显示
|
// 状态显示
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@@ -95,18 +118,10 @@ struct MenuContent: View {
|
|||||||
if appState.isFunctionEnabled {
|
if appState.isFunctionEnabled {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(appState.isUserIdle ? Color.orange : Color.green)
|
.fill(Color.green)
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
.transition(.scale(scale: 0.5, anchor: .center))
|
Text("保护中")
|
||||||
Text(appState.isUserIdle ? "空闲" : "活动中")
|
|
||||||
.font(.body)
|
.font(.body)
|
||||||
|
|
||||||
// 附加显示 caffeinate 状态
|
|
||||||
if !appState.isUserIdle {
|
|
||||||
Text("(保护中)")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
@@ -119,8 +134,7 @@ struct MenuContent: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.transition(.opacity)
|
|
||||||
.animation(.easeInOut(duration: 0.3), value: appState.isFunctionEnabled || appState.isUserIdle)
|
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(
|
.background(
|
||||||
|
|||||||
10
README.md
@@ -1,11 +1,17 @@
|
|||||||
# PowerBar
|
# PowerBar
|
||||||
|
|
||||||
一个智能的macOS菜单栏应用,根据用户活动状态自动管理系统睡眠。
|
一个智能的macOS菜单栏应用,根据用户活动状态和外部显示器连接状态自动管理系统睡眠。
|
||||||
|
|
||||||
|
## 版本
|
||||||
|
|
||||||
|
1.0.1
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠
|
- 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠
|
||||||
- 🔍 **空闲检测**:5分钟无操作自动视为空闲,停止阻止睡眠
|
- 🖥️ **外部显示器检测**:自动检测外部显示器连接状态
|
||||||
|
- 🚀 **自动控制**:外部显示器连接时自动开启保持亮屏
|
||||||
|
- 📢 **通知提醒**:外部显示器连接/断开时发送通知
|
||||||
- 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态
|
- 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态
|
||||||
- 🎨 **现代化UI**:简洁美观的菜单界面,易于操作
|
- 🎨 **现代化UI**:简洁美观的菜单界面,易于操作
|
||||||
- ⚡ **低资源消耗**:轻量级设计,几乎不占用系统资源
|
- ⚡ **低资源消耗**:轻量级设计,几乎不占用系统资源
|
||||||
|
|||||||
49
archive_export.sh
Executable 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 "=== 构建脚本执行结束 ==="
|
||||||
@@ -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 ==="
|
||||||
|
|
||||||
|
|||||||