Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
bflatastic
2026-10-29 23:21:59 +07:00
310 changed files with 14537 additions and 6017 deletions
+5 -1
View File
@@ -28,6 +28,7 @@ pub use choices::{APPEARANCE_MODES, DARK_THEMES, LANGUAGES, LIGHT_THEMES, THEMES
pub use dialog::{Item, Kind, Page, pages, pages_json, todo_keys};
pub use document::{migrate, page_id, section_key};
pub use queries::{CacheBudgets, WEB_LAYER_CACHE, WEB_MEDIA_CACHE, mask_color};
pub use recent::{MAX_RECENT_SCRIPTS, RecentScript};
pub use sections::{
Appearance, Audio, AutoSave, CompPreset, CompositionPrefs, Disk, Export, FacePrefs, General, Grids, Import, LabelPref, Memory, Previews, ProjectPrefs,
RotoPrefs, Scripting, Startup, ThreeD, TypePrefs, Video,
@@ -35,7 +36,7 @@ pub use sections::{
use sections::{Extra, default_labels, lenient_comp_presets};
/// Current settings layout version.
pub const PREFS_VERSION: u32 = 2;
pub const PREFS_VERSION: u32 = 3;
/// Settings file name in the config store.
pub const PREFS_FILE: &str = "prefs.json";
@@ -74,6 +75,8 @@ pub struct Prefs {
/// Animation ▸ Recent Animation Presets (`.ecpreset` paths or built-in preset ids), newest
/// first.
pub recent_presets: Vec<String>,
/// File ▸ Scripts ▸ Recent Script Files, newest first, at most [`MAX_RECENT_SCRIPTS`].
pub recent_scripts: Vec<RecentScript>,
/// Character panel: recently used font families, newest first.
pub recent_fonts: Vec<String>,
/// Effects & Presets: the starred effects' ids, in the order starred.
@@ -117,6 +120,7 @@ impl Default for Prefs {
recent_projects: vec![],
recent_footage: vec![],
recent_presets: vec![],
recent_scripts: vec![],
recent_fonts: vec![],
effects_favorites: vec![],
preview: crate::preview::PreviewSettings::default(),
+1 -1
View File
@@ -1,5 +1,5 @@
use super::choices::{APPEARANCE_MODES, DARK_THEMES, LANGUAGES, LIGHT_THEMES, WINDOW_GRAPHICS};
use super::*;
use super::{Value, json};
// ---------------------------------------------------------------- schema (Settings dialog)
+12 -2
View File
@@ -1,12 +1,14 @@
use super::choices::{APPEARANCE_MODES, DARK_THEMES, LANGUAGES, LIGHT_THEMES, THEMES, WINDOW_GRAPHICS, choices, is_choice, theme_family};
use super::recent::MAX_EFFECTS_FAVORITES;
use super::recent::{MAX_EFFECTS_FAVORITES, MAX_RECENT_SCRIPTS};
use super::sections::{CompPreset, default_labels, parse_hex};
use super::*;
use super::{Appearance, General, Map, PREFS_VERSION, Prefs, Startup, Value, json};
/// Bring an older settings document up to [`PREFS_VERSION`].
///
/// - version 0/1 (early builds) kept a flat object: `undoLevels`, `autoSaveEnabled`,
/// `autoSaveMinutes`, `autoSaveVersions`, `labelNames` (array) and `theme` at the top level.
/// - version 2 defaulted the disk cache to 100 GB, more than most laptops have free; the stored
/// old default becomes the new one (a size the user chose is kept).
pub fn migrate(mut v: Value) -> Value {
let Some(obj) = v.as_object_mut() else { return json!({}) };
let version = obj.get("version").and_then(Value::as_u64).unwrap_or(0);
@@ -34,6 +36,12 @@ pub fn migrate(mut v: Value) -> Value {
obj.insert("labels".into(), serde_json::to_value(labels).unwrap_or_default());
}
}
if version < 3
&& let Some(d) = obj.get_mut("disk").and_then(Value::as_object_mut)
&& d.get("diskCacheMaxGb").and_then(Value::as_u64) == Some(100)
{
d.insert("diskCacheMaxGb".into(), json!(20));
}
// Settings saved before appearance modes had one theme: keep showing it, as a fixed Dark or
// Light mode, instead of switching established users to Auto.
if let Some(a) = obj.get_mut("appearance").and_then(Value::as_object_mut)
@@ -138,6 +146,7 @@ impl Prefs {
self.grids.grid_spacing = self.grids.grid_spacing.clamp(1.0, 10_000.0);
let n = self.general.recent_items as usize;
self.recent_projects.truncate(n);
self.recent_scripts.truncate(MAX_RECENT_SCRIPTS);
}
/// The value at a dotted key (`general.undoLevels`, `labels.3.color`); `""` = everything.
@@ -203,6 +212,7 @@ impl Prefs {
recent_projects: std::mem::take(&mut self.recent_projects),
recent_footage: std::mem::take(&mut self.recent_footage),
recent_presets: std::mem::take(&mut self.recent_presets),
recent_scripts: std::mem::take(&mut self.recent_scripts),
recent_fonts: std::mem::take(&mut self.recent_fonts),
effects_favorites: std::mem::take(&mut self.effects_favorites),
preview: std::mem::take(&mut self.preview),
+1 -1
View File
@@ -1,5 +1,5 @@
use super::sections::parse_hex;
use super::*;
use super::{Label, Prefs, Serialize, Tick};
impl Prefs {
/// Display name of a label (Labels settings).
+31 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use serde::{Deserialize, Serialize};
use super::Prefs;
impl Prefs {
/// Remember a project in File ▸ Open Recent.
@@ -45,6 +47,34 @@ impl Prefs {
pub fn push_recent_preset(&mut self, path: &str) {
push_mru(&mut self.recent_presets, path, self.general.recent_items as usize);
}
/// Remember a script that ran for File ▸ Scripts ▸ Recent Script Files.
pub fn push_recent_script(&mut self, script: RecentScript) {
self.recent_scripts.retain(|x| *x != script);
self.recent_scripts.insert(0, script);
self.recent_scripts.truncate(MAX_RECENT_SCRIPTS);
}
}
/// Most scripts File ▸ Scripts ▸ Recent Script Files lists.
pub const MAX_RECENT_SCRIPTS: usize = 10;
/// A File ▸ Scripts ▸ Recent Script Files entry, stored as the `file.runScript` parameter that
/// ran it: `{"path": …}` for a script file, `{"name": …}` for an installed or bundled script.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RecentScript {
Path(String),
Name(String),
}
impl RecentScript {
/// The file's path or the script's name.
pub fn text(&self) -> &str {
match self {
RecentScript::Path(s) | RecentScript::Name(s) => s,
}
}
}
/// Most effects Effects & Presets keeps starred (a hand-edited settings file can hold more).
+2 -2
View File
@@ -1,5 +1,5 @@
use super::choices::theme_family;
use super::*;
use super::{BTreeMap, Deserialize, Label, Serialize, Value};
/// Unknown keys inside a page, kept so a newer version's settings survive a round trip.
pub(super) type Extra = BTreeMap<String, Value>;
@@ -179,7 +179,7 @@ page!(Audio {
page!(Disk {
disk_cache_enabled: bool = true,
disk_cache_max_gb: u32 = 100,
disk_cache_max_gb: u32 = 20,
disk_cache_folder: String = String::new(),
media_cache_folder: String = String::new(),
conformed_media_folder: String = String::new(),
+25 -1
View File
@@ -82,7 +82,9 @@ impl Session {
/// Take the latest reading of the system's memory and start the next one in the background
/// (desktop frontends call this every few seconds; it never waits for the system), then
/// re-apply the cache budgets when the reading changes them. Returns the effective budgets.
/// The disk cache's free disk space is read the same way (it keeps a reserve free).
pub fn memory_tick(&mut self) -> crate::prefs::CacheBudgets {
self.poll_disk_space();
let before = self.prefs.cache_budgets(self.sys_memory);
self.sys_memory = self.memory_watch.latest().or(self.sys_memory);
self.memory_watch.poll();
@@ -98,6 +100,19 @@ impl Session {
after
}
/// Read the disk cache's free disk space in the background and apply it there (the reserve
/// it keeps free): the free space and the cache's size are taken at the same moment, and the
/// reader holds its own cache, so a reading never reaches another folder's.
fn poll_disk_space(&mut self) {
if let Some(dc) = &self.disk_cache {
let dc = dc.clone();
self.disk_watch.poll_with("ec-disk-space", move || {
dc.set_disk_space(crate::sysinfo::disk_space(dc.folder()));
Some(())
});
}
}
/// 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).
@@ -113,6 +128,7 @@ impl Session {
// (no file system in the browser: the disk cache stays off there)
let Some(folder) = folder.filter(|_| d.disk_cache_enabled && !cfg!(target_arch = "wasm32")) else {
self.disk_cache = None;
self.disk_watch = Default::default();
self.layer_cache.set_disk(None);
return;
};
@@ -126,17 +142,25 @@ impl Session {
c.set_max_bytes(max);
c.clone()
}
// A new folder: the free-space reading was for the old one (a reader still running
// holds the old watcher's state, so it can't write into the new one).
_ => match DiskCache::open(&folder, max) {
Ok(c) => c,
Ok(c) => {
self.disk_watch = Default::default();
c
}
Err(e) => {
log::warn!("disk cache at {}: {e}", folder.display());
self.disk_cache = None;
self.disk_watch = Default::default();
self.layer_cache.set_disk(None);
return;
}
},
};
self.disk_cache = Some(dc.clone());
// The reserve applies from the start (a new folder's watcher is idle).
self.poll_disk_space();
let salt = self.refresh_disk_salt(true).unwrap_or_default();
self.layer_cache.set_disk(Some((dc.clone(), salt)));
// Roto Brush / Object Matte segmentations persist there too (held weakly: switching the
+15
View File
@@ -105,6 +105,21 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
&mut out,
);
}
// File ▸ Scripts ▸ Recent Script Files lists its scripts at run time; the first one, the most
// recent, has a key.
let recent = json!({"recent": 0});
push(
Bindable {
key: binding_key("file.runScript", &recent),
command: "file.runScript".into(),
params: recent,
label: "Run Most Recent Script File".into(),
path: vec!["File".into(), "Scripts".into(), "Recent Script Files".into()],
scope: APP_SCOPE.into(),
defaults: normalize("Cmd+Alt+Shift+D").into_iter().collect(),
},
&mut out,
);
for c in crate::command_specs() {
if !c.journal || entries.iter().any(|(_, e)| e.command == c.id) {
continue;