TutorialVault: complete Tauri v2 port with runtime fixes

Rename from TutorialDock to TutorialVault. Remove legacy Python app
and scripts. Fix video playback, subtitles, metadata display, window
state persistence, and auto-download of ffmpeg/ffprobe on first run.
Bundle fonts via npm instead of runtime download.
This commit is contained in:
Your Name
2026-02-19 12:44:57 +02:00
parent a459efae45
commit 9c8d7d94cd
25 changed files with 11665 additions and 7364 deletions

View File

@@ -26,6 +26,9 @@ use crate::AppPaths;
/// Maximum chunk size for full-file responses (4 MB).
const MAX_CHUNK: u64 = 4 * 1024 * 1024;
/// CORS header name.
const CORS: &str = "Access-Control-Allow-Origin";
/// Register the `tutdock` custom protocol on the builder.
pub fn register_protocol(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
builder.register_uri_scheme_protocol("tutdock", move |ctx, request| {
@@ -140,6 +143,7 @@ fn serve_full(path: &PathBuf, file_size: u64, mime: &str) -> http::Response<Vec<
.header(header::CONTENT_LENGTH, file_size.to_string())
.header(header::ACCEPT_RANGES, "bytes")
.header(header::CACHE_CONTROL, "no-store")
.header(CORS, "*")
.body(buf)
.unwrap_or_else(|_| internal_error("response build error"))
}
@@ -180,6 +184,7 @@ fn serve_range(
.header(header::CONTENT_LENGTH, length.to_string())
.header(header::ACCEPT_RANGES, "bytes")
.header(header::CACHE_CONTROL, "no-store")
.header(CORS, "*")
.body(buf)
.unwrap_or_else(|_| internal_error("response build error"))
}
@@ -215,33 +220,36 @@ fn parse_range(header: &str, file_size: u64) -> Option<(u64, u64)> {
// ---------------------------------------------------------------------------
fn handle_subtitle(app: &tauri::AppHandle, rest: &str) -> http::Response<Vec<u8>> {
// Path: sub/{libid}/{fid}
let parts: Vec<&str> = rest.splitn(2, '/').collect();
if parts.len() != 2 {
return not_found();
}
let fid = parts[1];
// rest is a VTT filename like "{fid}_{name}.vtt"
let filename = rest;
if !fid.chars().all(|c| c.is_ascii_hexdigit()) {
// Validate: no path traversal
if filename.contains("..") || filename.contains('/') || filename.contains('\\') || filename.is_empty() {
return not_found();
}
let paths = app.state::<AppPaths>();
let vtt_path = paths.subs_dir.join(format!("{}.vtt", fid));
let vtt_path = paths.subs_dir.join(filename);
if !vtt_path.exists() {
// Ensure resolved path is still inside subs_dir
if let (Ok(base), Ok(resolved)) = (paths.subs_dir.canonicalize(), vtt_path.canonicalize()) {
if !resolved.starts_with(&base) {
return not_found();
}
} else {
return not_found();
}
let data = match fs::read(&vtt_path) {
Ok(d) => d,
Err(_) => return internal_error("read error"),
Err(_) => return not_found(),
};
http::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/vtt; charset=utf-8")
.header(header::CACHE_CONTROL, "no-store")
.header(CORS, "*")
.body(data)
.unwrap_or_else(|_| internal_error("response build error"))
}
@@ -331,6 +339,7 @@ fn serve_static_file(base_dir: &PathBuf, filename: &str) -> http::Response<Vec<u
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "no-store")
.header(CORS, "*")
.body(data)
.unwrap_or_else(|_| internal_error("response build error"))
}