From 6a74e8e739c3d2db2db737b1ecb0117eb88a4129 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 18 May 2026 16:07:07 +0800
Subject: [PATCH] feat: show background activity status

---
 VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte |   25 +++++++++++-
 TODO.md                                                             |    9 ++++
 VoiceSnapGo/internal/permissions/permissions.go                     |    1 
 VoiceSnapGo/internal/permissions/permissions_other.go               |    1 
 VoiceSnapGo/app.go                                                  |    2 
 VoiceSnapGo/build/darwin/Info.plist                                 |    2 
 VoiceSnapGo/frontend/src/lib/i18n/zh.json                           |    7 +++
 VoiceSnapGo/frontend/src/lib/i18n/en.json                           |    7 +++
 VoiceSnapGo/internal/permissions/permissions_darwin.go              |   23 +++++++++++
 9 files changed, 71 insertions(+), 6 deletions(-)

diff --git a/TODO.md b/TODO.md
index a411d06..2621835 100644
--- a/TODO.md
+++ b/TODO.md
@@ -17,6 +17,15 @@
 
 ## Done
 
+- [2026-05-18] 将 macOS 后台活动/登录项纳入权限检查面板。
+  - 说明: 系统设置里的 “VoiceSnap Local” 是本地稳定签名证书的开发者名,下面挂着 VoiceSnap 的登录项/后台活动。
+  - 它不是右 Alt、录音、自动上屏的必需隐私权限,但会影响登录后是否自动在后台运行。
+  - 面板新增“后台活动 / 登录项”,标为“可选”,不影响核心权限总绿灯。
+  - build: `20260518.1604`
+  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
+  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-build20260518.1604-arm64-local.dmg`
+  - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-arm64-local.dmg` 已替换为同一份新包。
+  - 已安装到 `/Applications/VoiceSnap.app`;启动日志验证 build `20260518.1604`。
 - [2026-05-18] 修复右 Alt 事件进入但被 macOS 物理状态查询误判为未按下的问题。
   - 日志显示 R-Alt `flagsChanged` 事件正常进入,`flags=0x80140`,但 `physicalDown=false` 导致 `pollDown=false`。
   - 修复: 按事件 keyCode 区分左右 Alt,并用该事件的 modifier flags 判定按下/释放。
diff --git a/VoiceSnapGo/app.go b/VoiceSnapGo/app.go
index fa35b92..e33d529 100755
--- a/VoiceSnapGo/app.go
+++ b/VoiceSnapGo/app.go
@@ -27,7 +27,7 @@
 
 const (
 	appVersion        = "2.1.1"
-	appBuild          = "20260518.1554"
+	appBuild          = "20260518.1604"
 	appDisplayVersion = appVersion + " (build " + appBuild + ")"
 	appName           = "VoiceSnap"
 
diff --git a/VoiceSnapGo/build/darwin/Info.plist b/VoiceSnapGo/build/darwin/Info.plist
index d543e73..e55ed86 100755
--- a/VoiceSnapGo/build/darwin/Info.plist
+++ b/VoiceSnapGo/build/darwin/Info.plist
@@ -17,7 +17,7 @@
     <key>CFBundleShortVersionString</key>
     <string>2.1.1</string>
     <key>CFBundleVersion</key>
-    <string>20260518.1554</string>
+    <string>20260518.1604</string>
     <key>LSMinimumSystemVersion</key>
     <string>11.0</string>
     <key>NSMicrophoneUsageDescription</key>
diff --git a/VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte b/VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte
index 821bcf9..57f3850 100644
--- a/VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte
+++ b/VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte
@@ -10,7 +10,7 @@
     required: boolean
   }
 
-  const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring']
+  const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring', 'backgroundActivity']
 
   let statuses = $state<PermissionStatus[]>([])
   let loading = $state(true)
@@ -56,13 +56,15 @@
   }
 
   function stateText(item: PermissionStatus) {
+    if (!item.required) return item.granted ? t('permissions.enabled') : t('permissions.disabled')
     if (item.granted) return t('permissions.granted')
     if (loading) return t('permissions.checking')
     return t('permissions.missing')
   }
 
   function allGranted() {
-    return statuses.length > 0 && statuses.every(item => item.granted)
+    const required = statuses.filter(item => item.required)
+    return required.length > 0 && required.every(item => item.granted)
   }
 </script>
 
@@ -95,7 +97,12 @@
         </div>
         <div class="permission-info">
           <div class="permission-heading">
-            <span class="permission-title">{titleFor(item.id)}</span>
+            <span class="permission-title">
+              {titleFor(item.id)}
+              {#if !item.required}
+                <span class="optional-badge">{t('permissions.optional')}</span>
+              {/if}
+            </span>
             <span class="permission-state" class:granted={item.granted}>{stateText(item)}</span>
           </div>
           <p class="permission-desc">{descFor(item.id)}</p>
@@ -250,10 +257,22 @@
   }
 
   .permission-title {
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
     font-size: var(--font-size-lg);
     font-weight: 650;
   }
 
