initial: existing TutorialDock Python app + conversion design and plan
This commit is contained in:
244
docs/plans/2026-02-19-tauri-conversion-design.md
Normal file
244
docs/plans/2026-02-19-tauri-conversion-design.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# TutorialDock: PyWebView to Tauri Conversion Design
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Convert TutorialDock from a single-file Python app (PyWebView + Flask) to a Tauri v2 desktop app with a full Rust backend and Vite + TypeScript frontend. The app must remain 100% portable (everything next to the exe, no AppData, no registry, no installer).
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Choice |
|
||||
|----------|--------|
|
||||
| Backend | Full Rust rewrite (no Python dependency) |
|
||||
| Video serving | Tauri custom protocol (`tutdock://`) with range requests |
|
||||
| Frontend tooling | Vite + TypeScript |
|
||||
| ffmpeg strategy | Discovery (PATH -> next to exe -> state/ffmpeg/) with auto-download + progress UI |
|
||||
| Platform | Windows only, portable exe only |
|
||||
| API surface | Exact 1:1 mapping of all 24 PyWebView API methods as Tauri commands |
|
||||
| Window state | Manual save/restore via prefs.json (no tauri-plugin-window-state) |
|
||||
| Dialogs | tauri-plugin-dialog for folder picker and subtitle file picker |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
tutorial-app/
|
||||
├── src-tauri/
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Entry point, Tauri builder, plugin registration
|
||||
│ │ ├── commands.rs # All 24 #[tauri::command] functions
|
||||
│ │ ├── library.rs # Library struct + all library operations
|
||||
│ │ ├── prefs.rs # Prefs struct, defaults, load/save
|
||||
│ │ ├── state.rs # atomic_write_json, load_json_with_fallbacks, backup rotation
|
||||
│ │ ├── ffmpeg.rs # Discovery, download, duration_seconds, ffprobe_video_metadata
|
||||
│ │ ├── video_protocol.rs # Custom tutdock:// protocol with range request handling
|
||||
│ │ ├── subtitles.rs # SRT->VTT, sidecar discovery, embedded extraction
|
||||
│ │ ├── fonts.rs # Google Fonts + Font Awesome download & caching
|
||||
│ │ ├── recents.rs # Recent folders list management
|
||||
│ │ └── utils.rs # natural_key, file_fingerprint, pretty_title, path helpers
|
||||
│ ├── Cargo.toml
|
||||
│ ├── tauri.conf.json
|
||||
│ └── build.rs
|
||||
├── src/
|
||||
│ ├── index.html
|
||||
│ ├── main.ts # Boot sequence, tick loop, global event wiring
|
||||
│ ├── api.ts # Typed invoke() wrappers for all 24 commands
|
||||
│ ├── types.ts # All TypeScript interfaces
|
||||
│ ├── player.ts # Video element control, seek, volume, playback rate
|
||||
│ ├── playlist.ts # Playlist rendering, drag-and-drop reorder, tree SVG
|
||||
│ ├── subtitles.ts # Subtitle menu, track management
|
||||
│ ├── ui.ts # Zoom, splits, topbar, info panel, notes, toasts
|
||||
│ ├── tooltips.ts # Tooltip system
|
||||
│ ├── styles/
|
||||
│ │ ├── main.css # Variables, body, app layout, topbar
|
||||
│ │ ├── player.css # Video wrap, controls, seek bar, volume
|
||||
│ │ ├── playlist.css # List items, tree view, scrollbar
|
||||
│ │ ├── panels.css # Panel headers, dock grid, notes, info
|
||||
│ │ ├── components.css # Buttons, switches, dropdowns, tooltips, toasts
|
||||
│ │ └── animations.css # Keyframes
|
||||
│ └── vite-env.d.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
## Rust Backend
|
||||
|
||||
### State Management
|
||||
|
||||
Three pieces of managed Tauri state:
|
||||
- `Mutex<Library>` — video library + per-folder state
|
||||
- `Mutex<Prefs>` — global preferences
|
||||
- `AppPaths` — resolved portable paths (immutable after startup)
|
||||
|
||||
### Module Details
|
||||
|
||||
**state.rs** — Atomic JSON persistence
|
||||
- `atomic_write_json(path, data, backup_count=8)`: tmp-file write, rotate .bak1-.bak8, .lastgood copy
|
||||
- `load_json_with_fallbacks(path)`: try primary -> .lastgood -> .bak1-.bak10
|
||||
|
||||
**utils.rs** — Pure utility functions
|
||||
- `natural_key()`: natural sort (split on digits, mixed comparison)
|
||||
- `file_fingerprint()`: SHA-256 of size + first 256KB + last 256KB, 20-char hex
|
||||
- `compute_library_id()`: SHA-256 of sorted fingerprints, 16-char hex
|
||||
- `pretty_title_from_filename()`: strip index prefix, underscores to spaces, smart title case
|
||||
- `is_within_root()`, `clamp()`, `truthy()`, `folder_display_name()`
|
||||
|
||||
**prefs.rs** — Global preferences
|
||||
- Struct with serde Serialize/Deserialize
|
||||
- Fields: ui_zoom, split_ratio, dock_ratio, always_on_top, window (x/y/width/height), last_folder_path, last_library_id
|
||||
- Versioned (version: 19), merged on load
|
||||
|
||||
**recents.rs** — Recent folders (max 50, deduplicated, most-recent-first)
|
||||
|
||||
**ffmpeg.rs** — ffmpeg/ffprobe management
|
||||
- `discover()`: PATH -> next to exe -> state/ffmpeg/
|
||||
- `download_ffmpeg(progress_callback)`: download from github.com/BtbN/FFmpeg-Builds, extract
|
||||
- `duration_seconds()`: ffprobe first, ffmpeg stderr fallback
|
||||
- `ffprobe_video_metadata()`: full JSON probe, video/audio/subtitle stream extraction
|
||||
- Windows CREATE_NO_WINDOW for all subprocess calls
|
||||
|
||||
**subtitles.rs**
|
||||
- `srt_to_vtt()`: BOM removal, comma->dot timestamps
|
||||
- `auto_subtitle_sidecar()`: case-insensitive matching, language code awareness, priority candidates
|
||||
- `store_subtitle_for_fid()`: convert and save to state/subtitles/
|
||||
- `extract_embedded_subtitle()`: ffmpeg extraction to VTT
|
||||
|
||||
**library.rs** — Core library management
|
||||
- Scan folder for video files (VIDEO_EXTS set)
|
||||
- Build content-based fingerprints (survives renames/moves)
|
||||
- Load/create/merge per-library state
|
||||
- Playlist ordering with saved order + natural sort for new files
|
||||
- Tree display flags (depth-1 only)
|
||||
- Folder stats (finished/remaining counts, overall progress, remaining time)
|
||||
- Background duration scanner via tokio::spawn
|
||||
- "Once finished, always finished" sticky flag
|
||||
|
||||
**video_protocol.rs** — Custom protocol handler
|
||||
- Routes: /video/{index}, /sub/{libid}/{fid}, /fonts.css, /fonts/{file}, /fa.css, /fa/webfonts/{file}
|
||||
- Video: Range header parsing, 206 Partial Content, 512KB chunk streaming
|
||||
- MIME type mapping from file extension
|
||||
- Path traversal prevention
|
||||
|
||||
**commands.rs** — 24 Tauri commands (1:1 with Python API class)
|
||||
1. select_folder() — native folder dialog + set_root
|
||||
2. open_folder_path(folder)
|
||||
3. get_recents()
|
||||
4. remove_recent(path)
|
||||
5. get_library()
|
||||
6. set_current(index, timecode)
|
||||
7. tick_progress(index, current_time, duration, playing)
|
||||
8. set_folder_volume(volume)
|
||||
9. set_folder_autoplay(enabled)
|
||||
10. set_folder_rate(rate)
|
||||
11. set_order(fids)
|
||||
12. start_duration_scan()
|
||||
13. get_prefs()
|
||||
14. set_prefs(patch)
|
||||
15. set_always_on_top(enabled)
|
||||
16. save_window_state()
|
||||
17. get_note(fid)
|
||||
18. set_note(fid, note)
|
||||
19. get_current_video_meta()
|
||||
20. get_current_subtitle()
|
||||
21. get_embedded_subtitles()
|
||||
22. extract_embedded_subtitle(track_index)
|
||||
23. get_available_subtitles()
|
||||
24. load_sidecar_subtitle(file_path)
|
||||
25. choose_subtitle_file() — native file dialog + set subtitle
|
||||
|
||||
### Rust Crates
|
||||
|
||||
| Crate | Purpose |
|
||||
|-------|---------|
|
||||
| tauri | App framework, windows, commands, custom protocols |
|
||||
| serde + serde_json | JSON serialization |
|
||||
| sha2 | SHA-256 fingerprinting |
|
||||
| tokio | Async runtime (duration scan, downloads) |
|
||||
| reqwest | HTTP client (ffmpeg + font downloads) |
|
||||
| zip | Extracting ffmpeg archive |
|
||||
| which | Finding executables on PATH |
|
||||
| tauri-plugin-dialog | Native file/folder dialogs |
|
||||
|
||||
## Frontend
|
||||
|
||||
### TypeScript Modules
|
||||
|
||||
**types.ts** — Interfaces for all API responses (LibraryInfo, VideoItem, Prefs, VideoMeta, SubtitleInfo, etc.)
|
||||
|
||||
**api.ts** — Typed invoke() wrappers for all 24 commands. Every `window.pywebview.api.foo()` becomes `api.foo()`.
|
||||
|
||||
**main.ts** — Boot sequence, tick loop (250ms interval, progress save ~1s, library refresh ~3s, window state save), global keyboard shortcuts, Tauri event listeners (ffmpeg download progress).
|
||||
|
||||
**player.ts** — Video element with `tutdock://localhost/video/{index}` src, play/pause/seek/volume/speed/fullscreen, overlay, autoplay-next on ended.
|
||||
|
||||
**playlist.ts** — Render list items with progress bars and tree connectors, drag-and-drop reorder via pointer events, custom scrollbar.
|
||||
|
||||
**subtitles.ts** — Subtitle menu (sidecar + embedded + file picker + disable), track management via `tutdock://localhost/sub/...`.
|
||||
|
||||
**ui.ts** — Zoom control, split ratio dragging, topbar actions (always-on-top, autoplay, open folder, recent dropdown, reset progress, reload), info panel, notes textarea with debounced auto-save, toast notifications.
|
||||
|
||||
**tooltips.ts** — Event delegation on [data-tip] elements, zoom-aware positioning.
|
||||
|
||||
### CSS Split
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| main.css | :root variables, body, #zoomRoot, .app, .topbar, .brand |
|
||||
| player.css | .videoWrap, video, .controls, seek/volume bars, time display |
|
||||
| playlist.css | #list, .listItem, tree SVG, .listScrollbar, #emptyHint |
|
||||
| panels.css | .panel, .panelHeader, #dockGrid, .notesArea, .infoGrid |
|
||||
| components.css | .toolbarBtn, .splitBtn, .toggleSwitch, .dropdownPortal, #toast, #fancyTooltip |
|
||||
| animations.css | @keyframes logoSpin, logoWiggle, transitions |
|
||||
|
||||
### Video Source URLs
|
||||
|
||||
- Current: `http://127.0.0.1:{port}/video/{index}`
|
||||
- New: `tutdock://localhost/video/{index}`
|
||||
|
||||
Same change for subtitles, fonts, and font awesome assets.
|
||||
|
||||
## Custom Protocol Routing
|
||||
|
||||
| URL Pattern | Response |
|
||||
|-------------|----------|
|
||||
| tutdock://localhost/video/{index} | Video file (200/206 with range support) |
|
||||
| tutdock://localhost/sub/{libid}/{fid} | VTT subtitle file |
|
||||
| tutdock://localhost/fonts.css | Combined Google Fonts CSS |
|
||||
| tutdock://localhost/fonts/{filename} | Font woff2 files |
|
||||
| tutdock://localhost/fa.css | Font Awesome CSS |
|
||||
| tutdock://localhost/fa/webfonts/{filename} | Font Awesome webfont files |
|
||||
|
||||
## Startup Flow
|
||||
|
||||
1. Resolve exe directory, create state/ subdirectories, set WEBVIEW2_USER_DATA_FOLDER
|
||||
2. Load prefs.json, restore window size/position
|
||||
3. Create Tauri window, register tutdock:// protocol, register 24 commands
|
||||
4. Frontend boot(): apply zoom/splits, load last library if exists
|
||||
5. ffmpeg discovery (async): PATH -> exe dir -> state/ffmpeg/ -> auto-download with progress UI
|
||||
6. Font caching (async, silent): Google Fonts + Font Awesome
|
||||
|
||||
## Portable State Layout
|
||||
|
||||
```
|
||||
TutorialDock.exe
|
||||
state/
|
||||
├── prefs.json (+ .lastgood, .bak1-.bak8)
|
||||
├── recent_folders.json (+ backups)
|
||||
├── library_{id}.json (per unique library, + backups)
|
||||
├── webview_profile/ (WebView2 data)
|
||||
├── fonts/ (cached Google Fonts)
|
||||
│ ├── fonts.css
|
||||
│ ├── fonts_meta.json
|
||||
│ └── *.woff2
|
||||
├── fontawesome/ (cached Font Awesome)
|
||||
│ ├── fa.css
|
||||
│ ├── fa_meta.json
|
||||
│ └── webfonts/*.woff2
|
||||
├── subtitles/ (converted subtitle files)
|
||||
│ └── {fid}_*.vtt
|
||||
└── ffmpeg/ (auto-downloaded)
|
||||
├── ffmpeg.exe
|
||||
└── ffprobe.exe
|
||||
```
|
||||
Reference in New Issue
Block a user