feat: show background activity status
| | |
| | | |
| | | ## 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 判定按下/释放。 |
| | |
| | | |
| | | const ( |
| | | appVersion = "2.1.1" |
| | | appBuild = "20260518.1554" |
| | | appBuild = "20260518.1604" |
| | | appDisplayVersion = appVersion + " (build " + appBuild + ")" |
| | | appName = "VoiceSnap" |
| | | |
| | |
| | | <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> |
| | |
| | | required: boolean |
| | | } |
| | | |
| | | const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring'] |
| | | const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring', 'backgroundActivity'] |
| | | |
| | | let statuses = $state<PermissionStatus[]>([]) |
| | | let loading = $state(true) |
| | |
| | | } |
| | | |
| | | 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> |
| | | |
| | |
| | | </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> |
| | |
| | | } |
| | | |
| | | .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); |
| | |
| | | "grant": "Grant", |
| | | "granted": "Granted", |
| | | "missing": "Missing", |
| | | "enabled": "Enabled", |
| | | "disabled": "Disabled", |
| | | "optional": "Optional", |
| | | "checking": "Checking", |
| | | "allGranted": "All permissions are ready", |
| | | "needsAttention": "Some permissions need attention", |
| | |
| | | "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": { |
| | |
| | | "grant": "去授权", |
| | | "granted": "已授权", |
| | | "missing": "未授权", |
| | | "enabled": "已启用", |
| | | "disabled": "未启用", |
| | | "optional": "可选", |
| | | "checking": "检查中", |
| | | "allGranted": "所有权限已就绪", |
| | | "needsAttention": "有权限需要处理", |
| | |
| | | "inputMonitoring": { |
| | | "title": "输入监控", |
| | | "desc": "用于在 VoiceSnap 窗口不可见、其他 App 正在前台时监听右 Alt 全局热键。" |
| | | }, |
| | | "backgroundActivity": { |
| | | "title": "后台活动 / 登录项", |
| | | "desc": "用于登录系统后自动启动 VoiceSnap,并允许它作为菜单栏应用在后台运行;不影响当前已经打开的语音输入。" |
| | | } |
| | | }, |
| | | "hotkeys": { |
| | |
| | | IDMicrophone = "microphone" |
| | | IDAccessibility = "accessibility" |
| | | IDInputMonitoring = "inputMonitoring" |
| | | IDBackground = "backgroundActivity" |
| | | ) |
| | |
| | | */ |
| | | import "C" |
| | | |
| | | import "os/exec" |
| | | import ( |
| | | "os/exec" |
| | | "voicesnap/internal/startup" |
| | | ) |
| | | |
| | | // Statuses returns the current macOS privacy permission state. |
| | | func Statuses() []Status { |
| | |
| | | State: grantedState(C.inputMonitoringGranted() != 0), |
| | | Required: true, |
| | | }, |
| | | { |
| | | ID: IDBackground, |
| | | Granted: startup.IsEnabled(), |
| | | State: enabledState(startup.IsEnabled()), |
| | | Required: false, |
| | | }, |
| | | } |
| | | } |
| | | |
| | |
| | | openSettings(id) |
| | | case IDInputMonitoring: |
| | | C.requestInputMonitoring() |
| | | openSettings(id) |
| | | case IDBackground: |
| | | _ = startup.SetEnabled(true) |
| | | openSettings(id) |
| | | } |
| | | } |
| | |
| | | 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 { |
| | |
| | | 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() |
| | | } |
| | |
| | | {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}, |
| | | } |
| | | } |
| | | |