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>
177 lines
9.2 KiB
Markdown
177 lines
9.2 KiB
Markdown
# 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 一字串包辦:
|
||
|
||
```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` 讀取。
|