Ariver
2026-05-18 8058eb48fe3fd1b37be974b5183df6ffc69bfb2c
fix: show overlay in fullscreen spaces
10 files modified
81 ■■■■ changed files
CHANGELOG.md 12 ●●●●● patch | view | raw | blame | history
TODO.md 9 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/app.go 4 ●●●● patch | view | raw | blame | history
VoiceSnapGo/build/config.yml 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/build/darwin/Info.plist 4 ●●●● patch | view | raw | blame | history
VoiceSnapGo/build/windows/wails.exe.manifest 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/package-lock.json 4 ●●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/package.json 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/internal/overlay/overlay_darwin.go 40 ●●●● patch | view | raw | blame | history
CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
## v2.1.2 (2026-05-18)
### Bug 修复
- **全屏窗口下悬浮指示器不显示**:macOS 悬浮指示器从普通 `NSWindow` 调整为非激活 `NSPanel`,窗口层级提升到状态栏级别,并补充 `Transient` / `IgnoresCycle` / `FullScreenAuxiliary` / `CanJoinAllSpaces` 行为,确保任意 App 全屏 Space 下也能显示录音胶囊。
### 构建
- build: `20260518.1623`
---
## v2.1.1 (2026-02-27)
### Bug 修复
TODO.md
@@ -21,6 +21,15 @@
## Done
- [2026-05-18] 修复全屏窗口下语音输入悬浮指示器不显示。
  - 根因: macOS 全屏 Space 下普通 `NSWindow` + `NSFloatingWindowLevel` 不稳定,容易被全屏 App 压在下层。
  - 修复: 悬浮指示器改为非激活 `NSPanel`,窗口层级提升到 `kCGStatusWindowLevelKey`,并补充 `Transient`、`IgnoresCycle`、`FullScreenAuxiliary`、`CanJoinAllSpaces` 行为。
  - 版本: `2.1.2`
  - build: `20260518.1623`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.2-build20260518.1623-arm64-local.dmg`
  - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.2-arm64-local.dmg`
  - 已安装到 `/Applications/VoiceSnap.app`;签名验证为 `VoiceSnap Local Code Signing`。
- [2026-05-18] 将 macOS 后台活动/登录项纳入权限检查面板。
  - 说明: 系统设置里的 “VoiceSnap Local” 是本地稳定签名证书的开发者名,下面挂着 VoiceSnap 的登录项/后台活动。
  - 它不是右 Alt、录音、自动上屏的必需隐私权限,但会影响登录后是否自动在后台运行。
