1use crate::theme::PaletteOverride;
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6use std::path::PathBuf;
7
8#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
9#[serde(default)]
10pub struct RecentAsset {
11 pub path: String,
12 pub last_used: u64,
14 pub tags: Vec<String>,
15 pub label: u8,
17 pub pinned: bool,
19}
20
21impl Default for RecentAsset {
22 fn default() -> Self {
23 Self { path: String::new(), last_used: 0, tags: Vec::new(), label: 0, pinned: false }
24 }
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
29pub struct LayoutProfile {
30 pub name: String,
31 pub json: String,
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
37pub struct CurvePreset {
38 pub name: String,
39 pub keys: Vec<crate::model::Keyframe>,
40 pub absolute: bool,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
46pub struct MotionPreset {
47 pub name: String,
48 pub props: Vec<(String, CurvePreset)>,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
54pub struct EffectPreset {
55 pub name: String,
56 pub json: String,
57}
58
59impl EffectPreset {
60 pub fn is_graph(&self) -> bool {
62 self.json.trim_start().starts_with('{')
63 }
64}
65
66#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
68pub struct Template {
69 pub name: String,
70 pub json: String,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize)]
74#[serde(default)]
75pub struct Settings {
76 pub ffmpeg_dir: String,
78 pub ytdlp_dir: String,
80 pub download_dir: String,
82 pub encoder: String,
85 pub crf: u32,
87 pub preset: String,
89 pub confirm_overwrite: bool,
90 pub lossless_save: bool,
93 pub context_menu: bool,
95 pub theme: String,
97 pub decoder: String,
99 pub preview_max_width: u32,
101 pub snap: bool,
102 pub show_library: bool,
103 pub show_inspector: bool,
104 pub hotkeys: BTreeMap<String, String>,
106 pub recent_assets: Vec<RecentAsset>,
107 pub recent_projects: Vec<String>,
108 pub layout: String,
110 pub layout_profiles: Vec<LayoutProfile>,
112 pub curve_presets: Vec<CurvePreset>,
115 pub motion_presets: Vec<MotionPreset>,
116 pub effect_presets: Vec<EffectPreset>,
117 pub templates: Vec<Template>,
118 pub user_fonts: Vec<String>,
120 pub export_scaler: String,
123 pub export_resolution: String,
125 pub mcp_enabled: bool,
127 pub mcp_port: u16,
128 pub gpu: bool,
131 pub preview_quality: u32,
133 pub movie_mode: bool,
135 pub use_proxies: bool,
137 pub proxy_height: u32,
139 pub icon_overrides: BTreeMap<String, String>,
141 pub capture_fps: u32,
143 pub capture_bitrate_kbps: u32,
144 pub capture_mic: String,
145 pub capture_desktop_audio: bool,
146 pub capture_cursor: bool,
147 pub capture_on_blur: bool,
149 pub capture_dir: String,
150 pub voice_device: String,
152 pub voice_channels: u32,
153 pub frame_resolution: String,
155 pub frame_format: String,
156 pub frame_quality: u32,
157 pub effect_thumb_image: String,
159 pub palette: PaletteOverride,
162 pub whisper_dir: String,
165 pub transcribe_model: String,
167}
168
169impl Default for Settings {
170 fn default() -> Self {
171 Self {
172 ffmpeg_dir: String::new(),
173 ytdlp_dir: String::new(),
174 download_dir: String::new(),
175 encoder: "auto".into(),
176 crf: 18,
177 preset: "veryfast".into(),
178 confirm_overwrite: true,
179 lossless_save: false,
180 context_menu: true,
181 theme: "system".into(),
182 decoder: "auto".into(),
183 preview_max_width: 1280,
184 snap: true,
185 show_library: true,
186 show_inspector: true,
187 hotkeys: BTreeMap::new(),
188 recent_assets: Vec::new(),
189 recent_projects: Vec::new(),
190 layout: String::new(),
191 layout_profiles: Vec::new(),
192 curve_presets: Vec::new(),
193 motion_presets: Vec::new(),
194 effect_presets: Vec::new(),
195 templates: Vec::new(),
196 user_fonts: Vec::new(),
197 export_scaler: "lanczos".into(),
198 export_resolution: "project".into(),
199 mcp_enabled: false,
200 mcp_port: 7337,
201 gpu: true,
202 preview_quality: 100,
203 movie_mode: false,
204 use_proxies: true,
205 proxy_height: 720,
206 icon_overrides: BTreeMap::new(),
207 capture_fps: 30,
208 capture_bitrate_kbps: 8000,
209 capture_mic: String::new(),
210 capture_desktop_audio: true,
211 capture_cursor: true,
212 capture_on_blur: false,
213 capture_dir: String::new(),
214 voice_device: String::new(),
215 voice_channels: 1,
216 frame_resolution: "project".into(),
217 frame_format: "png".into(),
218 frame_quality: 92,
219 effect_thumb_image: String::new(),
220 palette: PaletteOverride::default(),
221 whisper_dir: String::new(),
222 transcribe_model: String::new(),
223 }
224 }
225}
226
227impl Settings {
228 pub fn dir() -> PathBuf {
230 let base = std::env::var_os("APPDATA").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."));
231 base.join("SimpleEditor")
232 }
233 pub fn cache_dir() -> PathBuf {
235 let base = std::env::var_os("LOCALAPPDATA").map(PathBuf::from).unwrap_or_else(Self::dir);
236 base.join("SimpleEditor").join("cache")
237 }
238 pub fn path() -> PathBuf {
239 Self::dir().join("settings.json")
240 }
241 pub fn load() -> Self {
242 std::fs::read_to_string(Self::path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()
243 }
244 pub fn save(&self) {
246 let _ = std::fs::create_dir_all(Self::dir());
247 if let Ok(s) = serde_json::to_string_pretty(self) {
248 let tmp = Self::dir().join("settings.json.tmp");
249 if std::fs::write(&tmp, s).is_ok() {
250 let _ = std::fs::rename(tmp, Self::path());
251 }
252 }
253 }
254 pub fn now() -> u64 {
255 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
256 }
257 pub fn touch_recent(&mut self, path: &str) {
260 let existing = self.recent_assets.iter().position(|r| r.path.eq_ignore_ascii_case(path));
261 let mut entry = existing.map(|i| self.recent_assets.remove(i)).unwrap_or_default();
262 entry.path = path.to_string();
263 entry.last_used = Self::now();
264 self.recent_assets.insert(0, entry);
265 self.sort_recent();
266 let mut n = self.recent_assets.len();
268 while n > 200 {
269 if let Some(i) = self.recent_assets.iter().rposition(|r| !r.pinned) {
270 self.recent_assets.remove(i);
271 n -= 1;
272 } else {
273 break;
274 }
275 }
276 }
277 pub fn remove_recent(&mut self, path: &str) {
278 self.recent_assets.retain(|r| !r.path.eq_ignore_ascii_case(path));
279 }
280 pub fn sort_recent(&mut self) {
282 self.recent_assets.sort_by(|a, b| b.pinned.cmp(&a.pinned).then(b.last_used.cmp(&a.last_used)));
283 }
284 pub fn touch_recent_project(&mut self, path: &str) {
285 self.recent_projects.retain(|r| !r.eq_ignore_ascii_case(path));
286 self.recent_projects.insert(0, path.to_string());
287 self.recent_projects.truncate(20);
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 #[test]
295 fn recent_keeps_tags_and_pins() {
296 let mut s = Settings::default();
297 s.touch_recent("a.mp4");
298 s.recent_assets[0].tags.push("x".into());
299 s.recent_assets[0].pinned = true;
300 s.touch_recent("b.mp4");
301 s.touch_recent("A.MP4"); assert_eq!(s.recent_assets.len(), 2);
303 assert_eq!(s.recent_assets[0].path, "A.MP4");
304 assert!(s.recent_assets[0].pinned && s.recent_assets[0].tags == vec!["x"]);
305 s.remove_recent("b.mp4");
306 assert_eq!(s.recent_assets.len(), 1);
307 }
308
309 #[test]
310 fn effect_thumb_image_round_trips() {
311 assert!(Settings::default().effect_thumb_image.is_empty(), "empty = the embedded default");
312 let mut s = Settings::default();
313 s.effect_thumb_image = r"C:\pics\stock.png".into();
314 let back: Settings = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
315 assert_eq!(back.effect_thumb_image, s.effect_thumb_image);
316 let old: Settings = serde_json::from_str("{}").unwrap();
318 assert!(old.effect_thumb_image.is_empty());
319 }
320
321 #[test]
322 fn palette_override_round_trips() {
323 assert_eq!(Settings::default().palette, PaletteOverride::default(), "unset = today's behaviour");
324 let mut s = Settings::default();
325 s.palette.mode = "custom".into();
326 s.palette.accent = Some([200, 30, 40]);
327 let back: Settings = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
328 assert_eq!(back.palette, s.palette);
329 let old: Settings = serde_json::from_str("{}").unwrap();
331 assert_eq!(old.palette, PaletteOverride::default());
332 }
333}