feat: 添加开机自启动功能和用户指引界面
- 新增开机自启动功能,使用SMAppService实现 - 添加用户首次使用指引界面,包含功能介绍和权限说明 - 更新菜单栏选项,增加自启动开关和查看指南按钮 - 添加应用权限配置文件PowerBar.entitlements - 创建README文档和.gitignore文件
This commit is contained in:
98
.gitignore
vendored
Normal file
98
.gitignore
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
# gitignore contributors: remember to update Global/Xcode.gitignore, too
|
||||||
|
|
||||||
|
## Build generated
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
## Various settings
|
||||||
|
*.pbxproj
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
*.xcuserstate
|
||||||
|
*.xcscmblueprint
|
||||||
|
*.xccheckout
|
||||||
|
|
||||||
|
## Other
|
||||||
|
*.moved-aside
|
||||||
|
*.xcuserstate
|
||||||
|
*.xcscmblueprint
|
||||||
|
|
||||||
|
## Obj-C/Swift specific
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
## CocoaPods
|
||||||
|
Pods/
|
||||||
|
Podfile.lock
|
||||||
|
|
||||||
|
## Carthage
|
||||||
|
Carthage/Build/
|
||||||
|
|
||||||
|
## fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
|
||||||
|
## Bundle artifact
|
||||||
|
*.jsbundle
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
*.framework
|
||||||
|
*.a
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
## App Store submission
|
||||||
|
*.ipa
|
||||||
|
*.app.dSYM.zip
|
||||||
|
*.app.dSYM
|
||||||
|
|
||||||
|
## Local testing
|
||||||
|
*.xcodeproj/xcshareddata/xcschemes/
|
||||||
|
*.xcodeproj/project.xcworkspace/xcuserdata/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must be in root
|
||||||
|
!Icon?
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# PowerBar specific
|
||||||
|
PowerBar.xcarchive/
|
||||||
|
ExportedApp/
|
||||||
|
PowerBar.dmg
|
||||||
|
ExportOptions.plist
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import IOKit.pwr_mgt
|
import IOKit.pwr_mgt
|
||||||
|
import ServiceManagement
|
||||||
|
|
||||||
// MARK: - App State Manager
|
// MARK: - App State Manager
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
@@ -12,6 +13,9 @@ class AppState: ObservableObject {
|
|||||||
// 用户当前是否空闲
|
// 用户当前是否空闲
|
||||||
@Published var isUserIdle: Bool = false
|
@Published var isUserIdle: Bool = false
|
||||||
|
|
||||||
|
// 自动启动设置
|
||||||
|
@Published var isLaunchAtLogin: Bool = false
|
||||||
|
|
||||||
// 内部属性
|
// 内部属性
|
||||||
private var monitorTimer: Timer?
|
private var monitorTimer: Timer?
|
||||||
private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现
|
private var caffeinateProcess: Process? // 直接使用Process对象,但简化实现
|
||||||
@@ -20,6 +24,12 @@ class AppState: ObservableObject {
|
|||||||
private let idleThreshold: TimeInterval = 300 // 5分钟无操作视为空闲
|
private let idleThreshold: TimeInterval = 300 // 5分钟无操作视为空闲
|
||||||
private let checkInterval: TimeInterval = 10 // 每10秒检查一次
|
private let checkInterval: TimeInterval = 10 // 每10秒检查一次
|
||||||
|
|
||||||
|
// 初始化方法
|
||||||
|
init() {
|
||||||
|
// 检查并设置初始的自动启动状态
|
||||||
|
checkLaunchAtLoginStatus()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
|
|
||||||
/// 切换主功能的开关
|
/// 切换主功能的开关
|
||||||
@@ -153,6 +163,34 @@ class AppState: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Launch at Login Management
|
||||||
|
|
||||||
|
/// 检查当前的自动启动状态
|
||||||
|
private func checkLaunchAtLoginStatus() {
|
||||||
|
// 使用 Bundle.main.bundleIdentifier 获取应用的 bundle ID
|
||||||
|
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
|
||||||
|
isLaunchAtLogin = SMAppService.mainApp.status == .enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换自动启动状态
|
||||||
|
func toggleLaunchAtLogin() {
|
||||||
|
isLaunchAtLogin.toggle()
|
||||||
|
|
||||||
|
do {
|
||||||
|
if isLaunchAtLogin {
|
||||||
|
try SMAppService.mainApp.register()
|
||||||
|
print("已设置开机自动启动")
|
||||||
|
} else {
|
||||||
|
try SMAppService.mainApp.unregister()
|
||||||
|
print("已取消开机自动启动")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("设置开机自动启动失败: \(error)")
|
||||||
|
// 恢复原来的状态
|
||||||
|
isLaunchAtLogin.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Deinitializer
|
// MARK: - Deinitializer
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|||||||
24
PowerBar/PowerBar.entitlements
Normal file
24
PowerBar/PowerBar.entitlements
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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>com.apple.security.automation.apple-events</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- 允许应用程序访问IOKit服务 -->
|
||||||
|
<key>com.apple.security.iokit-user-client-class</key>
|
||||||
|
<array>
|
||||||
|
<string>IOPMPowerSource</string>
|
||||||
|
<string>IOHIDSystem</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<!-- 允许应用程序执行系统命令(caffeinate) -->
|
||||||
|
<key>com.apple.security.shell</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- 允许应用程序访问文件系统的某些部分 -->
|
||||||
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -4,6 +4,8 @@ import SwiftUI
|
|||||||
struct PowerBarApp: App {
|
struct PowerBarApp: App {
|
||||||
// 创建一个状态对象来管理 App 的核心逻辑
|
// 创建一个状态对象来管理 App 的核心逻辑
|
||||||
@StateObject private var appState = AppState()
|
@StateObject private var appState = AppState()
|
||||||
|
// 跟踪是否显示指引界面
|
||||||
|
@AppStorage("hasShownGuide") private var hasShownGuide = false
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
// 使用 MenuBarExtra 创建一个状态栏应用
|
// 使用 MenuBarExtra 创建一个状态栏应用
|
||||||
@@ -23,7 +25,18 @@ struct PowerBarApp: App {
|
|||||||
.foregroundColor(appState.isFunctionEnabled ? .green : .gray)
|
.foregroundColor(appState.isFunctionEnabled ? .green : .gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 移除 setting,因为我们不需要设置窗口
|
// 添加主窗口用于显示指引界面
|
||||||
|
WindowGroup {
|
||||||
|
if !hasShownGuide {
|
||||||
|
GuideView()
|
||||||
|
.onDisappear {
|
||||||
|
hasShownGuide = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.windowStyle(.hiddenTitleBar)
|
||||||
|
.defaultSize(width: 400, height: 400)
|
||||||
|
.windowResizability(.contentSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +130,39 @@ struct MenuContent: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
// 自动启动选项
|
||||||
|
Toggle(isOn: Binding(
|
||||||
|
get: { appState.isLaunchAtLogin },
|
||||||
|
set: { _ in appState.toggleLaunchAtLogin() }
|
||||||
|
)) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "power.on.circle")
|
||||||
|
.frame(width: 20)
|
||||||
|
Text("开机自动启动")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggleStyle(.checkbox)
|
||||||
|
.font(.caption)
|
||||||
|
.padding(4)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// 显示指引界面选项
|
||||||
|
Button("查看使用指南") {
|
||||||
|
// 这里需要实现显示指引界面的逻辑
|
||||||
|
// 我们可以使用 AppStorage 来控制指引界面的显示
|
||||||
|
UserDefaults.standard.set(false, forKey: "hasShownGuide")
|
||||||
|
// 重启应用或打开新窗口显示指引界面
|
||||||
|
// 由于 macOS 应用的限制,我们可以提示用户重新启动应用查看指引
|
||||||
|
NSAlert.showAlert(title: "使用指南", message: "请重启应用以查看使用指南,或在下次启动时自动显示。")
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(4)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
// 退出应用
|
// 退出应用
|
||||||
Button("退出 PowerBar") {
|
Button("退出 PowerBar") {
|
||||||
NSApp.terminate(nil)
|
NSApp.terminate(nil)
|
||||||
@@ -135,3 +181,171 @@ struct MenuContent: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扩展 NSAlert,添加一个便捷方法来显示警报
|
||||||
|
extension NSAlert {
|
||||||
|
static func showAlert(title: String, message: String) {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = title
|
||||||
|
alert.informativeText = message
|
||||||
|
alert.addButton(withTitle: "OK")
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户指引界面
|
||||||
|
struct GuideView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// 标题
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: "power")
|
||||||
|
.font(.system(size: 48))
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
Text("欢迎使用 PowerBar")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Text("智能睡眠管理工具")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.top, 20)
|
||||||
|
|
||||||
|
// 功能介绍
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
// 功能说明
|
||||||
|
Section {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "sun.max.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.frame(width: 40)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("智能睡眠管理")
|
||||||
|
.font(.headline)
|
||||||
|
Text("根据您的活动状态自动控制屏幕睡眠,活动时保持屏幕常亮,空闲时允许系统睡眠。")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "power.on.circle")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.frame(width: 40)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("开机自动启动")
|
||||||
|
.font(.headline)
|
||||||
|
Text("可以选择在开机时自动启动应用,无需手动打开。")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限说明
|
||||||
|
Section {
|
||||||
|
Text("所需权限")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("1. **自动启动权限**:用于实现开机自动启动功能")
|
||||||
|
Text("2. **系统空闲时间访问**:用于检测您的活动状态")
|
||||||
|
Text("3. **系统命令执行**:用于运行 caffeinate 命令阻止系统睡眠")
|
||||||
|
}
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指引
|
||||||
|
Section {
|
||||||
|
Text("使用指引")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("1.")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.frame(width: 20, alignment: .leading)
|
||||||
|
Text("点击菜单栏中的太阳图标打开菜单")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("2.")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.frame(width: 20, alignment: .leading)
|
||||||
|
Text("点击\"开启常亮\"按钮启动智能睡眠管理")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("3.")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.frame(width: 20, alignment: .leading)
|
||||||
|
Text("应用会根据您的活动状态自动调整睡眠设置")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("4.")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.frame(width: 20, alignment: .leading)
|
||||||
|
Text("可以选择\"开机自动启动\"方便长期使用")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态说明
|
||||||
|
Section {
|
||||||
|
Text("状态说明")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "sun.max.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("绿色太阳:功能已开启,系统处于活动状态")
|
||||||
|
}
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "sun.max.fill")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("橙色太阳:功能已开启,系统处于空闲状态")
|
||||||
|
}
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "sun.max")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Text("灰色太阳:功能已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
Button(action: {
|
||||||
|
// 关闭指引界面
|
||||||
|
NSApp.keyWindow?.close()
|
||||||
|
}) {
|
||||||
|
Text("开始使用")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.blue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
.frame(width: 400, height: 500)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color.white)
|
||||||
|
)
|
||||||
|
.shadow(radius: 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
58
README.md
Normal file
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# PowerBar
|
||||||
|
|
||||||
|
一个智能的macOS菜单栏应用,根据用户活动状态自动管理系统睡眠。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📊 **智能睡眠管理**:根据用户活动状态自动控制系统睡眠
|
||||||
|
- 🔍 **空闲检测**:5分钟无操作自动视为空闲,停止阻止睡眠
|
||||||
|
- 🎯 **清晰的状态指示**:通过菜单栏图标直观显示当前状态
|
||||||
|
- 🎨 **现代化UI**:简洁美观的菜单界面,易于操作
|
||||||
|
- ⚡ **低资源消耗**:轻量级设计,几乎不占用系统资源
|
||||||
|
|
||||||
|
## 安装方法
|
||||||
|
|
||||||
|
### 方法1:直接运行
|
||||||
|
|
||||||
|
1. 下载并解压应用包
|
||||||
|
2. 双击 `PowerBar.app` 即可运行
|
||||||
|
|
||||||
|
### 方法2:通过DMG安装
|
||||||
|
|
||||||
|
1. 下载 `PowerBar.dmg` 文件
|
||||||
|
2. 双击打开DMG文件
|
||||||
|
3. 将 `PowerBar.app` 拖放到 `Applications` 文件夹
|
||||||
|
4. 从Launchpad或Applications文件夹启动应用
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
1. **启动应用**:双击应用图标,菜单栏会出现太阳图标
|
||||||
|
2. **查看状态**:点击太阳图标查看当前状态
|
||||||
|
3. **切换功能**:点击"开启常亮"或"关闭常亮"按钮切换功能
|
||||||
|
4. **退出应用**:点击"退出 PowerBar"退出应用
|
||||||
|
|
||||||
|
## 状态说明
|
||||||
|
|
||||||
|
- 🟢 **绿色太阳**:功能已开启,系统处于活动状态,显示屏受到保护
|
||||||
|
- 🟡 **橙色太阳**:功能已开启,系统处于空闲状态
|
||||||
|
- ⚪ **灰色太阳**:功能已关闭
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
- 基于SwiftUI开发
|
||||||
|
- 使用IOKit获取系统空闲时间
|
||||||
|
- 使用caffeinate命令管理系统睡眠
|
||||||
|
- 支持arm64和x86_64架构
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- macOS 13.0+ (Ventura)
|
||||||
|
- Apple Silicon 或 Intel 处理器
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 作者
|
||||||
|
|
||||||
|
Leo12025
|
||||||
Reference in New Issue
Block a user