# 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` — video library + per-folder state - `Mutex` — 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 ```