Compare commits
14 Commits
041a4bb911
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
d5ccfd9651
|
|||
|
57d605674e
|
|||
|
b133d8a4e9
|
|||
|
aef0f74616
|
|||
|
886b460d5e
|
|||
|
27e98a527f
|
|||
|
230a2836a6
|
|||
|
3514f450c9
|
|||
|
6b645dedf0
|
|||
|
3cb14be4bc
|
|||
|
be65e275b3
|
|||
|
21bb3131a0
|
|||
|
693e4966d4
|
|||
|
37d73db4e5
|
3
.gitignore
vendored
@@ -94,5 +94,6 @@ Temporary Items
|
||||
# PowerBar specific
|
||||
PowerBar.xcarchive/
|
||||
ExportedApp/
|
||||
PowerBar.app
|
||||
PowerBar-*.dmg
|
||||
PowerBar.dmg
|
||||
ExportOptions.plist
|
||||
|
||||
20
ExportOptions.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>development</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>W9MA4VFH3F</string>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>compileBitcode</key>
|
||||
<false/>
|
||||
<key>thinning</key>
|
||||
<string><none></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -250,6 +250,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -259,12 +260,13 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSUIElement = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -282,6 +284,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -291,12 +294,13 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSUIElement = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.leo12025.PowerBar;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>PowerBar.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,6 +2,8 @@ import Foundation
|
||||
import Combine
|
||||
import IOKit.pwr_mgt
|
||||
import ServiceManagement
|
||||
import AppKit
|
||||
import UserNotifications
|
||||
|
||||
// MARK: - App State Manager
|
||||
class AppState: ObservableObject {
|
||||
@@ -10,24 +12,25 @@ class AppState: ObservableObject {
|
||||
// 功能的总开关
|
||||
@Published var isFunctionEnabled: Bool = false
|
||||
|
||||
// 用户当前是否空闲
|
||||
@Published var isUserIdle: Bool = false
|
||||
|
||||
// 自动启动设置
|
||||
@Published var isLaunchAtLogin: Bool = false
|
||||
|
||||
// 内部属性
|
||||
private var monitorTimer: Timer?
|
||||
private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现
|
||||
|
||||
// 配置参数
|
||||
private let idleThreshold: TimeInterval = 300 // 5分钟无操作视为空闲
|
||||
private let checkInterval: TimeInterval = 10 // 每10秒检查一次
|
||||
private var displayCountObserver: AnyCancellable? // 监听显示器数量变化
|
||||
private var wasAutomaticallyEnabled: Bool = false // 记录是否是自动启用的
|
||||
private var currentDisplayCount: Int = NSScreen.screens.count // 当前显示器数量
|
||||
|
||||
// 初始化方法
|
||||
init() {
|
||||
// 检查并设置初始的自动启动状态
|
||||
checkLaunchAtLoginStatus()
|
||||
|
||||
// 请求通知权限
|
||||
requestNotificationPermission()
|
||||
|
||||
// 设置显示器数量变化监听
|
||||
setupDisplayCountObserver()
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
@@ -36,52 +39,11 @@ class AppState: ObservableObject {
|
||||
func toggleFunction() {
|
||||
isFunctionEnabled.toggle()
|
||||
if isFunctionEnabled {
|
||||
// 开启功能,立即开始一轮检查,并设置定时器
|
||||
performCheck()
|
||||
startMonitoringTimer()
|
||||
// 开启功能,直接启动caffeinate,不检查用户活动
|
||||
startCaffeinate()
|
||||
} else {
|
||||
// 关闭功能,停止一切
|
||||
stopMonitoringTimer()
|
||||
// 关闭功能,停止caffeinate
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +86,7 @@ class AppState: ObservableObject {
|
||||
// 简化实现,直接启动 caffeinate 进程
|
||||
caffeinateProcess = Process()
|
||||
caffeinateProcess?.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")
|
||||
caffeinateProcess?.arguments = ["-d"] // -d 指示系统不要关闭显示屏
|
||||
caffeinateProcess?.arguments = ["-i", "-s", "-d"] // -i 防止用户不活动导致睡眠,-s 防止系统进入睡眠,-d 防止显示屏关闭
|
||||
|
||||
// 配置输出,避免产生不必要的输出
|
||||
let outputPipe = Pipe()
|
||||
@@ -154,14 +116,7 @@ class AppState: ObservableObject {
|
||||
print("已重置 caffeinate 进程引用")
|
||||
}
|
||||
|
||||
// MARK: - UI State Update
|
||||
|
||||
/// 在主线程更新用户空闲状态
|
||||
private func updateUserIdleStatus(_ isIdle: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
self.isUserIdle = isIdle
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Launch at Login Management
|
||||
|
||||
@@ -191,11 +146,178 @@ class AppState: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permission Management
|
||||
|
||||
/// 检查应用程序所需的权限
|
||||
func checkPermissions() {
|
||||
print("检查应用程序权限...")
|
||||
|
||||
// 检查自动启动权限状态
|
||||
checkLaunchAtLoginStatus()
|
||||
print("自动启动权限: \(isLaunchAtLogin ? "已启用" : "已禁用")")
|
||||
|
||||
// 检查是否可以执行系统命令
|
||||
let canExecuteSystemCommands = checkSystemCommandPermission()
|
||||
print("系统命令执行权限: \(canExecuteSystemCommands ? "已允许" : "已拒绝")")
|
||||
}
|
||||
|
||||
/// 检查是否可以执行系统命令
|
||||
private func checkSystemCommandPermission() -> Bool {
|
||||
// 尝试运行一个简单的系统命令来检查权限
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/echo")
|
||||
process.arguments = ["test"]
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
return process.terminationStatus == 0
|
||||
} catch {
|
||||
print("执行系统命令失败: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开系统设置的隐私与安全性页面
|
||||
func openSecuritySettings() {
|
||||
print("打开系统设置的隐私与安全性页面...")
|
||||
|
||||
let settingsUrl = URL(string: "x-apple.systempreferences:com.apple.preference.security")!
|
||||
if NSWorkspace.shared.open(settingsUrl) {
|
||||
print("已打开系统设置页面")
|
||||
} else {
|
||||
print("打开系统设置页面失败")
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开系统设置的登录项页面
|
||||
func openLoginItemsSettings() {
|
||||
print("打开系统设置的登录项页面...")
|
||||
|
||||
let settingsUrl = URL(string: "x-apple.systempreferences:com.apple.preference.usersandgroups?LoginItems")!
|
||||
if NSWorkspace.shared.open(settingsUrl) {
|
||||
print("已打开登录项设置页面")
|
||||
} else {
|
||||
print("打开登录项设置页面失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 退出时清理资源
|
||||
stopMonitoringTimer()
|
||||
stopCaffeinate()
|
||||
displayCountObserver?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,61 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"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,4 +1,6 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
struct PowerBarApp: App {
|
||||
@@ -25,24 +27,38 @@ struct PowerBarApp: App {
|
||||
.foregroundColor(appState.isFunctionEnabled ? .green : .gray)
|
||||
}
|
||||
}
|
||||
// 添加主窗口用于显示指引界面
|
||||
WindowGroup {
|
||||
if !hasShownGuide {
|
||||
GuideView()
|
||||
.onDisappear {
|
||||
hasShownGuide = true
|
||||
}
|
||||
}
|
||||
.onChange(of: ScenePhase.active) {
|
||||
// 当应用变为活动状态时,设置通知中心代理
|
||||
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
||||
NotificationDelegate.shared.appState = appState
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.defaultSize(width: 400, height: 400)
|
||||
.windowResizability(.contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
// 通知代理,处理通知响应
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单内容视图
|
||||
struct MenuContent: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
// 保持对窗口的强引用,避免过早释放
|
||||
@State private var guideWindow: NSWindow?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@@ -64,12 +80,15 @@ struct MenuContent: View {
|
||||
// 主要操作按钮:切换功能
|
||||
Button(action: {
|
||||
// 点击按钮,切换功能的开关状态
|
||||
appState.toggleFunction()
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
appState.toggleFunction()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: appState.isFunctionEnabled ? "stop.circle.fill" : "play.circle.fill")
|
||||
.foregroundColor(appState.isFunctionEnabled ? .red : .green)
|
||||
.font(.title2)
|
||||
.transition(.scale(scale: 0.8, anchor: .center))
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(appState.isFunctionEnabled ? "关闭常亮" : "开启常亮")
|
||||
.font(.body)
|
||||
@@ -83,6 +102,7 @@ struct MenuContent: View {
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(appState.isFunctionEnabled ? Color.red.opacity(0.1) : Color.green.opacity(0.1))
|
||||
.transition(.opacity)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -98,17 +118,10 @@ struct MenuContent: View {
|
||||
if appState.isFunctionEnabled {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(appState.isUserIdle ? Color.orange : Color.green)
|
||||
.fill(Color.green)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(appState.isUserIdle ? "空闲" : "活动中")
|
||||
Text("保护中")
|
||||
.font(.body)
|
||||
|
||||
// 附加显示 caffeinate 状态
|
||||
if !appState.isUserIdle {
|
||||
Text("(保护中)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 6) {
|
||||
@@ -121,6 +134,7 @@ struct MenuContent: View {
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
.padding(8)
|
||||
.background(
|
||||
@@ -130,36 +144,94 @@ struct MenuContent: View {
|
||||
|
||||
Divider()
|
||||
|
||||
// 权限管理
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("权限管理")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// 打开系统设置按钮
|
||||
Button(action: {
|
||||
// 打开系统设置的自动化页面
|
||||
let settingsUrl = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")!
|
||||
if NSWorkspace.shared.open(settingsUrl) {
|
||||
print("已打开系统设置自动化页面")
|
||||
} else {
|
||||
// 如果失败,尝试打开通用安全设置
|
||||
let fallbackUrl = URL(string: "x-apple.systempreferences:com.apple.preference.security")!
|
||||
NSWorkspace.shared.open(fallbackUrl)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.frame(width: 20)
|
||||
.foregroundColor(.blue)
|
||||
Text("打开系统设置")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.blue.opacity(0.05))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
|
||||
// 打开用户与群组设置,用于管理登录项
|
||||
Button(action: {
|
||||
let settingsUrl = URL(string: "x-apple.systempreferences:com.apple.preference.usersandgroups?LoginItems")!
|
||||
NSWorkspace.shared.open(settingsUrl)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "person.2.fill")
|
||||
.frame(width: 20)
|
||||
.foregroundColor(.blue)
|
||||
Text("管理登录项")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.blue.opacity(0.05))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
Divider()
|
||||
|
||||
// 自动启动选项
|
||||
Toggle(isOn: Binding(
|
||||
get: { appState.isLaunchAtLogin },
|
||||
set: { _ in appState.toggleLaunchAtLogin() }
|
||||
)) {
|
||||
HStack {
|
||||
Image(systemName: "power.on.circle")
|
||||
Image(systemName: "power.circle")
|
||||
.frame(width: 20)
|
||||
Text("开机自动启动")
|
||||
}
|
||||
}
|
||||
.toggleStyle(.checkbox)
|
||||
.font(.caption)
|
||||
.padding(4)
|
||||
.padding(8)
|
||||
|
||||
Divider()
|
||||
|
||||
// 显示指引界面选项
|
||||
Button("查看使用指南") {
|
||||
// 这里需要实现显示指引界面的逻辑
|
||||
// 我们可以使用 AppStorage 来控制指引界面的显示
|
||||
UserDefaults.standard.set(false, forKey: "hasShownGuide")
|
||||
// 重启应用或打开新窗口显示指引界面
|
||||
// 由于 macOS 应用的限制,我们可以提示用户重新启动应用查看指引
|
||||
NSAlert.showAlert(title: "使用指南", message: "请重启应用以查看使用指南,或在下次启动时自动显示。")
|
||||
// 直接打开指引窗口
|
||||
openGuideWindow()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(4)
|
||||
.padding(8)
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -170,16 +242,47 @@ struct MenuContent: View {
|
||||
.buttonStyle(.plain)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(4)
|
||||
.padding(8)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minWidth: 280)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white)
|
||||
.fill(Color(.windowBackgroundColor))
|
||||
.shadow(radius: 4)
|
||||
)
|
||||
}
|
||||
|
||||
// 打开指引窗口
|
||||
private func openGuideWindow() {
|
||||
// 如果窗口已存在,直接显示
|
||||
if let existingWindow = guideWindow {
|
||||
existingWindow.makeKeyAndOrderFront(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新窗口
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 400, height: 500),
|
||||
styleMask: [.titled, .closable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
// 设置窗口属性
|
||||
window.title = "PowerBar 使用指南"
|
||||
window.contentView = NSHostingView(rootView: GuideView())
|
||||
window.center()
|
||||
|
||||
// 显示窗口
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
// 记录已显示指引
|
||||
UserDefaults.standard.set(true, forKey: "hasShownGuide")
|
||||
|
||||
// 保存窗口引用
|
||||
guideWindow = window
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展 NSAlert,添加一个便捷方法来显示警报
|
||||
@@ -231,7 +334,7 @@ struct GuideView: View {
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "power.on.circle")
|
||||
Image(systemName: "power.circle")
|
||||
.font(.title)
|
||||
.foregroundColor(.orange)
|
||||
.frame(width: 40)
|
||||
@@ -338,13 +441,15 @@ struct GuideView: View {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.blue)
|
||||
)
|
||||
.transition(.scale(scale: 0.9, anchor: .center))
|
||||
.animation(.easeInOut(duration: 0.2), value: UUID())
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.frame(width: 400, height: 500)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white)
|
||||
.fill(Color(.windowBackgroundColor))
|
||||
)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
10
README.md
@@ -1,11 +1,17 @@
|
||||
# PowerBar
|
||||
|
||||
一个智能的macOS菜单栏应用,根据用户活动状态自动管理系统睡眠。
|
||||
一个智能的macOS菜单栏应用,根据用户活动状态和外部显示器连接状态自动管理系统睡眠。
|
||||
|
||||
## 版本
|
||||
|
||||
1.0.1
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠
|
||||
- 🔍 **空闲检测**:5分钟无操作自动视为空闲,停止阻止睡眠
|
||||
- 🖥️ **外部显示器检测**:自动检测外部显示器连接状态
|
||||
- 🚀 **自动控制**:外部显示器连接时自动开启保持亮屏
|
||||
- 📢 **通知提醒**:外部显示器连接/断开时发送通知
|
||||
- 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态
|
||||
- 🎨 **现代化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 "=== 构建脚本执行结束 ==="
|
||||
83
build_and_package.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PowerBar 打包脚本
|
||||
# 用于构建应用程序并创建DMG安装包
|
||||
|
||||
# 设置变量
|
||||
PROJECT_NAME="PowerBar"
|
||||
SCHEME="PowerBar"
|
||||
CONFIGURATION="Release"
|
||||
VERSION="1.0.1"
|
||||
|
||||
echo "=== PowerBar 打包脚本 v$VERSION ==="
|
||||
|
||||
# 1. 清理之前的构建产物和缓存
|
||||
echo "\n1. 清理之前的构建产物和缓存..."
|
||||
rm -rf "$PROJECT_NAME.app"
|
||||
rm -rf "$PROJECT_NAME-$VERSION.dmg"
|
||||
rm -rf "build"
|
||||
rm -rf "~/Library/Developer/Xcode/DerivedData/$PROJECT_NAME-*"
|
||||
rm -rf "$PROJECT_NAME.xcodeproj/xcuserdata"
|
||||
rm -rf "$PROJECT_NAME.xcodeproj/project.xcworkspace/xcuserdata"
|
||||
|
||||
# 2. 执行Xcode清理
|
||||
echo "\n2. 执行Xcode清理..."
|
||||
xcodebuild -scheme "$SCHEME" -configuration "$CONFIGURATION" clean
|
||||
|
||||
# 3. 直接构建应用程序
|
||||
echo "\n3. 构建应用程序..."
|
||||
xcodebuild -scheme "$SCHEME" -configuration "$CONFIGURATION" -derivedDataPath "build" build
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "构建失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 复制应用程序到当前目录
|
||||
echo "\n4. 复制应用程序到当前目录..."
|
||||
cp -R "build/Build/Products/$CONFIGURATION/$PROJECT_NAME.app" .
|
||||
|
||||
# 5. 验证应用程序
|
||||
echo "\n5. 验证应用程序..."
|
||||
if [ -d "$PROJECT_NAME.app" ]; then
|
||||
echo "应用程序构建成功:$PROJECT_NAME.app"
|
||||
else
|
||||
echo "应用程序构建失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 6. 创建DMG安装包
|
||||
echo "\n6. 创建DMG安装包..."
|
||||
|
||||
# 创建临时目录用于构建DMG
|
||||
temp_dmg_dir="temp_dmg"
|
||||
rm -rf "$temp_dmg_dir"
|
||||
mkdir -p "$temp_dmg_dir"
|
||||
|
||||
# 复制应用程序到临时目录
|
||||
cp -R "$PROJECT_NAME.app" "$temp_dmg_dir/"
|
||||
|
||||
# 创建Applications快捷方式
|
||||
ln -s /Applications "$temp_dmg_dir/Applications"
|
||||
|
||||
# 创建DMG
|
||||
hdiutil create -volname "$PROJECT_NAME-$VERSION" -srcfolder "$temp_dmg_dir" -ov -format UDZO "$PROJECT_NAME-$VERSION.dmg"
|
||||
|
||||
# 7. 清理临时文件
|
||||
echo "\n7. 清理临时文件..."
|
||||
rm -rf "$temp_dmg_dir"
|
||||
rm -rf "build"
|
||||
|
||||
# 8. 验证DMG
|
||||
echo "\n8. 验证DMG..."
|
||||
if [ -f "$PROJECT_NAME-$VERSION.dmg" ]; then
|
||||
echo "DMG创建成功:$PROJECT_NAME-$VERSION.dmg"
|
||||
echo "文件大小:$(du -h "$PROJECT_NAME-$VERSION.dmg" | cut -f1)"
|
||||
else
|
||||
echo "DMG创建失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\n=== 打包完成! ==="
|
||||
echo "应用程序:$PROJECT_NAME.app"
|
||||
echo "安装包:$PROJECT_NAME-$VERSION.dmg"
|
||||