11 KiB
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 stateMutex<Prefs>— global preferencesAppPaths— 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 copyload_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 hexcompute_library_id(): SHA-256 of sorted fingerprints, 16-char hexpretty_title_from_filename(): strip index prefix, underscores to spaces, smart title caseis_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, extractduration_seconds(): ffprobe first, ffmpeg stderr fallbackffprobe_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 timestampsauto_subtitle_sidecar(): case-insensitive matching, language code awareness, priority candidatesstore_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)
- select_folder() — native folder dialog + set_root
- open_folder_path(folder)
- get_recents()
- remove_recent(path)
- get_library()
- set_current(index, timecode)
- tick_progress(index, current_time, duration, playing)
- set_folder_volume(volume)
- set_folder_autoplay(enabled)
- set_folder_rate(rate)
- set_order(fids)
- start_duration_scan()
- get_prefs()
- set_prefs(patch)
- set_always_on_top(enabled)
- save_window_state()
- get_note(fid)
- set_note(fid, note)
- get_current_video_meta()
- get_current_subtitle()
- get_embedded_subtitles()
- extract_embedded_subtitle(track_index)
- get_available_subtitles()
- load_sidecar_subtitle(file_path)
- 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
- Resolve exe directory, create state/ subdirectories, set WEBVIEW2_USER_DATA_FOLDER
- Load prefs.json, restore window size/position
- Create Tauri window, register tutdock:// protocol, register 24 commands
- Frontend boot(): apply zoom/splits, load last library if exists
- ffmpeg discovery (async): PATH -> exe dir -> state/ffmpeg/ -> auto-download with progress UI
- 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