//! 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>, prefs: tauri::State<'_, Mutex>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, prefs: tauri::State<'_, Mutex>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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 { let all = recents::load_recents(&paths.state_dir); let items: Vec = 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 { recents::remove_recent(&paths.state_dir, &path); Ok(json!({"ok": true})) } // =========================================================================== // 5. get_library // =========================================================================== #[tauri::command] pub fn get_library(library: tauri::State<'_, Mutex>) -> Result { 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>, ) -> Result { 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, playing: bool, library: tauri::State<'_, Mutex>, ) -> Result { 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>, ) -> Result { 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>, ) -> Result { 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>, ) -> Result { 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, library: tauri::State<'_, Mutex>, ) -> Result { 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>, ) -> Result { 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::>(); 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>) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>) -> Result { 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>, ) -> Result { 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>, ) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, ) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, ) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, paths: tauri::State<'_, AppPaths>, ) -> Result { 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>, ) -> Result { let mut lib = library.lock().map_err(|e| format!("lock error: {}", e))?; Ok(lib.reset_watch_progress()) }