VoiceSnapGo/app.go
@@ -26,8 +26,8 @@
)
const (
    appVersion        = "2.1.1"
    appBuild          = "20260518.1604"
    appVersion        = "2.1.2"
    appBuild          = "20260518.1623"
    appDisplayVersion = appVersion + " (build " + appBuild + ")"
    appName           = "VoiceSnap"
VoiceSnapGo/build/config.yml
@@ -7,7 +7,7 @@
  description: "Offline voice to text tool"
  copyright: "(c) 2025, VoiceSnap"
  comments: "Press and hold hotkey to record, release to transcribe"
  version: "2.1.1"
  version: "2.1.2"
dev_mode:
  root_path: .
VoiceSnapGo/build/darwin/Info.plist
@@ -15,9 +15,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>2.1.1</string>
    <string>2.1.2</string>
    <key>CFBundleVersion</key>
    <string>20260518.1604</string>
    <string>20260518.1623</string>
    <key>LSMinimumSystemVersion</key>
    <string>11.0</string>
    <key>NSMicrophoneUsageDescription</key>
VoiceSnapGo/build/windows/wails.exe.manifest
@@ -3,7 +3,7 @@
  <assemblyIdentity
    type="win32"
    name="VoiceSnap"
    version="2.1.1.0"
    version="2.1.2.0"
    processorArchitecture="*"/>
  <dependency>
    <dependentAssembly>
VoiceSnapGo/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
  "name": "voicesnap-frontend",
  "version": "2.1.1",
  "version": "2.1.2",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "voicesnap-frontend",
      "version": "2.1.1",
      "version": "2.1.2",
      "dependencies": {
        "@wailsio/runtime": "latest"
      },
VoiceSnapGo/frontend/package.json
@@ -1,7 +1,7 @@
{
  "name": "voicesnap-frontend",
  "private": true,
  "version": "2.1.1",
  "version": "2.1.2",
  "type": "module",
  "scripts": {
    "dev": "vite dev",
VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte
@@ -2,7 +2,7 @@
  import { Call } from '@wailsio/runtime'
  import { t } from '../../lib/i18n'
  let version = $state('2.1.1')
  let version = $state('2.1.2')
  let updateStatus = $state('')
  let updateColor = $state('')
  let checking = $state(false)
VoiceSnapGo/internal/overlay/overlay_darwin.go
@@ -4,8 +4,9 @@
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#cgo LDFLAGS: -framework Cocoa -framework ApplicationServices
#import <Cocoa/Cocoa.h>
#import <ApplicationServices/ApplicationServices.h>
#include <dispatch/dispatch.h>
#include <string.h>
@@ -118,8 +119,8 @@
@end
// ---- Custom NSWindow for drag tracking ----
@interface VoiceSnapOverlayWindow : NSWindow {
// ---- Custom NSPanel for drag tracking ----
@interface VoiceSnapOverlayWindow : NSPanel {
    NSPoint _dragStartOrigin;
}
@end
@@ -148,6 +149,7 @@
}
- (BOOL)canBecomeKeyWindow { return NO; }
- (BOOL)canBecomeMainWindow { return NO; }
@end
@@ -155,6 +157,21 @@
static NSWindow* g_window = nil;
static VoiceSnapOverlayView* g_view = nil;
static NSTimer* g_timer = nil;
static void applyOverlayWindowPolicy(NSWindow* window) {
    if (!window) return;
    [window setLevel:CGWindowLevelForKey(kCGStatusWindowLevelKey)];
    [window setCollectionBehavior:
        NSWindowCollectionBehaviorCanJoinAllSpaces |
        NSWindowCollectionBehaviorStationary |
        NSWindowCollectionBehaviorFullScreenAuxiliary |
        NSWindowCollectionBehaviorTransient |
        NSWindowCollectionBehaviorIgnoresCycle];
    [window setCanHide:NO];
    if ([window isKindOfClass:[NSPanel class]]) {
        [(NSPanel*)window setHidesOnDeactivate:NO];
    }
}
static void animateBars(void) {
    g_state.animTime += 0.033f;
@@ -230,6 +247,7 @@
    }
    if (g_state.fadeTarget > 0 && ![g_window isVisible]) {
        applyOverlayWindowPolicy(g_window);
        [g_window orderFrontRegardless];
    }
@@ -259,17 +277,13 @@
    VoiceSnapOverlayWindow* window = [[VoiceSnapOverlayWindow alloc]
        initWithContentRect:NSMakeRect(0, 0, CAP_W, CAP_H)
        styleMask:NSWindowStyleMaskBorderless
        styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
        backing:NSBackingStoreBuffered
        defer:NO];
    [window setOpaque:NO];
    [window setBackgroundColor:[NSColor clearColor]];
    [window setLevel:NSFloatingWindowLevel];
    [window setCollectionBehavior:
        NSWindowCollectionBehaviorCanJoinAllSpaces |
        NSWindowCollectionBehaviorStationary |
        NSWindowCollectionBehaviorFullScreenAuxiliary];
    applyOverlayWindowPolicy(window);
    [window setIgnoresMouseEvents:NO];
    [window setMovableByWindowBackground:YES];
    [window setHasShadow:NO];
@@ -307,11 +321,19 @@
    g_state.hasPosition = 1;
}
static void doBringOverlayForward(void* ctx) {
    (void)ctx;
    if (!g_window) return;
    applyOverlayWindowPolicy(g_window);
    [g_window orderFrontRegardless];
}
static void overlayShow(void) {
    g_state.fadeTarget = 1.0f;
    if (!g_state.hasPosition) {
        dispatch_async_f(dispatch_get_main_queue(), NULL, doAutoPosition);
    }
    dispatch_async_f(dispatch_get_main_queue(), NULL, doBringOverlayForward);
}
static void overlayHide(void) {