Merge branch 'claude/fix-942-cube-import-messages'
# Conflicts: # docs/parity-checklist.md
This commit is contained in:
commit
b89fb8bbab
|
|
@ -34,22 +34,34 @@ pub fn register_all(s: &Session) {
|
|||
}
|
||||
}
|
||||
|
||||
/// `.cube` files in `paths` (folders searched, zip bundles opened).
|
||||
fn cube_files(paths: &[String]) -> Vec<(String, String, Option<String>)> {
|
||||
/// `.cube` files in `paths` (folders searched, zip bundles opened), and the files that couldn't
|
||||
/// be read with the reason (`[name, reason]`).
|
||||
fn cube_files(paths: &[String]) -> (Vec<(String, String, Option<String>)>, Vec<Value>) {
|
||||
// (name, text, group)
|
||||
let mut out = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
let stem = |f: &Path| f.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
for p in paths {
|
||||
let path = Path::new(p);
|
||||
if path.is_dir() {
|
||||
let cube = |f: &Path| f.extension().is_some_and(|x| x.eq_ignore_ascii_case("cube"));
|
||||
for ep in crate::walk::files_in(path, None, crate::walk::Limits::default(), cube).files {
|
||||
if let Ok(t) = std::fs::read_to_string(&ep) {
|
||||
let group = ep.parent().and_then(|d| d.file_name()).map(|n| n.to_string_lossy().to_string());
|
||||
out.push((ep.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(), t, group));
|
||||
match std::fs::read_to_string(&ep) {
|
||||
Ok(t) => {
|
||||
let group = ep.parent().and_then(|d| d.file_name()).map(|n| n.to_string_lossy().to_string());
|
||||
out.push((stem(&ep), t, group));
|
||||
}
|
||||
Err(e) => failed.push(json!([stem(&ep), e.to_string()])),
|
||||
}
|
||||
}
|
||||
} else if path.extension().is_some_and(|x| x.eq_ignore_ascii_case("zip")) {
|
||||
let Ok(b) = std::fs::read(path) else { continue };
|
||||
let b = match std::fs::read(path) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
failed.push(json!([stem(path), e.to_string()]));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for (inner, data) in crate::preset_import::read_zip(&b).unwrap_or_default() {
|
||||
if inner.to_ascii_lowercase().ends_with(".cube") {
|
||||
let ip = Path::new(&inner);
|
||||
|
|
@ -65,11 +77,14 @@ fn cube_files(paths: &[String]) -> Vec<(String, String, Option<String>)> {
|
|||
));
|
||||
}
|
||||
}
|
||||
} else if let Ok(t) = std::fs::read_to_string(path) {
|
||||
out.push((path.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(), t, None));
|
||||
} else {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(t) => out.push((stem(path), t, None)),
|
||||
Err(e) => failed.push(json!([stem(path), e.to_string()])),
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
(out, failed)
|
||||
}
|
||||
|
||||
fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -83,8 +98,9 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(d) = &dir {
|
||||
std::fs::create_dir_all(d).map_err(|e| bad(C, format!("{}: {e}", d.display())))?;
|
||||
}
|
||||
let (mut imported, mut failed) = (Vec::new(), Vec::new());
|
||||
for (name, text, group) in cube_files(&paths) {
|
||||
let (files, mut failed) = cube_files(&paths);
|
||||
let mut imported = Vec::new();
|
||||
for (name, text, group) in files {
|
||||
let lut = match lightcraft_pipeline::lut::Lut3d::parse_cube(&text) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ mod tests_open_lightroom;
|
|||
#[cfg(test)]
|
||||
mod tests_panels;
|
||||
#[cfg(test)]
|
||||
mod tests_preset_import;
|
||||
#[cfg(test)]
|
||||
mod tests_preview_limit;
|
||||
#[cfg(test)]
|
||||
mod tests_quit_unsaved;
|
||||
|
|
|
|||
|
|
@ -1494,64 +1494,100 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
l.ends_with(".cube") || l.ends_with(".zip") || std::path::Path::new(p.as_str()).is_dir()
|
||||
})
|
||||
.collect();
|
||||
let profiles = if lut_paths.is_empty() {
|
||||
0
|
||||
} else {
|
||||
app.session
|
||||
.execute("profile.import", &json!({"paths": lut_paths}))
|
||||
.ok()
|
||||
.and_then(|v| v["imported"].as_array().map(Vec::len))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
// one toast for both halves (a second toast would replace the first)
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
// `name: reason` for each file that couldn't be imported
|
||||
let mut problems: Vec<String> = Vec::new();
|
||||
let mut profiles_result = Value::Null;
|
||||
if !lut_paths.is_empty() {
|
||||
match app.session.execute("profile.import", &json!({"paths": lut_paths})) {
|
||||
Ok(v) => {
|
||||
let profiles = v["imported"].as_array().map_or(0, Vec::len);
|
||||
if profiles > 0 {
|
||||
parts.push(crate::i18n::tr_format!(
|
||||
"Imported {profiles} profile{} (Profile browser ▸ their groups)",
|
||||
if profiles == 1 { "" } else { "s" },
|
||||
profiles = profiles
|
||||
));
|
||||
}
|
||||
problems.extend(v["failed"].as_array().into_iter().flatten().map(|f| {
|
||||
let name = f.get(0).and_then(Value::as_str).unwrap_or_default();
|
||||
let reason = f.get(1).and_then(Value::as_str).unwrap_or_default();
|
||||
format!("{name}: {reason}")
|
||||
}));
|
||||
profiles_result = v;
|
||||
}
|
||||
Err(e) => {
|
||||
problems.push(e.to_string());
|
||||
profiles_result = json!({"error": e.to_string()});
|
||||
}
|
||||
}
|
||||
}
|
||||
let preset_paths: Vec<&String> = paths.iter().filter(|p| !p.to_ascii_lowercase().ends_with(".cube")).collect();
|
||||
let r = if preset_paths.is_empty() {
|
||||
Ok(json!({"imported": [], "failed": [], "skipped": 0}))
|
||||
} else {
|
||||
app.session.execute("preset.import", &json!({"paths": preset_paths})).map_err(|e| e.to_string())
|
||||
};
|
||||
if profiles > 0 {
|
||||
app.toast(
|
||||
&ctx,
|
||||
crate::i18n::tr_format!(
|
||||
"Imported {profiles} profile{} (Profile browser ▸ their groups)",
|
||||
if profiles == 1 { "" } else { "s" },
|
||||
profiles = profiles
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Ok(v) = &r {
|
||||
if let (false, Ok(v)) = (preset_paths.is_empty(), &r) {
|
||||
let n = v["imported"].as_array().map_or(0, Vec::len);
|
||||
let failed = v["failed"].as_array().map_or(0, Vec::len);
|
||||
let mut msg = match (n, failed) {
|
||||
(0, 0) => "No new presets".to_string(),
|
||||
(n, 0) => crate::i18n::tr_format!("Imported {n} preset{}", if n == 1 { "" } else { "s" }, n = n),
|
||||
(n, f) => crate::i18n::tr_format!(
|
||||
let profile_news = !parts.is_empty() || !problems.is_empty();
|
||||
let msg = match (n, failed) {
|
||||
// a zip of LUTs only: the profile half already says what happened
|
||||
(0, 0) if profile_news => None,
|
||||
(0, 0) => Some("No new presets".to_string()),
|
||||
(n, 0) => Some(crate::i18n::tr_format!("Imported {n} preset{}", if n == 1 { "" } else { "s" }, n = n)),
|
||||
(n, f) => Some(crate::i18n::tr_format!(
|
||||
"Imported {n} preset{}, {f} file{} not readable",
|
||||
if n == 1 { "" } else { "s" },
|
||||
if f == 1 { "" } else { "s" },
|
||||
f = f,
|
||||
n = n
|
||||
),
|
||||
)),
|
||||
};
|
||||
// settings with no counterpart here (the other editor's profiles, masks…)
|
||||
let mut skipped: Vec<&str> = v["imported"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|i| i["unmapped"].as_array().into_iter().flatten())
|
||||
.filter_map(Value::as_str)
|
||||
.collect();
|
||||
skipped.sort_unstable();
|
||||
skipped.dedup();
|
||||
if !skipped.is_empty() {
|
||||
let names: Vec<&str> = skipped.iter().take(3).copied().collect();
|
||||
msg += &crate::i18n::tr_format!(" — not carried over: {}{}", names.join(", "), if skipped.len() > 3 { "…" } else { "" });
|
||||
if let Some(mut msg) = msg {
|
||||
// settings with no counterpart here (the other editor's profiles, masks…)
|
||||
let mut skipped: Vec<&str> = v["imported"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|i| i["unmapped"].as_array().into_iter().flatten())
|
||||
.filter_map(Value::as_str)
|
||||
.collect();
|
||||
skipped.sort_unstable();
|
||||
skipped.dedup();
|
||||
if !skipped.is_empty() {
|
||||
let names: Vec<&str> = skipped.iter().take(3).copied().collect();
|
||||
msg += &crate::i18n::tr_format!(" — not carried over: {}{}", names.join(", "), if skipped.len() > 3 { "…" } else { "" });
|
||||
}
|
||||
parts.push(msg);
|
||||
}
|
||||
app.toast(&ctx, msg);
|
||||
if n > 0 {
|
||||
app.ui.presets = true;
|
||||
}
|
||||
}
|
||||
let mut msg = parts.join(" · ");
|
||||
if !problems.is_empty() {
|
||||
let detail =
|
||||
format!("{}{}", problems.iter().take(2).cloned().collect::<Vec<_>>().join("; "), if problems.len() > 2 { "…" } else { "" });
|
||||
if msg.is_empty() {
|
||||
msg = crate::i18n::tr_format!("Import failed: {e}", e = detail);
|
||||
} else {
|
||||
msg += &crate::i18n::tr_format!(" · {} failed: {}", problems.len(), detail);
|
||||
}
|
||||
app.toast_error(&ctx, msg);
|
||||
} else if !msg.is_empty() {
|
||||
app.toast(&ctx, msg);
|
||||
}
|
||||
let r = r.map(|mut v| {
|
||||
if !lut_paths.is_empty()
|
||||
&& let Some(o) = v.as_object_mut()
|
||||
{
|
||||
o.insert("profiles".into(), profiles_result);
|
||||
}
|
||||
v
|
||||
});
|
||||
return Some(r);
|
||||
}
|
||||
"file.importKeywords" => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
//! File ▸ Import Profiles & Presets…: `.cube` LUTs become profiles, everything else presets, and
|
||||
//! one toast says what happened to both (issue #942).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::{LightcraftApp, Services};
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
const SETTLE: Duration = Duration::from_secs(120);
|
||||
|
||||
fn app() -> Headless {
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
h.settle(SETTLE);
|
||||
h
|
||||
}
|
||||
|
||||
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("lc-ui-preset-import-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn import(h: &mut Headless, paths: &[&std::path::Path]) -> serde_json::Value {
|
||||
let paths: Vec<String> = paths.iter().map(|p| p.to_string_lossy().to_string()).collect();
|
||||
let r = h.request("engine.execute", json!({"command": "file.importPresets", "params": {"paths": paths}}), T);
|
||||
h.settle(SETTLE);
|
||||
r
|
||||
}
|
||||
|
||||
fn toast(h: &Headless) -> String {
|
||||
h.app.ui.toast.clone().map(|t| t.0).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A 2×2×2 identity LUT.
|
||||
fn identity_cube() -> String {
|
||||
let mut s = String::from("TITLE \"Identity\"\nLUT_3D_SIZE 2\n");
|
||||
for b in 0..2 {
|
||||
for g in 0..2 {
|
||||
for r in 0..2 {
|
||||
s += &format!("{r} {g} {b}\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Importing only `.cube` files reports the profiles, not "No new presets".
|
||||
#[test]
|
||||
fn a_cube_only_import_reports_the_profiles() {
|
||||
let dir = temp_dir("ok");
|
||||
let cube = dir.join("Warm.cube");
|
||||
std::fs::write(&cube, identity_cube()).unwrap();
|
||||
let mut h = app();
|
||||
let r = import(&mut h, &[&cube]);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let t = toast(&h);
|
||||
assert!(t.contains("1 profile"), "{t}");
|
||||
assert!(!t.contains("preset"), "{t}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A `.cube` that can't be imported says which file and why.
|
||||
#[test]
|
||||
fn a_broken_cube_says_why() {
|
||||
let dir = temp_dir("bad");
|
||||
let bad = dir.join("Broken.cube");
|
||||
std::fs::write(&bad, "LUT_3D_SIZE 2\n0 0 0\n").unwrap();
|
||||
let mut h = app();
|
||||
let r = import(&mut h, &[&bad]);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let t = toast(&h);
|
||||
assert!(t.contains("Broken"), "{t}");
|
||||
assert!(!t.contains("No new presets"), "{t}");
|
||||
let reason = r["result"]["profiles"]["failed"][0][1].as_str().unwrap_or_default().to_string();
|
||||
assert!(!reason.is_empty() && t.contains(&reason), "{t} / {r}");
|
||||
// with a good one alongside, both outcomes are in the one toast
|
||||
let good = dir.join("Good.cube");
|
||||
std::fs::write(&good, identity_cube()).unwrap();
|
||||
import(&mut h, &[&good, &bad]);
|
||||
let t = toast(&h);
|
||||
assert!(t.contains("1 profile") && t.contains("Broken"), "{t}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue