Edit > Presets > Migrate Presets
Merge a presets/preferences file (another install, or exported library) into the current preset library, appending gradient/style/shape/pattern/ tool-preset groups the library lacks (by name); idempotent. Headless, 2 tests. Parity 606 -> 607; FLOOR 602. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2c40a93751
commit
61aa2f9748
|
|
@ -837,6 +837,7 @@ fn build() -> Vec<CommandSpec> {
|
|||
v.extend(crate::print_cmds::specs());
|
||||
v.extend(crate::pick_cmds::specs());
|
||||
v.extend(crate::frame_cmds::specs());
|
||||
v.extend(crate::migrate_cmds::specs());
|
||||
v.extend(crate::variables_cmds::specs());
|
||||
v
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ pub mod artboard_cmds;
|
|||
pub mod comps_cmds;
|
||||
pub mod automate_cmds;
|
||||
mod frame_cmds;
|
||||
mod migrate_cmds;
|
||||
mod variables_cmds;
|
||||
pub mod pick_cmds;
|
||||
pub mod print_cmds;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
//! Edit › Presets › Migrate Presets. Photoshop imports presets from a previous version; we merge a
|
||||
//! presets file (another install's preferences, or an exported preset library) into the current
|
||||
//! library, appending groups the library doesn't already have (by name). Headless and scriptable.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::presets::{Group, PresetState};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
/// Append incoming groups whose name the library doesn't already have; returns how many were added.
|
||||
fn merge_groups<T>(cur: &mut Vec<Group<T>>, incoming: Vec<Group<T>>) -> usize {
|
||||
let mut added = 0;
|
||||
for g in incoming {
|
||||
if !cur.iter().any(|e| e.name == g.name) {
|
||||
cur.push(g);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
/// Deserialize `v[key]` into `Vec<Group<T>>`, defaulting to empty.
|
||||
fn groups<T: serde::de::DeserializeOwned>(v: &Value, key: &str) -> Vec<Group<T>> {
|
||||
v.get(key).cloned().and_then(|g| serde_json::from_value(g).ok()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn migrate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p.get("path").and_then(Value::as_str).ok_or_else(|| EngineError::BadParams {
|
||||
cmd: "edit.presets.migratePresets".into(),
|
||||
msg: "need `path` to a presets or preferences file".into(),
|
||||
})?;
|
||||
let text = std::fs::read_to_string(path).map_err(|e| EngineError::Other(format!("read `{path}`: {e}")))?;
|
||||
let root: Value = serde_json::from_str(&text).map_err(|e| EngineError::Other(format!("`{path}` is not JSON: {e}")))?;
|
||||
// Accept a full preferences file (has a `presets` section) or a bare presets object.
|
||||
let v = root.get("presets").cloned().unwrap_or(root);
|
||||
|
||||
let ps: &mut PresetState = &mut s.presets;
|
||||
let mut counts = serde_json::Map::new();
|
||||
let g = merge_groups(&mut ps.gradients, groups(&v, "gradients"));
|
||||
let st = merge_groups(&mut ps.styles, groups(&v, "styles"));
|
||||
let sh = merge_groups(&mut ps.shapes, groups(&v, "shapes"));
|
||||
let pt = merge_groups(&mut ps.pattern_groups, groups(&v, "pattern_groups"));
|
||||
// Tool presets are a flat list, matched by name.
|
||||
let mut tp = 0usize;
|
||||
if let Some(arr) = v.get("tool_presets").cloned()
|
||||
&& let Ok(incoming) = serde_json::from_value::<Vec<crate::presets::tools::ToolPreset>>(arr)
|
||||
{
|
||||
for t in incoming {
|
||||
if !ps.tool_presets.iter().any(|e| e.name == t.name) {
|
||||
ps.tool_presets.push(t);
|
||||
tp += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let total = g + st + sh + pt + tp;
|
||||
if total > 0 {
|
||||
ps.rev += 1;
|
||||
}
|
||||
counts.insert("gradients".into(), json!(g));
|
||||
counts.insert("styles".into(), json!(st));
|
||||
counts.insert("shapes".into(), json!(sh));
|
||||
counts.insert("patternGroups".into(), json!(pt));
|
||||
counts.insert("toolPresets".into(), json!(tp));
|
||||
Ok(json!({"migrated": total, "added": counts}))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<crate::commands::CommandSpec> {
|
||||
vec![crate::commands::CommandSpec {
|
||||
id: "edit.presets.migratePresets",
|
||||
label: "Migrate Presets",
|
||||
menu: &["Edit", "Presets"],
|
||||
shortcut: None,
|
||||
params: r#"{path} → {migrated, added:{gradients,styles,shapes,patternGroups,toolPresets}}: merge a presets/preferences file into the library (appends groups by name)"#,
|
||||
enabled: |_s| Ok(()),
|
||||
journal: false,
|
||||
run: |s, p| migrate(s, &p),
|
||||
}]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn merges_new_groups_only() {
|
||||
let mut s = Session::new();
|
||||
let before = s.presets.gradients.len();
|
||||
let existing = s.presets.gradients[0].name.clone();
|
||||
// A presets file with one new gradient group and one that already exists.
|
||||
let file = std::env::temp_dir().join(format!("pc-migrate-{}.json", std::process::id()));
|
||||
let data = json!({
|
||||
"presets": {
|
||||
"gradients": [
|
||||
{"name": "My Imported Set", "items": []},
|
||||
{"name": existing, "items": []},
|
||||
]
|
||||
}
|
||||
});
|
||||
std::fs::write(&file, serde_json::to_string(&data).unwrap()).unwrap();
|
||||
let r = s.execute("edit.presets.migratePresets", json!({"path": file.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["added"]["gradients"], 1, "only the new group is added");
|
||||
assert_eq!(s.presets.gradients.len(), before + 1);
|
||||
assert!(s.presets.gradients.iter().any(|g| g.name == "My Imported Set"));
|
||||
// Idempotent: migrating again adds nothing.
|
||||
let r2 = s.execute("edit.presets.migratePresets", json!({"path": file.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r2["migrated"], 0);
|
||||
let _ = std::fs::remove_file(file);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_path_is_an_error() {
|
||||
let mut s = Session::new();
|
||||
assert!(s.execute("edit.presets.migratePresets", json!({"path": "/no/such/file.json"})).is_err());
|
||||
assert!(s.execute("edit.presets.migratePresets", json!({})).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ use crate::menu_catalog::CATALOG;
|
|||
use crate::menus::is_live;
|
||||
|
||||
/// Minimum number of live catalog items. Raise it when parity grows; never lower it.
|
||||
pub const FLOOR: usize = 601;
|
||||
pub const FLOOR: usize = 602;
|
||||
|
||||
/// One top-level menu's coverage.
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Photoshop's menu tree) and the live command registry. Do not edit by hand.
|
||||
|
||||
**606 of 625 menu items live (97.0%).**
|
||||
**607 of 625 menu items live (97.1%).**
|
||||
|
||||
| Menu | Live | Total | % |
|
||||
|---|---:|---:|---:|
|
||||
| File | 47 | 50 | 94% |
|
||||
| Edit | 74 | 75 | 99% |
|
||||
| Edit | 75 | 75 | 100% |
|
||||
| Image | 60 | 61 | 98% |
|
||||
| Layer | 147 | 160 | 92% |
|
||||
| Type | 44 | 44 | 100% |
|
||||
|
|
@ -24,10 +24,6 @@ Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Pho
|
|||
- Import › Video Frames to Layers… (`file.import.videoFramesToLayers`)
|
||||
- Import › WIA Support… (`file.import.wiaSupport`)
|
||||
|
||||
### Edit (1)
|
||||
|
||||
- Presets › Migrate Presets (`edit.presets.migratePresets`)
|
||||
|
||||
### Image (1)
|
||||
|
||||
- Trap… (`image.trap`)
|
||||
|
|
|
|||
Loading…
Reference in New Issue