local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.2 KiB
06 — 跨平台 UX 差異處理
原則:視覺層統一、互動層遵循平台慣例。
6.1 Window Chrome(視窗標題列)
| 平台 | 策略 | 理由 |
|---|---|---|
| macOS | frameless + 顯示 traffic lights(紅綠黃)於左上 | 最原生的感覺;Wails 支援 TitleBar: { Hide: true } + 保留 traffic lights |
| Windows | 標準 title bar(系統提供) | 不自畫 close/minimize,避免 DPI / 深色模式坑 |
| Linux | 標準 title bar(GTK/Qt) | 同上,讓 WM 決定 |
標題列內容:App 名稱 visionA-local(所有平台統一)。macOS 的 frameless 沒有標題列文字,改把 page title 放在 header 區塊裡即可(見 03-wireframes 的 Header)。
最小化 / 最大化行為:全平台一致 — 都能最小化、最大化、調整視窗大小。最小視窗尺寸 960×640(保證 sidebar + content 不擠壓)。
6.2 快捷鍵
修飾鍵對照
| 動作 | macOS | Windows / Linux |
|---|---|---|
| Settings | ⌘ , | Ctrl + , |
| 結束 | ⌘ Q | Ctrl + Q(或 Alt + F4) |
| 關閉視窗(=結束程式) | ⌘ W | Ctrl + W |
| 切換主區塊(Dashboard/Models/Devices/Workspace) | ⌘ 1–4 | Ctrl + 1–4 |
| 重新整理裝置 | ⌘ Shift + R | Ctrl + Shift + R |
| 上傳模型 | ⌘ U | Ctrl + U |
⌘W 語意:因為決策 Q7 為「關閉視窗 = 結束程式」、且決策 Q-A 砍掉 tray,⌘W 與 ⌘Q 行為一致(關閉唯一主視窗即結束 App)。
第四輪 R4-6 決策變更:
⌘R→⌘Shift+R:原本⌘R會與 WebView 內建的「重新載入頁面」衝突(使用者一按可能誤觸前端整頁 reload),改為⌘Shift+R作為「重新掃描裝置」的明確動作,避免與瀏覽器反射行為打架。- 取消原
⌘Shift+W 前往 Workspace:此組合與 macOS 內建「關閉所有視窗」衝突,且⌘4已涵蓋「切到 Workspace」的需求,移除避免語意重疊。
實作方式:所有快捷鍵定義在 Wails 的原生 menu API,由 OS 派送;不要在前端用 keydown 監聽(會與輸入框衝突且跨平台不一致)。
衝突避免:
- 不佔用 ⌘ X/C/V/A/Z(輸入操作)
- 不佔用 ⌘ F(應該給頁面內搜尋用)
原生 App 選單(macOS 獨有)
macOS 需要一個原生 menu bar(因為無 window menu):
visionA-local | File | Edit | View | Devices | Help
- visionA-local:About、Preferences (⌘,)、Services、Hide、Quit
- File:新增裝置掃描、上傳模型、匯出結果
- Edit:Undo / Redo / Cut / Copy / Paste(標準 macOS)
- View:切到 Dashboard/Models/Devices/Workspace (⌘1-4)、重新載入
- Devices:重新掃描、切換模式、連線紀錄
- Help:說明文件、關於 visionA-local、回報問題(開外部瀏覽器)
Windows/Linux 不做原生 menu bar(使用者會覺得多餘),改放在 Settings / 頁面內。
6.3 檔案對話框
| 場景 | 實作 |
|---|---|
上傳 .nef 模型 |
Wails runtime.OpenFileDialog,filter *.nef |
| 選擇影片檔 | Wails runtime.OpenFileDialog,filter *.mp4,*.mov,*.avi |
| 選擇圖片批次 | Wails runtime.OpenMultipleFilesDialog,filter image/* |
| 匯出結果 | Wails runtime.SaveFileDialog |
| 「在 Finder/Explorer 顯示」 | 平台指令:open (mac) / explorer /select, (win) / xdg-open (linux) |
禁止:自畫 file picker、用 HTML <input type=file>(桌面 app 應該用原生)。
拖放:支援 drag-and-drop 到整個 Models / Workspace 頁面,由前端處理 drop event,Wails 會傳 file path 給 Go 端。
6.4 通知(第四輪 R4-8 定案)
策略原則:
- App 內 toast 處理一般資訊、裝置連/斷等前景事件(使用者必然在看著主視窗,不必打擾 OS 通知中心)
- OS 原生通知 只用於「App 可能不在前景、且必須立刻知道」的嚴重事件(主要是 Server 崩潰)
| 場景 | 通知方式 | 說明 |
|---|---|---|
| 裝置連接成功 | App 內 toast(success) | 關閉 = 結束程式,使用者必然在主視窗前,不需要 OS 通知 |
| 裝置斷線 | App 內 toast(warning) | 同上 |
| 模型上傳完成 | App 內 toast(success) | — |
| 模型載入失敗 | App 內 toast(destructive) | — |
| Server 崩潰 / 自動重啟 | OS 原生通知(嚴重) | 崩潰時前端可能也沒了,唯有 OS 通知可靠 |
| 推論結果(使用者要求時) | App 內 toast / timeline 更新 | — |
| 一般資訊(設定已儲存等) | App 內 toast | — |
實作方式(shell out,Wails 原生通知 API 有限):
- macOS:
osascript -e 'display notification "訊息" with title "visionA-local"' - Windows:PowerShell
New-BurntToastNotification -Text "訊息"(需內建 BurntToast 模組,若沒有則退回 msg box) - Linux:
notify-send "visionA-local" "訊息"
需 Architect在後端提供統一封裝,前端只需呼叫 /api/notify 或透過 Wails Go↔JS bridge 觸發。
6.9 Single-instance(單例)行為
情境:使用者二次雙擊 app 圖示、或從 Dock/Start menu 再次啟動 visiona-local,但 App 已在執行。
策略(與 Architect tray-and-lifecycle.md §2.3 對齊):靜默 raise,不彈 toast、不彈對話框。
| 動作 | 預期行為 |
|---|---|
| 第二個程序啟動 | 偵測到 lock file → 呼叫既有程序的 /ipc/raise → 既有程序 WindowShow(ctx) 把主視窗帶到最前並聚焦 → 第二個程序立即退出(exit 0) |
| 視覺回饋 | 無 toast、無對話框。使用者看到主視窗被帶到前景即可(這本身就是最直觀的回饋) |
| Dock click(macOS) | 由 Cocoa 自動處理,無需介入 |
/ipc/raise 失敗(例如 IPC port 讀不到) |
顯示 error modal「無法喚起既有的 visiona-local 視窗,請手動切換」+「確認」按鈕 → 第二個程序退出。絕對不覆寫 lock 另開新視窗(會出現兩個視窗,體驗極差) |
設計理由:
- 彈 toast「已有另一個 visionA-local 在執行中」是冗餘資訊——使用者會看到視窗浮起來,不需要再被告知一次
- 但 raise 失敗的錯誤 modal 必要,否則使用者會以為 app 壞了而反覆雙擊
與 i18n 的互動:error modal 的文案走 errors:singleInstance.raiseFailed,需要中英兩版。
6.5 字型
| 平台 | 字型 |
|---|---|
| macOS | -apple-system, "SF Pro Text", "PingFang TC" |
| Windows | "Segoe UI", "Microsoft JhengHei" |
| Linux | "Inter", "Noto Sans TC", "Ubuntu" |
CSS font-family 一字串包辦:
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang TC",
"Microsoft JhengHei", "Noto Sans TC", Inter, Ubuntu,
Roboto, sans-serif;
等寬字型(log viewer):
font-family:
ui-monospace, SFMono-Regular, "SF Mono", Menlo,
Consolas, "Cascadia Mono", monospace;
6.6 Tray icon 資產
見
已於第三輪決策 Q-A 砍除 tray(05-tray.md。05-tray.md 刪檔)。本版不做 tray,App 只透過主視窗與原生 menu bar(僅 macOS)互動。
6.7 資料目錄位置
| 平台 | 路徑 |
|---|---|
| macOS | ~/Library/Application Support/visiona-local/ |
| Windows | %APPDATA%\visiona-local\ |
| Linux | ~/.local/share/visiona-local/ |
UI 顯示:Settings > 一般 統一顯示為「資料目錄:[完整路徑]」並附「在 Finder/Explorer 顯示」按鈕,使用者不需要記平台差異。
第三輪決策 Q-E1 採 OS 慣例路徑;第四輪 R4-5 進一步決定目錄名稱全小寫(visiona-local 而非 visionA-local),理由:
- 對齊 Bundle ID
com.innovedus.visiona-local - 符合 Linux 檔案系統慣例(case-sensitive,且習慣全小寫)
- 避免 macOS/Windows(case-insensitive)與 Linux 產生不一致
- App 顯示名稱「visionA-local」不受影響,僅檔案系統層路徑全小寫
6.8 深色模式
統一方案(第四輪定案):純 CSS prefers-color-scheme media query + Tailwind dark: variant,不需要 Go/Wails 在系統主題變更時 emit event 給前端。
| 平台 | 偵測方式 |
|---|---|
| macOS | WebView 自動支援 prefers-color-scheme(Cocoa → WKWebView 會在系統切換 light/dark 時自動更新 media query 結果) |
| Windows 10/11 | WebView2(Edge Chromium)原生支援 prefers-color-scheme |
| Linux | WebKit2GTK 支援 prefers-color-scheme(需 GTK 系統主題正確設定) |
實作簡化理由:
- 三平台 WebView 都已原生支援
prefers-color-scheme的即時切換(使用者在系統設定切 light/dark 的瞬間,CSS media query 就會重新 match,前端 re-render 即可) - 不需要 Wails Go 端監聽系統主題事件再 emit 到前端(原本規劃這樣做是為了 fallback,但實測多餘)
- 前端只要用
@media (prefers-color-scheme: dark) { ... }或 Tailwinddark:variant,無需 JS 介入
不提供手動切換(決策 Q15)。Settings > 一般 只顯示唯讀的「主題:跟隨系統(目前:深色/淺色)」,其中「目前」狀態用 window.matchMedia('(prefers-color-scheme: dark)').matches 讀取。