94e1c86d89
- effectcraft-psd (L1): PSD/PSB reader from Adobe's published file format spec (layers, groups, masks, blend modes, text engine data, lfx2 effects, fill/adjustment layers, vector masks; 1/8/16/32-bit, RGB/CMYK/Gray/Lab/Indexed, raw/RLE/ZIP) and a minimal writer for fixtures. - File > Import: importAs footage|composition|compositionLayerSizes, layer (Choose Layer); groups -> precomps (pass-through collapses), text -> editable text, styles -> Layer Styles, adjustments -> adjustment layers with effects; UI asks the Import Kind for Photoshop files. - effectcraft-svg (L2): SVG parser (shapes, paths, transforms, use, CSS, gradients) and rasteriser; .svg imports as footage, continuously rasterised when the switch is on. - layer.create: shapesFromVector, shapesFromText, masksFromText, editableText (stub removed). - Disk cache (render::disk_cache): LZ4 content-keyed frames and layer buffers, LRU size limit from prefs, atomic writes, restart persistence; edit.purge clears it, cache.diskStats query; viewer frames read/write it and the timeline shows disk-only frames in blue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
7.6 KiB
Rust
190 lines
7.6 KiB
Rust
//! Session side of Settings, keyboard shortcut presets, auto-save and crash recovery.
|
|
|
|
use std::path::Path;
|
|
|
|
use crate::autosave::{self, Sentinel};
|
|
use crate::prefs::{PREFS_FILE, Prefs};
|
|
use crate::shortcuts::{Keymaps, SHORTCUTS_FILE, ShortcutTable, UiCommand};
|
|
use crate::{EngineError, Result, Session};
|
|
|
|
impl Session {
|
|
/// Load settings and shortcut presets from the config store and apply them.
|
|
pub fn load_settings(&mut self) {
|
|
if let Some(c) = &self.config {
|
|
if let Some(t) = c.read(PREFS_FILE) {
|
|
self.prefs = Prefs::from_json(&t);
|
|
}
|
|
if let Some(t) = c.read(SHORTCUTS_FILE) {
|
|
self.keymaps = Keymaps::from_json(&t);
|
|
}
|
|
}
|
|
self.prefs_changed();
|
|
self.shortcuts_changed();
|
|
}
|
|
|
|
/// Settings changed: apply what the engine owns (cache budgets) and tell frontends.
|
|
pub fn prefs_changed(&mut self) {
|
|
self.prefs.normalize();
|
|
self.layer_cache.set_budget(self.prefs.layer_cache_bytes());
|
|
self.footage.set_cache_budget(self.prefs.media_cache_bytes());
|
|
self.configure_disk_cache();
|
|
// Fewer undo levels apply right away.
|
|
let levels = self.prefs.general.undo_levels.max(1) as usize;
|
|
if self.history.undo.len() > levels {
|
|
let extra = self.history.undo.len() - levels;
|
|
self.history.undo.drain(..extra);
|
|
}
|
|
self.prefs_revision += 1;
|
|
}
|
|
|
|
/// Apply Settings ▸ Media & Disk Cache ▸ Disk Cache: open (or close) the persistent cache
|
|
/// and attach it to the layer cache. Without a folder setting the platform cache folder is
|
|
/// used, but only by frontends with a settings store (headless sessions stay off disk).
|
|
pub fn configure_disk_cache(&mut self) {
|
|
use effectcraft_render::disk_cache::{DiskCache, default_folder, footage_salt};
|
|
let d = &self.prefs.disk;
|
|
let folder = if d.disk_cache_folder.trim().is_empty() {
|
|
self.config.is_some().then(default_folder)
|
|
} else {
|
|
Some(std::path::PathBuf::from(d.disk_cache_folder.trim()))
|
|
};
|
|
let max = d.disk_cache_max_gb.max(1) as u64 * (1 << 30);
|
|
let Some(folder) = folder.filter(|_| d.disk_cache_enabled) else {
|
|
self.disk_cache = None;
|
|
self.layer_cache.set_disk(None);
|
|
return;
|
|
};
|
|
let dc = match &self.disk_cache {
|
|
Some(c) if c.folder() == folder.as_path() => {
|
|
c.set_max_bytes(max);
|
|
c.clone()
|
|
}
|
|
_ => match DiskCache::open(&folder, max) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::warn!("disk cache at {}: {e}", folder.display());
|
|
self.disk_cache = None;
|
|
self.layer_cache.set_disk(None);
|
|
return;
|
|
}
|
|
},
|
|
};
|
|
self.layer_cache.set_disk(Some((dc.clone(), footage_salt(&self.project))));
|
|
self.disk_cache = Some(dc);
|
|
}
|
|
|
|
/// Write settings to the config store.
|
|
pub fn save_prefs(&self) {
|
|
if let Some(c) = &self.config
|
|
&& let Err(e) = c.write(PREFS_FILE, &self.prefs.to_json())
|
|
{
|
|
log::warn!("saving settings: {e}");
|
|
}
|
|
}
|
|
|
|
/// Shortcut presets changed: rebuild the table and save them.
|
|
pub fn shortcuts_changed(&mut self) {
|
|
self.shortcut_table = std::sync::OnceLock::new();
|
|
if let Some(c) = &self.config
|
|
&& let Err(e) = c.write(SHORTCUTS_FILE, &self.keymaps.to_json())
|
|
{
|
|
log::warn!("saving keyboard shortcuts: {e}");
|
|
}
|
|
}
|
|
|
|
/// The frontend's own bindable commands (tools, timeline navigation…).
|
|
pub fn set_ui_commands(&mut self, cmds: Vec<UiCommand>) {
|
|
self.ui_commands = cmds;
|
|
self.shortcut_table = std::sync::OnceLock::new();
|
|
}
|
|
|
|
/// The active shortcut preset resolved against every bindable command.
|
|
pub fn shortcuts(&self) -> &ShortcutTable {
|
|
self.shortcut_table.get_or_init(|| ShortcutTable::build(&self.keymaps, &self.ui_commands))
|
|
}
|
|
|
|
/// Remember a project in File ▸ Open Recent and in the crash-recovery sentinel.
|
|
pub(crate) fn note_project_path(&mut self, path: &str) {
|
|
self.prefs.push_recent(path);
|
|
self.save_prefs();
|
|
self.autosave = autosave::AutoSaveState { last: self.autosave.last, sentinel: self.autosave.sentinel, ..Default::default() };
|
|
self.update_sentinel();
|
|
}
|
|
|
|
/// Refresh the crash-recovery sentinel (when one is active).
|
|
pub fn update_sentinel(&self) {
|
|
if let Some(c) = &self.config
|
|
&& self.autosave.sentinel
|
|
{
|
|
let s = Sentinel { pid: autosave::pid(), project: self.path.clone(), autosave: self.autosave.last_path.clone(), dirty: self.is_dirty() };
|
|
autosave::update(c.as_ref(), &s);
|
|
}
|
|
}
|
|
|
|
/// Fallback folder for auto-saves of untitled projects (next to the settings).
|
|
pub fn default_autosave_root(&self) -> Option<std::path::PathBuf> {
|
|
self.config.as_ref().and_then(|c| c.dir())
|
|
}
|
|
|
|
/// Write an auto-save now (whether or not the project is dirty). Returns its path.
|
|
pub fn autosave_now(&mut self) -> Result<String> {
|
|
let json = self.project.to_json();
|
|
let root = self.default_autosave_root();
|
|
let (path, slot) = autosave::write(&self.prefs, self.path.as_deref(), root.as_deref(), self.autosave.last_slot, &json)
|
|
.map_err(|e| EngineError::Other(format!("auto-save failed: {e}")))?;
|
|
let p = path.to_string_lossy().to_string();
|
|
self.autosave.last_slot = Some(slot);
|
|
self.autosave.saved_revision = Some(self.revision);
|
|
self.autosave.last_path = Some(p.clone());
|
|
self.update_sentinel();
|
|
Ok(p)
|
|
}
|
|
|
|
/// Call periodically with wall-clock seconds: auto-saves a dirty project once the interval
|
|
/// has passed since the last auto-save (or since the first call). Returns the written path.
|
|
pub fn autosave_tick(&mut self, now: f64) -> Option<Result<String>> {
|
|
let a = &self.prefs.auto_save;
|
|
if !a.enabled {
|
|
self.autosave.last = Some(now);
|
|
return None;
|
|
}
|
|
let interval = a.interval_minutes.max(1) as f64 * 60.0;
|
|
let last = *self.autosave.last.get_or_insert(now);
|
|
if now - last < interval {
|
|
return None;
|
|
}
|
|
self.autosave.last = Some(now);
|
|
if !self.is_dirty() || self.autosave.saved_revision == Some(self.revision) || self.render_job.is_some() {
|
|
return None;
|
|
}
|
|
if self.path.is_none() && self.prefs.auto_save.location != "custom" && self.default_autosave_root().is_none() {
|
|
return None;
|
|
}
|
|
let r = self.autosave_now();
|
|
if let Ok(p) = &r {
|
|
self.toast(format!("Auto-saved {}", Path::new(p).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default()));
|
|
}
|
|
Some(r)
|
|
}
|
|
|
|
/// At launch: start crash recovery. Returns the previous run's state when it didn't exit
|
|
/// cleanly (and the setting asks for it).
|
|
pub fn begin_recovery(&mut self) -> Option<autosave::Recovery> {
|
|
let c = self.config.clone()?;
|
|
let r = autosave::begin(c.as_ref(), &self.prefs);
|
|
self.autosave.sentinel = true;
|
|
self.update_sentinel();
|
|
r.filter(|_| self.prefs.startup.offer_crash_recovery)
|
|
}
|
|
|
|
/// Clean exit: remove the crash-recovery sentinel.
|
|
pub fn end_recovery(&mut self) {
|
|
if let Some(c) = &self.config
|
|
&& self.autosave.sentinel
|
|
{
|
|
autosave::end(c.as_ref());
|
|
self.autosave.sentinel = false;
|
|
}
|
|
}
|
|
}
|