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:
@@ -519,7 +519,11 @@ impl Library {
|
||||
let folder = self
|
||||
.root
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.map(|p| {
|
||||
let s = p.to_string_lossy().to_string();
|
||||
// Strip Windows extended-length path prefix.
|
||||
s.strip_prefix("\\\\?\\").unwrap_or(&s).to_string()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let count = self.fids.len();
|
||||
@@ -623,6 +627,14 @@ impl Library {
|
||||
|
||||
/// Find the next unfinished video index after the current position.
|
||||
fn _compute_next_up(&self, current_index: Option<usize>) -> Value {
|
||||
let make_result = |i: usize| -> Value {
|
||||
let fid = &self.fids[i];
|
||||
let title = self.fid_to_rel.get(fid)
|
||||
.map(|r| pretty_title_from_filename(r))
|
||||
.unwrap_or_default();
|
||||
json!({"index": i, "title": title})
|
||||
};
|
||||
|
||||
let start = current_index.map(|i| i + 1).unwrap_or(0);
|
||||
for i in start..self.fids.len() {
|
||||
let fid = &self.fids[i];
|
||||
@@ -633,7 +645,7 @@ impl Library {
|
||||
.map(|m| m.finished)
|
||||
.unwrap_or(false);
|
||||
if !finished {
|
||||
return json!(i);
|
||||
return make_result(i);
|
||||
}
|
||||
}
|
||||
// Wrap around from beginning.
|
||||
@@ -647,7 +659,7 @@ impl Library {
|
||||
.map(|m| m.finished)
|
||||
.unwrap_or(false);
|
||||
if !finished {
|
||||
return json!(i);
|
||||
return make_result(i);
|
||||
}
|
||||
}
|
||||
Value::Null
|
||||
@@ -914,7 +926,7 @@ impl Library {
|
||||
None => return json!({"ok": false, "error": "current fid not in library"}),
|
||||
};
|
||||
|
||||
let mut result = self._basic_file_meta(&fid);
|
||||
let basic = self._basic_file_meta(&fid);
|
||||
|
||||
// Probe if not cached.
|
||||
if !self.meta_cache.contains_key(&fid) {
|
||||
@@ -929,6 +941,12 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = json!({
|
||||
"ok": true,
|
||||
"basic": basic,
|
||||
"ffprobe_found": self.ffprobe.is_some(),
|
||||
});
|
||||
|
||||
if let Some(cached) = self.meta_cache.get(&fid) {
|
||||
if let Ok(meta_val) = serde_json::to_value(cached) {
|
||||
result
|
||||
@@ -938,10 +956,6 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("ok".to_string(), json!(true));
|
||||
result
|
||||
}
|
||||
|
||||
@@ -949,6 +963,23 @@ impl Library {
|
||||
// Subtitle methods
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Build a protocol URL for a stored subtitle VTT path.
|
||||
/// The `vtt` field is like `"subtitles/{fid}_{name}.vtt"` — extract the filename.
|
||||
fn _sub_url(vtt: &str) -> String {
|
||||
let filename = vtt.rsplit('/').next().unwrap_or(vtt);
|
||||
format!("http://tutdock.localhost/sub/{}", filename)
|
||||
}
|
||||
|
||||
/// Build a successful subtitle JSON response with `has`, `url`, and `label`.
|
||||
fn _sub_response(vtt: &str, label: &str) -> Value {
|
||||
json!({
|
||||
"ok": true,
|
||||
"has": true,
|
||||
"url": Self::_sub_url(vtt),
|
||||
"label": label,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get subtitle for the current video.
|
||||
///
|
||||
/// Priority: stored subtitle -> sidecar -> embedded.
|
||||
@@ -968,12 +999,7 @@ impl Library {
|
||||
if let Some(ref sub_ref) = meta.subtitle {
|
||||
let vtt_path = state_dir.join(&sub_ref.vtt);
|
||||
if vtt_path.exists() {
|
||||
return json!({
|
||||
"ok": true,
|
||||
"source": "stored",
|
||||
"vtt": sub_ref.vtt,
|
||||
"label": sub_ref.label,
|
||||
});
|
||||
return Self::_sub_response(&sub_ref.vtt, &sub_ref.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -993,12 +1019,7 @@ impl Library {
|
||||
});
|
||||
}
|
||||
self.save_state();
|
||||
return json!({
|
||||
"ok": true,
|
||||
"source": "sidecar",
|
||||
"vtt": stored.vtt,
|
||||
"label": stored.label,
|
||||
});
|
||||
return Self::_sub_response(&stored.vtt, &stored.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1026,12 +1047,7 @@ impl Library {
|
||||
});
|
||||
}
|
||||
self.save_state();
|
||||
return json!({
|
||||
"ok": true,
|
||||
"source": "embedded",
|
||||
"vtt": stored.vtt,
|
||||
"label": stored.label,
|
||||
});
|
||||
return Self::_sub_response(&stored.vtt, &stored.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1055,7 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
json!({"ok": true, "source": "none"})
|
||||
json!({"ok": true, "has": false})
|
||||
}
|
||||
|
||||
/// Store a user-selected subtitle file for the current video.
|
||||
@@ -1064,11 +1080,7 @@ impl Library {
|
||||
});
|
||||
}
|
||||
self.save_state();
|
||||
json!({
|
||||
"ok": true,
|
||||
"vtt": stored.vtt,
|
||||
"label": stored.label,
|
||||
})
|
||||
Self::_sub_response(&stored.vtt, &stored.label)
|
||||
}
|
||||
None => {
|
||||
json!({"ok": false, "error": "unsupported subtitle format"})
|
||||
@@ -1154,11 +1166,7 @@ impl Library {
|
||||
});
|
||||
}
|
||||
self.save_state();
|
||||
json!({
|
||||
"ok": true,
|
||||
"vtt": stored.vtt,
|
||||
"label": stored.label,
|
||||
})
|
||||
Self::_sub_response(&stored.vtt, &stored.label)
|
||||
}
|
||||
Err(e) => {
|
||||
json!({"ok": false, "error": e})
|
||||
@@ -1288,10 +1296,15 @@ impl Library {
|
||||
});
|
||||
|
||||
for (label, path, _) in &found {
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_default();
|
||||
sidecar_subs.push(json!({
|
||||
"type": "sidecar",
|
||||
"label": label,
|
||||
"path": path,
|
||||
"format": ext,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1358,11 +1371,7 @@ impl Library {
|
||||
});
|
||||
}
|
||||
self.save_state();
|
||||
json!({
|
||||
"ok": true,
|
||||
"vtt": stored.vtt,
|
||||
"label": stored.label,
|
||||
})
|
||||
Self::_sub_response(&stored.vtt, &stored.label)
|
||||
}
|
||||
None => {
|
||||
json!({"ok": false, "error": "unsupported subtitle format or read error"})
|
||||
@@ -1572,7 +1581,10 @@ impl Library {
|
||||
|
||||
let folder = path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.map(|p| {
|
||||
let s = p.to_string_lossy().to_string();
|
||||
s.strip_prefix("\\\\?\\").unwrap_or(&s).to_string()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
return json!({
|
||||
|
||||
Reference in New Issue
Block a user