Files
tutorialvault/docs/plans/2026-02-19-tauri-conversion-design.md

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 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