Compare commits

..

14 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
aef0f74616 refactor: 简化空闲状态检测逻辑,直接显示保护状态
移除空闲状态检测及相关UI显示,直接显示"保护中"状态
2025-12-27 22:01:35 +08:00
886b460d5e fix: 添加更多caffeinate参数以防止系统睡眠
添加-i和-s参数以全面防止系统睡眠,包括用户不活动和系统睡眠的情况
2025-12-27 21:52:39 +08:00
27e98a527f feat(assets): 添加应用图标资源文件
添加不同尺寸的Mac应用图标文件并更新Contents.json配置
2025-12-27 21:32:33 +08:00
230a2836a6 chore: 删除临时目录中的Applications符号链接 2025-12-27 20:21:17 +08:00
3514f450c9 refactor: 移除菜单内容中多余的过渡动画效果 2025-12-27 20:17:03 +08:00
6b645dedf0 refactor: 移除指引窗口的动画效果以简化代码 2025-12-27 20:15:27 +08:00
3cb14be4bc feat(UI): 添加动画效果和优化界面布局
为菜单内容和指引界面添加平滑的动画过渡效果
优化权限管理区域的布局和视觉层次
调整窗口打开/关闭时的渐变动画
2025-12-27 20:09:29 +08:00
be65e275b3 feat(权限): 添加权限检查和管理功能
添加权限检查功能,包括自动启动和系统命令执行权限
在菜单中添加打开系统设置和登录项管理的按钮
2025-12-27 19:57:52 +08:00
21bb3131a0 chore: 清理构建脚本中的用户数据和缓存
删除xcuserdata相关文件并更新构建脚本,增加清理Xcode缓存和用户数据的步骤
2025-12-27 19:52:04 +08:00
693e4966d4 feat: 添加打包脚本并优化指引窗口管理
添加 build_and_package.sh 脚本用于自动化构建和打包
在 MenuContent 中增加对指引窗口的引用管理,避免重复创建
更新 .gitignore 忽略构建产物
添加 ExportOptions.plist 文件用于构建配置
2025-12-27 19:43:00 +08:00
37d73db4e5 feat: 优化应用启动和指引窗口逻辑
添加 LSUIElement 配置使应用不显示在 Dock 中
重构指引窗口显示逻辑,改为直接打开新窗口
移除自动显示的主窗口,改为按需打开
更新菜单图标和背景颜色以匹配系统风格
2025-12-27 19:35:30 +08:00
20 changed files with 501 additions and 115 deletions

3
.gitignore vendored
View File

@@ -94,5 +94,6 @@ Temporary Items
# PowerBar specific # PowerBar specific
PowerBar.xcarchive/ PowerBar.xcarchive/
ExportedApp/ ExportedApp/
PowerBar.app
PowerBar-*.dmg
PowerBar.dmg PowerBar.dmg
ExportOptions.plist

20
ExportOptions.plist Normal file
View 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>&lt;none&gt;</string>
</dict>
</plist>

View File

