Skip to content

Commit 54ac552

Browse files
feat(openspec): open change wviewer-v2-react-tauri-migration
1 parent a8aa9f0 commit 54ac552

File tree

18 files changed

+882
-0
lines changed

18 files changed

+882
-0
lines changed

.claude/settings.local.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(git ls-tree:*)",
5+
"WebSearch",
6+
"WebFetch(domain:github.com)",
7+
"WebFetch(domain:v2.tauri.app)",
8+
"WebFetch(domain:bun.com)",
9+
"WebFetch(domain:dev.to)",
10+
"Bash(openspec:*)",
11+
"Bash(openspec new:*)",
12+
"Bash(openspec status:*)",
13+
"Bash(openspec instructions:*)"
14+
]
15+
}
16+
}

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"openspec"
4+
]
5+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-03-27
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
## Context
2+
3+
WViewer v1.4.0 is a desktop image/video viewer built with Vue 3 + Quasar + Electron. It provides waterfall (masonry) layout for browsing images from local folders, full-screen image viewing with multiple viewer backends (photoswipe, bigger-picture, viewerjs), settings persistence via electron-store, and a frameless custom-titlebar window. The app supports English and Chinese, pagination, sorting (name/time asc/desc), drag-and-drop folder opening, and delete-to-trash.
4+
5+
The Electron main process handles filesystem traversal (sync and async with pagination), image dimension extraction, native dialogs, and IPC for store operations. The frontend uses Pinia for state, Vue Router for routing, and Quasar components for UI. The app uses a custom `atom://` protocol to serve local files to the renderer.
6+
7+
This is an open-source project targeting Windows, macOS, and Linux.
8+
9+
## Goals / Non-Goals
10+
11+
**Goals:**
12+
- Full feature parity with v1.4.0 on the new stack
13+
- Dramatically smaller binary size (Electron ~150MB → Tauri ~5-10MB)
14+
- Significantly faster directory traversal and image metadata extraction via Rust
15+
- Modern, maintainable React codebase with type-safe state and i18n
16+
- Professional CI/CD pipeline with cross-platform releases and auto-update
17+
- MUI-primary UI with shadcn/ui accents for a polished, dynamic look
18+
- Virtualized rendering for large image collections (thousands of images)
19+
20+
**Non-Goals:**
21+
- Mobile or web deployment (desktop only via Tauri)
22+
- Cloud storage or remote image sources (local filesystem only)
23+
- Image editing capabilities
24+
- Database-backed image indexing or search (may come later)
25+
- macOS App Store or Windows Store distribution (GitHub Releases only)
26+
- Video playback in the viewer (video format support was commented out in v1.4.0; defer to future)
27+
28+
## Decisions
29+
30+
### D1: Rust-native file engine over tauri-plugin-fs for traversal
31+
32+
**Decision:** Implement directory traversal, image metadata extraction, and file operations as Rust Tauri commands, not via tauri-plugin-fs from the frontend.
33+
34+
**Rationale:** The old app's Node.js traversal was a bottleneck — synchronous `fs.readdirSync` blocked the main process, and even the async version used `promisify(imageSize)` sequentially. Rust's `walkdir` crate with `rayon` for parallel image dimension reads can achieve 10-50x speedup. Tauri commands are invoked via `invoke()` from the frontend, keeping the API clean.
35+
36+
**Alternatives considered:**
37+
- *tauri-plugin-fs from frontend*: Simpler but requires many IPC round-trips for recursive traversal; image dimension extraction would need a separate solution (no `image-size` equivalent in JS without Node.js).
38+
- *Hybrid (Rust traversal + JS metadata)*: Splits logic across languages unnecessarily.
39+
40+
**Rust crates:**
41+
- `walkdir` — recursive directory traversal
42+
- `image` — image dimension extraction (header-only, fast)
43+
- `rayon` — parallel processing for large directories
44+
- `trash` — cross-platform delete-to-trash
45+
- `serde` / `serde_json` — serialization for Tauri commands
46+
- `natord` — natural sort order for filenames
47+
48+
### D2: Tauri asset protocol for local file serving
49+
50+
**Decision:** Use Tauri's built-in `asset://` protocol (via asset scope configuration) to serve local images to the webview, replacing Electron's custom `atom://` protocol.
51+
52+
**Rationale:** Tauri v2 provides `asset://localhost/<encoded-path>` with configurable scope permissions. This is the official, security-audited approach. No custom protocol registration needed.
53+
54+
**Configuration:** In `tauri.conf.json`, define an asset scope that permits access to user-selected directories. Use `tauri-plugin-persisted-scope` to remember permissions across sessions.
55+
56+
### D3: MUI as primary framework + shadcn/ui for dynamic accents
57+
58+
**Decision:** Use MUI (Material UI) as the primary component framework for layout, navigation, dialogs, and forms. Use shadcn/ui for select interactive components where its design adds visual dynamism (e.g., command palette, toast notifications, animated cards).
59+
60+
**Rationale:** MUI provides a comprehensive component set with built-in theming, accessibility, and a mature ecosystem. shadcn/ui adds modern, copy-paste components that are easy to customize with Tailwind CSS. Using both leverages MUI's structural strength and shadcn's visual flair.
61+
62+
**Integration:** Both use different styling approaches (MUI uses Emotion by default, shadcn uses Tailwind). To avoid conflicts:
63+
- Configure MUI with `@mui/material` + Tailwind CSS v4 coexistence (MUI supports `styled-engine` swapping, but the simplest path is letting both coexist — MUI for its components, Tailwind for custom styling and shadcn)
64+
- Use MUI's `CssBaseline` as the reset, with Tailwind's preflight disabled to avoid conflicts
65+
66+
### D4: Custom titlebar with MUI AppBar
67+
68+
**Decision:** Implement the frameless window titlebar using MUI's `AppBar` + `Toolbar` components with Tauri's `data-tauri-drag-region` attribute.
69+
70+
**Rationale:** The old app used Quasar's `q-bar` with menu dropdowns (File, Edit, View, Window, Help) and window controls (minimize, maximize, close). MUI's AppBar provides the same structure with better theming. Window control buttons invoke Tauri's `appWindow` API (`minimize()`, `toggleMaximize()`, `close()`).
71+
72+
### D5: Streaming pagination via Tauri events
73+
74+
**Decision:** The Rust file engine sends image batches to the frontend via Tauri events (similar to the old `async:imageLinks-append` IPC pattern), enabling progressive rendering as directories are scanned.
75+
76+
**Rationale:** The old `AsyncReadFilePath` class accumulated images in pages and sent them via `webContents.send()`. Tauri's event system (`app.emit()` / `listen()`) provides the same push-based pattern. The frontend appends batches to the Zustand store, and masonic's virtualized grid renders them incrementally.
77+
78+
**Flow:**
79+
```
80+
┌──────────────────────────────────────────────────────────┐
81+
│ │
82+
│ Frontend Rust Backend │
83+
│ │
84+
│ invoke("scan_directory", ──────▶ Start walkdir │
85+
│ { paths, formats, traversal │
86+
│ page_size, sort }) │ │
87+
│ ▼ │
88+
│ Read batch of │
89+
│ N images + dims │
90+
│ │ │
91+
│ Zustand store ◀────────────── emit("images:batch", │
92+
│ appends batch { images, done }) │
93+
│ │ │ │
94+
│ ▼ ▼ │
95+
│ masonic re-renders Continue until │
96+
│ (virtualized) all files scanned │
97+
│ │ │
98+
│ UI shows "scan ▼ │
99+
│ complete" ◀────────────────── emit("images:batch", │
100+
│ { images: [], done }) │
101+
│ │
102+
└──────────────────────────────────────────────────────────┘
103+
```
104+
105+
### D6: Zustand store architecture
106+
107+
**Decision:** Three Zustand stores, mirroring the old Pinia stores but with cleaner separation:
108+
109+
| Store | Responsibility |
110+
|-------|---------------|
111+
| `useSettingsStore` | Persisted settings (formats, sort, page size, language, viewer prefs). Synced bidirectionally with tauri-plugin-store. |
112+
| `useViewerStore` | Runtime viewer state (images array, current index, viewer open/closed, scan status). Not persisted. |
113+
| `useAppStore` | App-level state (selected folders, window state, UI flags). Not persisted. |
114+
115+
### D7: wouter routing structure
116+
117+
**Decision:** Minimal routes mirroring v1.4.0:
118+
119+
| Route | Component | Description |
120+
|-------|-----------|-------------|
121+
| `/` | `HomePage` | Folder selector + waterfall grid |
122+
| `/settings` | `SettingsPage` | Settings panel (or modal/drawer from any page) |
123+
| `/about` | `AboutPage` | App info, version, links |
124+
125+
Settings may alternatively be a MUI Drawer rather than a separate route, matching the old overlay pattern (`setStore.isOpen`). Decision deferred to implementation.
126+
127+
### D8: Biome for linting + formatting
128+
129+
**Decision:** Use Biome as the single linter and formatter, replacing ESLint + Prettier.
130+
131+
**Rationale:** 10-25x faster, single config file, single dependency. Biome 2.0+ has type inference and covers React hooks rules. For a new project with no existing ESLint config investment, Biome is strictly better.
132+
133+
### D9: GitHub Actions CI/CD
134+
135+
**Decision:** Two workflows:
136+
137+
1. **CI (on PR/push to dev/master):** Biome check, tsc, cargo fmt, cargo clippy, build verification on Ubuntu only (save CI minutes on PRs).
138+
2. **Release (on v* tag push):** Full matrix build on Windows + macOS (x64+ARM) + Linux (x64), create draft GitHub Release with artifacts and `latest.json` for auto-updater.
139+
140+
**Actions:** `tauri-apps/tauri-action@v0`, `oven-sh/setup-bun@v2`, `Swatinem/rust-cache@v2`, `dtolnay/rust-toolchain@stable`.
141+
142+
### D10: Renovate for dependency management
143+
144+
**Decision:** Use Renovate over Dependabot for automated dependency updates.
145+
146+
**Rationale:** Better cross-ecosystem grouping (Cargo + npm), Bun lockfile support, auto-merge for patches, monorepo awareness.
147+
148+
## Risks / Trade-offs
149+
150+
**[MUI + Tailwind coexistence]** → Two styling systems add complexity. Mitigate by using MUI for its components only and Tailwind for all custom styling. Avoid mixing MUI's `sx` prop with Tailwind classes on the same element.
151+
152+
**[shadcn/ui + MUI overlap]** → Some components exist in both. Mitigate by establishing a clear rule: MUI for structural/form components, shadcn for decorative/interactive accents. Document which components come from which library.
153+
154+
**[Tauri v2 ecosystem maturity]** → Tauri v2 is stable but younger than Electron. Some edge cases in asset protocol or plugin interactions may surface. Mitigate by staying on stable releases and testing cross-platform early.
155+
156+
**[Rust learning curve]** → If contributors are JS-focused, the Rust backend may be a barrier. Mitigate by keeping the Rust surface small (file engine + commands) and well-documented.
157+
158+
**[typesafe-i18n bundle size]** → Minimal risk — typesafe-i18n is compile-time with near-zero runtime. But it's less widely adopted than react-i18next, so community support is thinner. Mitigate by keeping the i18n layer simple (2 locales, flat key structure).
159+
160+
**[masonic maintenance]** → masonic is functional but has infrequent updates. If it becomes unmaintained, @tanstack/react-virtual with custom masonry layout is the fallback.
161+
162+
**[No video viewer in v2.0]** → Video format support was commented out in v1.4.0. Deferring to post-launch avoids scope creep but means regression for users who relied on partial video support. Mitigate by documenting this as a known gap and planning a follow-up.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## Why
2+
3+
WViewer v1.4.0 is built on Vue 3 + Quasar + Electron — a stack that has served well but now carries significant weight: Electron bundles a full Chromium instance (~150MB+), the Quasar framework couples UI tightly to Vue idioms making customization rigid, and the Node.js main process limits performance for filesystem-heavy operations like recursive directory traversal and image metadata extraction. Migrating to React + Tauri v2 delivers a dramatically smaller binary (~5-10MB), native Rust performance for file I/O, stronger security via Tauri's sandboxed architecture, and access to the larger React ecosystem for UI innovation. Now is the right time because Tauri v2 has stabilized, and the codebase is at a natural breakpoint between archived versions.
4+
5+
## What Changes
6+
7+
- **BREAKING**: Complete rewrite from Vue 3 / Quasar / Electron to React 19 / Tauri v2
8+
- **BREAKING**: Replace Pinia state management with Zustand
9+
- **BREAKING**: Replace Vue Router with wouter
10+
- **BREAKING**: Replace electron-store with tauri-plugin-store
11+
- Replace Node.js filesystem operations with Rust-native directory traversal (walkdir + image crates) for 10x+ performance improvement
12+
- Replace Electron's custom `atom://` protocol with Tauri's `asset://` protocol for local file serving
13+
- Adopt MUI as the primary UI framework with shadcn/ui for supplementary dynamic components
14+
- Implement custom titlebar using MUI (frameless window)
15+
- Adopt Tailwind CSS v4 for utility styling
16+
- Adopt typesafe-i18n for type-safe internationalization (English + Chinese)
17+
- Adopt masonic for virtualized waterfall/masonry layout (replaces vue-waterfall-plugin-next)
18+
- Adopt yet-another-react-lightbox for image viewing (replaces photoswipe/bigger-picture/viewerjs)
19+
- Set up Biome for linting and formatting (replaces ESLint + Prettier)
20+
- Set up GitHub Actions CI (PR checks + cross-platform release workflow)
21+
- Set up Renovate for automated dependency updates
22+
- Add auto-update support via tauri-plugin-updater
23+
24+
## Capabilities
25+
26+
### New Capabilities
27+
28+
- `project-scaffold`: Tauri v2 + React 19 + Vite project structure, Bun package manager, Biome config, TypeScript config, Tailwind CSS v4 setup
29+
- `rust-file-engine`: Rust-side directory traversal, image metadata extraction, file operations (delete-to-trash), and Tauri command API exposed to frontend
30+
- `asset-protocol`: Tauri asset:// protocol configuration for serving local images/videos to the webview
31+
- `app-shell`: Frameless window with MUI custom titlebar, window state persistence, single-instance enforcement, drag regions
32+
- `waterfall-view`: Virtualized masonry/waterfall image grid using masonic with responsive breakpoints, pagination, and sort controls
33+
- `image-viewer`: Full-screen lightbox image/video viewing using yet-another-react-lightbox with zoom, pan, and navigation
34+
- `settings-panel`: Settings UI and persistence via tauri-plugin-store — image formats, video formats, sort method, per-page count, viewer preferences, language selection
35+
- `folder-management`: Folder selection via native dialog (tauri-plugin-dialog), multi-folder support, drag-and-drop folder opening
36+
- `i18n`: Type-safe internationalization with typesafe-i18n for English and Chinese locales
37+
- `state-management`: Zustand stores for application state — viewer state, settings, folder/image data
38+
- `routing`: Client-side routing with wouter — main view, settings, about page
39+
- `ci-cd`: GitHub Actions workflows for PR checks (Biome, tsc, clippy, fmt, build) and cross-platform release (Windows/macOS/Linux), Renovate config, auto-updater integration
40+
41+
### Modified Capabilities
42+
43+
_(No existing capabilities — this is a greenfield rewrite)_
44+
45+
## Impact
46+
47+
- **Codebase**: Full replacement — no Vue/Quasar/Electron code carries forward. Architecture and feature knowledge from v1.4.0 informs the rewrite.
48+
- **Dependencies**: Entirely new dependency tree. Frontend: ~15 npm packages. Backend: ~10 Rust crates. Net reduction in total dependency weight.
49+
- **Build system**: Vite (retained) + Tauri CLI (replaces Quasar CLI + electron-builder). Package manager switches from npm to Bun.
50+
- **Binary size**: Expected reduction from ~150MB (Electron) to ~5-10MB (Tauri).
51+
- **Platform support**: Windows, macOS (x64 + ARM), Linux (x64 + ARM) — same coverage, better native integration.
52+
- **File protocol**: `atom://``asset://` — all image/video source URLs change format.
53+
- **Storage**: electron-store JSON → tauri-plugin-store JSON — data format similar but migration path needed if upgrading existing installs.
54+
- **CI/CD**: New GitHub Actions workflows replace any existing build scripts.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Frameless window with custom titlebar
4+
The application SHALL run in a frameless window (`"decorations": false` in tauri.conf.json) with a custom titlebar implemented using MUI's AppBar component.
5+
6+
#### Scenario: Window renders without native titlebar
7+
- **WHEN** the application launches
8+
- **THEN** the native OS titlebar SHALL not be visible and a custom MUI AppBar SHALL appear at the top of the window
9+
10+
### Requirement: Titlebar drag region
11+
The custom titlebar SHALL be draggable to move the window. Non-interactive areas SHALL use `data-tauri-drag-region`.
12+
13+
#### Scenario: Window dragging
14+
- **WHEN** the user clicks and drags on the titlebar background
15+
- **THEN** the application window SHALL move with the cursor
16+
17+
#### Scenario: Menu interaction does not drag
18+
- **WHEN** the user clicks on a menu item or button in the titlebar
19+
- **THEN** the window SHALL not move
20+
21+
### Requirement: Window control buttons
22+
The titlebar SHALL include minimize, maximize/restore, and close buttons.
23+
24+
#### Scenario: Minimize
25+
- **WHEN** the user clicks the minimize button
26+
- **THEN** the window SHALL minimize to the taskbar/dock
27+
28+
#### Scenario: Maximize and restore
29+
- **WHEN** the user clicks the maximize button on a non-maximized window
30+
- **THEN** the window SHALL maximize to fill the screen
31+
- **WHEN** the user clicks the maximize button on a maximized window
32+
- **THEN** the window SHALL restore to its previous size and position
33+
34+
#### Scenario: Close
35+
- **WHEN** the user clicks the close button
36+
- **THEN** the application SHALL close
37+
38+
### Requirement: Titlebar menus
39+
The titlebar SHALL include dropdown menus: File (with Quit), Window (with Dev Tools toggle in dev mode), and Help (with About link).
40+
41+
#### Scenario: File > Quit
42+
- **WHEN** the user selects File > Quit from the titlebar menu
43+
- **THEN** the application SHALL close
44+
45+
#### Scenario: Help > About
46+
- **WHEN** the user selects Help > About
47+
- **THEN** the application SHALL navigate to the About page
48+
49+
### Requirement: Window state persistence
50+
The application SHALL remember window size and position across sessions using `tauri-plugin-window-state`.
51+
52+
#### Scenario: Window state restored
53+
- **WHEN** the user resizes/moves the window and restarts the application
54+
- **THEN** the window SHALL open at the previously saved size and position
55+
56+
### Requirement: Single instance enforcement
57+
The application SHALL prevent multiple instances from running simultaneously using `tauri-plugin-single-instance`.
58+
59+
#### Scenario: Second instance attempted
60+
- **WHEN** the user launches a second instance of the application
61+
- **THEN** the existing instance SHALL be brought to focus and the second instance SHALL not open

0 commit comments

Comments
 (0)