use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use tauri::{AppHandle, Manager}; #[derive(Serialize, Deserialize, Clone)] pub struct Settings { #[serde(default = "default_sam_url")] pub sam_url: String, #[serde(default)] pub sam_enabled: bool, /// Model id chosen from the backend's /models list (e.g. /// "sam2.1_hiera_small"). `None` lets the backend pick its default. #[serde(default)] pub sam_model: Option, #[serde(default = "default_cache_capacity")] pub cache_capacity: usize, /// ffmpeg `-hwaccel` argument. Empty string = software decode (default). /// Recognized: "auto", "vaapi", "cuda", "vdpau". Applied on next /// `open_video` or worker respawn. #[serde(default)] pub hwaccel: String, /// Decimation factor used by playback (advance by N frames per tick) and /// bulk extract (take every Nth frame in range). 1 = no decimation. #[serde(default = "default_frame_skip")] pub frame_skip: u64, /// Maximum height (in pixels) of the canvas backing store during preview. /// 0 = no cap beyond native resolution. Common presets: 2160 (4K), /// 1440 (2K), 1080, 720, 480. Smaller → less draw cost. #[serde(default)] pub preview_max_height: u32, } fn default_sam_url() -> String { "http://localhost:9090".into() } fn default_cache_capacity() -> usize { crate::decoder::DEFAULT_CACHE_CAPACITY } fn default_frame_skip() -> u64 { 1 } fn normalize(mut s: Settings) -> Settings { s.cache_capacity = s.cache_capacity.clamp( crate::decoder::MIN_CACHE_CAPACITY, crate::decoder::MAX_CACHE_CAPACITY, ); if s.frame_skip < 1 { s.frame_skip = 1; } s } impl Default for Settings { fn default() -> Self { Self { sam_url: default_sam_url(), sam_enabled: false, sam_model: None, cache_capacity: default_cache_capacity(), hwaccel: String::new(), frame_skip: default_frame_skip(), preview_max_height: 0, } } } fn settings_path(app: &AppHandle) -> Result { let dir = app .path() .app_local_data_dir() .map_err(|e| format!("app data dir: {e}"))?; fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {e}", dir.display()))?; Ok(dir.join("settings.json")) } pub fn load(app: &AppHandle) -> Result { let path = settings_path(app)?; if !path.exists() { return Ok(Settings::default()); } let text = fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?; serde_json::from_str(&text) .map(normalize) .map_err(|e| format!("parse settings: {e}")) } pub fn save(app: &AppHandle, s: &Settings) -> Result<(), String> { let path = settings_path(app)?; let normalized = normalize(s.clone()); let text = serde_json::to_string_pretty(&normalized).map_err(|e| format!("serialize: {e}"))?; fs::write(&path, text).map_err(|e| format!("write {}: {e}", path.display())) } #[tauri::command] pub fn get_settings(app: AppHandle) -> Result { load(&app) } #[tauri::command] pub fn save_settings(app: AppHandle, settings: Settings) -> Result<(), String> { save(&app, &settings) }