feat: implement video_protocol.rs, commands.rs, wire up main.rs, and index.html
- video_protocol.rs: tutdock:// custom protocol with HTTP Range support for video streaming, subtitle/font serving with path traversal protection - commands.rs: all 26 Tauri command handlers as thin wrappers - main.rs: full Tauri bootstrap with state management, window restore, async font caching, and ffmpeg discovery - index.html: complete HTML markup extracted from Python app - lib.rs: updated with all module declarations and AppPaths struct
This commit is contained in:
487
src-tauri/src/commands.rs
Normal file
487
src-tauri/src/commands.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! Tauri v2 command handlers for TutorialDock.
|
||||
//!
|
||||
//! Each function is a thin `#[tauri::command]` wrapper that acquires the
|
||||
//! relevant state lock(s) and delegates to the appropriate module.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
use crate::ffmpeg;
|
||||
use crate::library::Library;
|
||||
use crate::prefs::Prefs;
|
||||
use crate::recents;
|
||||
use crate::AppPaths;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Extract a short display name from a folder path (last component).
|
||||
fn display_name_for_path(p: &str) -> String {
|
||||
Path::new(p)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| p.to_string())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 1. select_folder
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_folder(
|
||||
app: tauri::AppHandle,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
prefs: tauri::State<'_, Mutex<Prefs>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let folder = app.dialog().file().blocking_pick_folder();
|
||||
|
||||
let folder_path = match folder {
|
||||
Some(fp) => fp.as_path().unwrap().to_path_buf(),
|
||||
None => return Ok(json!({"ok": false, "cancelled": true})),
|
||||
};
|
||||
|
||||
let folder_str = folder_path.to_string_lossy().to_string();
|
||||
|
||||
let result = {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
lib.set_root(&folder_str, &paths.state_dir)?
|
||||
};
|
||||
|
||||
recents::push_recent(&paths.state_dir, &folder_str);
|
||||
|
||||
{
|
||||
let mut p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
p.last_folder_path = Some(folder_str);
|
||||
p.save(&paths.state_dir);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 2. open_folder_path
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_folder_path(
|
||||
folder: String,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
prefs: tauri::State<'_, Mutex<Prefs>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let result = {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
lib.set_root(&folder, &paths.state_dir)?
|
||||
};
|
||||
|
||||
recents::push_recent(&paths.state_dir, &folder);
|
||||
|
||||
{
|
||||
let mut p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
p.last_folder_path = Some(folder);
|
||||
p.save(&paths.state_dir);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 3. get_recents
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_recents(paths: tauri::State<'_, AppPaths>) -> Result<Value, String> {
|
||||
let all = recents::load_recents(&paths.state_dir);
|
||||
|
||||
let items: Vec<Value> = all
|
||||
.into_iter()
|
||||
.filter(|p| Path::new(p).is_dir())
|
||||
.map(|p| {
|
||||
let name = display_name_for_path(&p);
|
||||
json!({"path": p, "name": name})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(json!({"ok": true, "items": items}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 4. remove_recent
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_recent(path: String, paths: tauri::State<'_, AppPaths>) -> Result<Value, String> {
|
||||
recents::remove_recent(&paths.state_dir, &path);
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 5. get_library
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_library(library: tauri::State<'_, Mutex<Library>>) -> Result<Value, String> {
|
||||
let lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.get_library_info())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 6. set_current
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_current(
|
||||
index: usize,
|
||||
timecode: f64,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_current(index, timecode))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 7. tick_progress
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn tick_progress(
|
||||
index: usize,
|
||||
current_time: f64,
|
||||
duration: Option<f64>,
|
||||
playing: bool,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.update_progress(index, current_time, duration, playing))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 8. set_folder_volume
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_folder_volume(
|
||||
volume: f64,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_folder_volume(volume))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 9. set_folder_autoplay
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_folder_autoplay(
|
||||
enabled: bool,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_folder_autoplay(enabled))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 10. set_folder_rate
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_folder_rate(
|
||||
rate: f64,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_folder_rate(rate))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 11. set_order
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_order(
|
||||
fids: Vec<String>,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_order(fids))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 12. start_duration_scan
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_duration_scan(
|
||||
app: tauri::AppHandle,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let (pending, stop_flag, ffprobe_path, ffmpeg_path) = {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
lib.start_duration_scan();
|
||||
let pending = lib.get_pending_scans();
|
||||
let stop_flag = lib.scan_stop_flag();
|
||||
let ffprobe_path = lib.ffprobe.clone();
|
||||
let ffmpeg_path = lib.ffmpeg.clone();
|
||||
(pending, stop_flag, ffprobe_path, ffmpeg_path)
|
||||
};
|
||||
|
||||
let count = pending.len();
|
||||
if count == 0 {
|
||||
return Ok(json!({"ok": true, "pending": 0}));
|
||||
}
|
||||
|
||||
let handle = app.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let ff_paths = ffmpeg::FfmpegPaths {
|
||||
ffprobe: ffprobe_path,
|
||||
ffmpeg: ffmpeg_path,
|
||||
};
|
||||
|
||||
let library_state = handle.state::<Mutex<Library>>();
|
||||
|
||||
for (fid, file_path) in &pending {
|
||||
if stop_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
if let Some(dur) = ffmpeg::duration_seconds(file_path, &ff_paths) {
|
||||
if let Ok(mut lib) = library_state.lock() {
|
||||
lib.apply_scanned_duration(fid, dur);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(json!({"ok": true, "pending": count}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 13. get_prefs
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_prefs(prefs: tauri::State<'_, Mutex<Prefs>>) -> Result<Value, String> {
|
||||
let p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(json!({"ok": true, "prefs": serde_json::to_value(&*p).unwrap_or(json!({}))}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 14. set_prefs
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_prefs(
|
||||
patch: Value,
|
||||
prefs: tauri::State<'_, Mutex<Prefs>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let mut p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
p.update(&patch, &paths.state_dir);
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 15. set_always_on_top
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_always_on_top(
|
||||
enabled: bool,
|
||||
app: tauri::AppHandle,
|
||||
prefs: tauri::State<'_, Mutex<Prefs>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
{
|
||||
let mut p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
p.always_on_top = enabled;
|
||||
p.save(&paths.state_dir);
|
||||
}
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window
|
||||
.set_always_on_top(enabled)
|
||||
.map_err(|e| format!("set_always_on_top failed: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 16. save_window_state
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_window_state(
|
||||
app: tauri::AppHandle,
|
||||
prefs: tauri::State<'_, Mutex<Prefs>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let window = app
|
||||
.get_webview_window("main")
|
||||
.ok_or_else(|| "main window not found".to_string())?;
|
||||
|
||||
let pos = window
|
||||
.outer_position()
|
||||
.map_err(|e| format!("outer_position failed: {}", e))?;
|
||||
let size = window
|
||||
.outer_size()
|
||||
.map_err(|e| format!("outer_size failed: {}", e))?;
|
||||
|
||||
let mut p = prefs.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
p.window.x = Some(pos.x);
|
||||
p.window.y = Some(pos.y);
|
||||
p.window.width = size.width as i32;
|
||||
p.window.height = size.height as i32;
|
||||
p.save(&paths.state_dir);
|
||||
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 17. get_note
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_note(fid: String, library: tauri::State<'_, Mutex<Library>>) -> Result<Value, String> {
|
||||
let lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
let note = lib.get_note(&fid);
|
||||
Ok(json!({"ok": true, "note": note}))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 18. set_note
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_note(
|
||||
fid: String,
|
||||
note: String,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_note(&fid, ¬e))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 19. get_current_video_meta
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_current_video_meta(
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.get_current_video_metadata())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 20. get_current_subtitle
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_current_subtitle(
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.get_subtitle_for_current(&paths.state_dir))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 21. get_embedded_subtitles
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_embedded_subtitles(
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.get_embedded_subtitles())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 22. extract_embedded_subtitle
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn extract_embedded_subtitle(
|
||||
track_index: u32,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.extract_embedded_subtitle(track_index, &paths.state_dir))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 23. get_available_subtitles
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_available_subtitles(
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.get_available_subtitles())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 24. load_sidecar_subtitle
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_sidecar_subtitle(
|
||||
file_path: String,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.load_sidecar_subtitle(&file_path, &paths.state_dir))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 25. choose_subtitle_file
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn choose_subtitle_file(
|
||||
app: tauri::AppHandle,
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
paths: tauri::State<'_, AppPaths>,
|
||||
) -> Result<Value, String> {
|
||||
let file = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter("Subtitles", &["srt", "vtt"])
|
||||
.blocking_pick_file();
|
||||
|
||||
let file_path = match file {
|
||||
Some(fp) => fp.as_path().unwrap().to_path_buf(),
|
||||
None => return Ok(json!({"ok": false, "cancelled": true})),
|
||||
};
|
||||
|
||||
let file_str = file_path.to_string_lossy().to_string();
|
||||
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.set_subtitle_for_current(&file_str, &paths.state_dir))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 26. reset_watch_progress
|
||||
// ===========================================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn reset_watch_progress(
|
||||
library: tauri::State<'_, Mutex<Library>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?;
|
||||
Ok(lib.reset_watch_progress())
|
||||
}
|
||||
Reference in New Issue
Block a user