@@ -250,6 +250,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@@ -259,12 +260,13 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly; ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(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;
@@ -282,6 +284,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@@ -291,12 +294,13 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly; ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(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

@@ -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>

View File

@@ -2,6 +2,8 @@ import Foundation
import Combine import Combine
import IOKit.pwr_mgt import IOKit.pwr_mgt
import ServiceManagement import ServiceManagement
import AppKit
import UserNotifications
// MARK: - App State Manager // MARK: - App State Manager
class AppState: ObservableObject { class AppState: ObservableObject {
@@ -10,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
@@ -36,52 +39,11 @@ class AppState: ObservableObject {
func toggleFunction() { func toggleFunction() {
isFunctionEnabled.toggle() isFunctionEnabled.toggle()
if isFunctionEnabled { if isFunctionEnabled {
// // caffeinate
performCheck()
startMonitoringTimer()
} else {
//
stopMonitoringTimer()
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() startCaffeinate()
} } else {
// caffeinate
stopCaffeinate()
} }
} }
@@ -124,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()
@@ -154,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
@@ -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 // MARK: - Deinitializer
deinit { deinit {
// App 退 // App 退
stopMonitoringTimer()
stopCaffeinate() stopCaffeinate()
displayCountObserver?.cancel()
} }
} }

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -1,4 +1,6 @@
import SwiftUI import SwiftUI
import AppKit
import UserNotifications
@main @main
struct PowerBarApp: App { struct PowerBarApp: App {
@@ -25,24 +27,38 @@ struct PowerBarApp: App {
.foregroundColor(appState.isFunctionEnabled ? .green : .gray) .foregroundColor(appState.isFunctionEnabled ? .green : .gray)
} }
} }
// .onChange(of: ScenePhase.active) {
WindowGroup { //
if !hasShownGuide { UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
GuideView() NotificationDelegate.shared.appState = appState
.onDisappear {
hasShownGuide = true
} }
} }
} }
.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 { struct MenuContent: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
//
@State private var guideWindow: NSWindow?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -64,12 +80,15 @@ struct MenuContent: View {
// //
Button(action: { Button(action: {
// //
withAnimation(.easeInOut(duration: 0.3)) {
appState.toggleFunction() appState.toggleFunction()
}
}) { }) {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: appState.isFunctionEnabled ? "stop.circle.fill" : "play.circle.fill") Image(systemName: appState.isFunctionEnabled ? "stop.circle.fill" : "play.circle.fill")
.foregroundColor(appState.isFunctionEnabled ? .red : .green) .foregroundColor(appState.isFunctionEnabled ? .red : .green)
.font(.title2) .font(.title2)
.transition(.scale(scale: 0.8, anchor: .center))
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(appState.isFunctionEnabled ? "关闭常亮" : "开启常亮") Text(appState.isFunctionEnabled ? "关闭常亮" : "开启常亮")
.font(.body) .font(.body)
@@ -83,6 +102,7 @@ struct MenuContent: View {
.background( .background(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(appState.isFunctionEnabled ? Color.red.opacity(0.1) : Color.green.opacity(0.1)) .fill(appState.isFunctionEnabled ? Color.red.opacity(0.1) : Color.green.opacity(0.1))
.transition(.opacity)
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -98,17 +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)
Text(appState.isUserIdle ? "空闲" : "活动") Text("保护")
.font(.body) .font(.body)
// caffeinate
if !appState.isUserIdle {
Text("(保护中)")
.font(.caption2)
.foregroundColor(.secondary)
}
} }
} else { } else {
HStack(spacing: 6) { HStack(spacing: 6) {
@@ -121,6 +134,7 @@ struct MenuContent: View {
} }
Spacer() Spacer()
} }
} }
.padding(8) .padding(8)
.background( .background(
@@ -130,36 +144,94 @@ struct MenuContent: View {
Divider() 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( Toggle(isOn: Binding(
get: { appState.isLaunchAtLogin }, get: { appState.isLaunchAtLogin },
set: { _ in appState.toggleLaunchAtLogin() } set: { _ in appState.toggleLaunchAtLogin() }
)) { )) {
HStack { HStack {
Image(systemName: "power.on.circle") Image(systemName: "power.circle")
.frame(width: 20) .frame(width: 20)
Text("开机自动启动") Text("开机自动启动")
} }
} }
.toggleStyle(.checkbox) .toggleStyle(.checkbox)
.font(.caption) .font(.caption)
.padding(4) .padding(8)
Divider() Divider()
// //
Button("查看使用指南") { Button("查看使用指南") {
// //
// 使 AppStorage openGuideWindow()
UserDefaults.standard.set(false, forKey: "hasShownGuide")
//
// macOS
NSAlert.showAlert(title: "使用指南", message: "请重启应用以查看使用指南,或在下次启动时自动显示。")
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(4) .padding(8)
Divider() Divider()
@@ -170,16 +242,47 @@ struct MenuContent: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(4) .padding(8)
} }
.padding(12) .padding(12)
.frame(minWidth: 280) .frame(minWidth: 280)
.background( .background(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.white) .fill(Color(.windowBackgroundColor))
.shadow(radius: 4) .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便 // NSAlert便
@@ -231,7 +334,7 @@ struct GuideView: View {
} }
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "power.on.circle") Image(systemName: "power.circle")
.font(.title) .font(.title)
.foregroundColor(.orange) .foregroundColor(.orange)
.frame(width: 40) .frame(width: 40)
@@ -338,13 +441,15 @@ struct GuideView: View {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(.blue) .fill(.blue)
) )
.transition(.scale(scale: 0.9, anchor: .center))
.animation(.easeInOut(duration: 0.2), value: UUID())
} }
.padding(.bottom, 20) .padding(.bottom, 20)
} }
.frame(width: 400, height: 500) .frame(width: 400, height: 500)
.background( .background(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.white) .fill(Color(.windowBackgroundColor))
) )
.shadow(radius: 10) .shadow(radius: 10)
} }

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 "=== 构建脚本执行结束 ==="

83
build_and_package.sh Executable file
View 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"