+  .optional-badge {
+    border-radius: 999px;
+    background: var(--color-bg-secondary);
+    color: var(--color-secondary-label);
+    font-size: var(--font-size-xs);
+    font-weight: 600;
+    padding: 2px 7px;
+  }
+
   .permission-state {
     color: var(--color-red);
     font-size: var(--font-size-sm);
diff --git a/VoiceSnapGo/frontend/src/lib/i18n/en.json b/VoiceSnapGo/frontend/src/lib/i18n/en.json
index 96aa055..4527cd3 100755
--- a/VoiceSnapGo/frontend/src/lib/i18n/en.json
+++ b/VoiceSnapGo/frontend/src/lib/i18n/en.json
@@ -54,6 +54,9 @@
     "grant": "Grant",
     "granted": "Granted",
     "missing": "Missing",
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "optional": "Optional",
     "checking": "Checking",
     "allGranted": "All permissions are ready",
     "needsAttention": "Some permissions need attention",
@@ -69,6 +72,10 @@
     "inputMonitoring": {
       "title": "Input Monitoring",
       "desc": "Listens for the global right Alt hotkey while other apps are in front."
+    },
+    "backgroundActivity": {
+      "title": "Background Activity / Login Item",
+      "desc": "Starts VoiceSnap after login and allows it to run as a menu bar app in the background; it does not affect the currently running voice input."
     }
   },
   "hotkeys": {
diff --git a/VoiceSnapGo/frontend/src/lib/i18n/zh.json b/VoiceSnapGo/frontend/src/lib/i18n/zh.json
index c15a30a..8a52d67 100755
--- a/VoiceSnapGo/frontend/src/lib/i18n/zh.json
+++ b/VoiceSnapGo/frontend/src/lib/i18n/zh.json
@@ -54,6 +54,9 @@
     "grant": "去授权",
     "granted": "已授权",
     "missing": "未授权",
+    "enabled": "已启用",
+    "disabled": "未启用",
+    "optional": "可选",
     "checking": "检查中",
     "allGranted": "所有权限已就绪",
     "needsAttention": "有权限需要处理",
@@ -69,6 +72,10 @@
     "inputMonitoring": {
       "title": "输入监控",
       "desc": "用于在 VoiceSnap 窗口不可见、其他 App 正在前台时监听右 Alt 全局热键。"
+    },
+    "backgroundActivity": {
+      "title": "后台活动 / 登录项",
+      "desc": "用于登录系统后自动启动 VoiceSnap,并允许它作为菜单栏应用在后台运行;不影响当前已经打开的语音输入。"
     }
   },
   "hotkeys": {
diff --git a/VoiceSnapGo/internal/permissions/permissions.go b/VoiceSnapGo/internal/permissions/permissions.go
index e9cf138..dfee258 100644
--- a/VoiceSnapGo/internal/permissions/permissions.go
+++ b/VoiceSnapGo/internal/permissions/permissions.go
@@ -12,4 +12,5 @@
 	IDMicrophone      = "microphone"
 	IDAccessibility   = "accessibility"
 	IDInputMonitoring = "inputMonitoring"
+	IDBackground      = "backgroundActivity"
 )
diff --git a/VoiceSnapGo/internal/permissions/permissions_darwin.go b/VoiceSnapGo/internal/permissions/permissions_darwin.go
index 6879a75..8a66fc2 100644
--- a/VoiceSnapGo/internal/permissions/permissions_darwin.go
+++ b/VoiceSnapGo/internal/permissions/permissions_darwin.go
@@ -42,7 +42,10 @@
 */
 import "C"
 
-import "os/exec"
+import (
+	"os/exec"
+	"voicesnap/internal/startup"
+)
 
 // Statuses returns the current macOS privacy permission state.
 func Statuses() []Status {
@@ -65,6 +68,12 @@
 			State:    grantedState(C.inputMonitoringGranted() != 0),
 			Required: true,
 		},
+		{
+			ID:       IDBackground,
+			Granted:  startup.IsEnabled(),
+			State:    enabledState(startup.IsEnabled()),
+			Required: false,
+		},
 	}
 }
 
@@ -80,6 +89,9 @@
 		openSettings(id)
 	case IDInputMonitoring:
 		C.requestInputMonitoring()
+		openSettings(id)
+	case IDBackground:
+		_ = startup.SetEnabled(true)
 		openSettings(id)
 	}
 }
@@ -110,6 +122,13 @@
 	return "denied"
 }
 
+func enabledState(enabled bool) string {
+	if enabled {
+		return "enabled"
+	}
+	return "disabled"
+}
+
 func openSettings(id string) {
 	url := "x-apple.systempreferences:com.apple.preference.security"
 	switch id {
@@ -119,6 +138,8 @@
 		url = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
 	case IDInputMonitoring:
 		url = "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
+	case IDBackground:
+		url = "x-apple.systempreferences:com.apple.LoginItems-Settings.extension"
 	}
 	_ = exec.Command("open", url).Start()
 }
diff --git a/VoiceSnapGo/internal/permissions/permissions_other.go b/VoiceSnapGo/internal/permissions/permissions_other.go
index ccb9e70..db31980 100644
--- a/VoiceSnapGo/internal/permissions/permissions_other.go
+++ b/VoiceSnapGo/internal/permissions/permissions_other.go
@@ -7,6 +7,7 @@
 		{ID: IDMicrophone, Granted: true, State: "granted", Required: true},
 		{ID: IDAccessibility, Granted: true, State: "granted", Required: false},
 		{ID: IDInputMonitoring, Granted: true, State: "granted", Required: false},
+		{ID: IDBackground, Granted: true, State: "enabled", Required: false},
 	}
 }
 

--
Gitblit v1.9.3