# 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 ``(桌面 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 一字串包辦: ```css font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang TC", "Microsoft JhengHei", "Noto Sans TC", Inter, Ubuntu, Roboto, sans-serif; ``` 等寬字型(log viewer): ```css font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Cascadia Mono", monospace; ``` ## 6.6 Tray icon 資產 ~~見 `05-tray.md`。~~ **已於第三輪決策 Q-A 砍除 tray**(`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) { ... }` 或 Tailwind `dark:` variant,無需 JS 介入 **不提供手動切換**(決策 Q15)。Settings > 一般 只顯示唯讀的「主題:跟隨系統(目前:深色/淺色)」,其中「目前」狀態用 `window.matchMedia('(prefers-color-scheme: dark)').matches` 讀取。