#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::sync::Mutex; use tauri::Manager; use tutorialvault_lib::{commands, ffmpeg, fonts, library, prefs, video_protocol, AppPaths}; fn main() { // 1. Resolve exe directory for portability. let exe_dir = std::env::current_exe() .expect("cannot resolve exe path") .parent() .expect("exe has no parent") .to_path_buf(); let state_dir = exe_dir.join("state"); // 2. Create all subdirectories. let paths = AppPaths { exe_dir: exe_dir.clone(), state_dir: state_dir.clone(), fonts_dir: state_dir.join("fonts"), fa_dir: state_dir.join("fontawesome"), subs_dir: state_dir.join("subtitles"), }; std::fs::create_dir_all(&paths.fonts_dir).ok(); std::fs::create_dir_all(&paths.fa_dir.join("webfonts")).ok(); std::fs::create_dir_all(&paths.subs_dir).ok(); // 3. Set WebView2 user data folder for portability (no AppData usage). let webview_profile = state_dir.join("webview_profile"); std::fs::create_dir_all(&webview_profile).ok(); unsafe { std::env::set_var("WEBVIEW2_USER_DATA_FOLDER", &webview_profile); } // 4. Load preferences. let prefs_data = prefs::Prefs::load(&state_dir); // 5. Initialize library (empty - loaded from last folder or frontend action). let mut lib = library::Library::new(); // Discover ffmpeg/ffprobe. let ff_paths = ffmpeg::discover(&paths.exe_dir, &paths.state_dir); let needs_ffmpeg_download = ff_paths.ffprobe.is_none() || ff_paths.ffmpeg.is_none(); lib.ffprobe = ff_paths.ffprobe; lib.ffmpeg = ff_paths.ffmpeg; // Restore last folder if it exists. if let Some(ref last_path) = prefs_data.last_folder_path { if std::path::Path::new(last_path).is_dir() { let _ = lib.set_root(last_path, &state_dir); } } // 6. Build Tauri app. let builder = tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .manage(Mutex::new(lib)) .manage(Mutex::new(prefs_data)) .manage(paths) .invoke_handler(tauri::generate_handler![ commands::select_folder, commands::open_folder_path, commands::get_recents, commands::remove_recent, commands::get_library, commands::set_current, commands::tick_progress, commands::set_folder_volume, commands::set_folder_autoplay, commands::set_folder_rate, commands::set_order, commands::start_duration_scan, commands::get_prefs, commands::set_prefs, commands::set_always_on_top, commands::save_window_state, commands::get_note, commands::set_note, commands::get_current_video_meta, commands::get_current_subtitle, commands::get_embedded_subtitles, commands::extract_embedded_subtitle, commands::get_available_subtitles, commands::load_sidecar_subtitle, commands::choose_subtitle_file, commands::reset_watch_progress, ]); // Register custom protocol for video/subtitle/font serving. let builder = video_protocol::register_protocol(builder); // Configure window from saved prefs and launch. builder .setup(move |app| { let prefs_state = app.state::>(); let p = prefs_state.lock().unwrap(); let win = app.get_webview_window("main").unwrap(); // Only restore position/size if values are sane (not minimized/offscreen). let w = p.window.width.max(640) as u32; let h = p.window.height.max(480) as u32; if let (Some(x), Some(y)) = (p.window.x, p.window.y) { if x > -10000 && y > -10000 && x < 10000 && y < 10000 { let _ = win.set_position(tauri::Position::Physical( tauri::PhysicalPosition::new(x, y), )); } } let _ = win.set_size(tauri::Size::Physical(tauri::PhysicalSize::new(w, h))); let _ = win.set_always_on_top(p.always_on_top); drop(p); // Spawn async font caching (silent, non-blocking). let app_paths = app.state::(); let fonts_dir = app_paths.fonts_dir.clone(); let fa_dir = app_paths.fa_dir.clone(); tauri::async_runtime::spawn(async move { let _ = fonts::ensure_google_fonts_local(&fonts_dir).await; let _ = fonts::ensure_fontawesome_local(&fa_dir).await; }); // Auto-download ffmpeg/ffprobe if not found locally. if needs_ffmpeg_download { let handle = app.handle().clone(); tauri::async_runtime::spawn(async move { let sd = handle.state::().state_dir.clone(); let (tx, _rx) = tokio::sync::mpsc::channel(16); match ffmpeg::download_ffmpeg(&sd, tx).await { Ok(paths) => { let lib_state = handle.state::>(); if let Ok(mut lib) = lib_state.lock() { if lib.ffprobe.is_none() { lib.ffprobe = paths.ffprobe; } if lib.ffmpeg.is_none() { lib.ffmpeg = paths.ffmpeg; } } eprintln!("[ffmpeg] Auto-download complete"); } Err(e) => { eprintln!("[ffmpeg] Auto-download failed: {}", e); } } }); } Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }