diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 0eafa49a5..39e81db90 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -987,7 +987,7 @@ pub fn specs() -> Vec { "Auto Import Now", [], None, - "{listing?: [[path, size]] (the folder as listed by the caller, e.g. on a worker thread), start?: bool (false: return the `library.import` params as `import` instead of importing)} — add the watched folder's new photos (files the library doesn't have yet; partial / still-copying files wait for the next scan) → {imported, folder, import?}", + "{listing?: [[path, size]] (the folder as listed by the caller, e.g. on a worker thread), start?: bool (false: return the `library.import` params as `import` instead of importing)} — add the watched folder's new photos (files the library doesn't have yet; partial / still-copying files wait for the next scan) → {imported, folder, warnings?: [{path, code, profile?, mode?, fields?}], import?}", always, |s, p| { let Some(folder) = s.import_defaults.auto_folder.clone() else { return Ok(json!({"imported": [], "folder": null})) }; @@ -1041,7 +1041,11 @@ pub fn specs() -> Vec { let r = s.execute("library.import", ¶ms)?; // arriving photos don't take over the selection s.selection = sel; - Ok(json!({"imported": r["imported"], "folder": folder})) + let mut result = json!({"imported": r["imported"], "folder": folder}); + if let Some(warnings) = r.get("warnings") { + result["warnings"] = warnings.clone(); + } + Ok(result) } ), cmd!("album.removePhotos", "Remove from Album", [], None, "{id: albumId, ids?}", has_selection, |s, p| { @@ -1103,7 +1107,7 @@ pub fn specs() -> Vec { "Import Photos", ["File"], Some("Cmd+Shift+I"), - "{paths: [file or folder (recursive)], mode?: add|copy|move (add = reference the files in place; copy = into the library's Originals/YYYY/YYYY-MM-DD/; move = as copy, then each original and its XMP sidecars are removed from the source — only after the copy is verified (a hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate and unchecked files keep their sources; a taken name gets -1, -2…; undo removes the photos from the library but leaves the files at the destination), destination?: folder for copies / moves, organize?: date (YYYY/YYYY-MM-DD) | month (YYYY/YYYY-MM) | flat | a folder template, e.g. `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 (the template's `/` make the folders, each level expanded with the rename tokens and made a safe folder name: never outside the destination; must be relative, no `..`; a level with missing metadata is `unknown`) — dated by capture time, else the import time, rename?: file-name template for copies, original extension added (tokens: {name} {num} {seq} {seq:N} {date} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext}; photo.renameTokens explains each; blank = keep names), renameStart?: 1, metadataPreset?: name, dng?: bool (copy raws as DNG; copy only), local?: bool (browsing: the photos stay out of the library, like library.browse; not with move), album?: albumId, albumName?: new album, preset?: presetId, keywords?: [..], onDeleted?: skip|restore|fresh (a file that is in Recently Deleted: skip = leave it there and say so, restore = bring the photo back with its edits, fresh = delete the trashed record and import the file as new)} → {imported, duplicates: [{path, existing, reason, existingDeleted?}], restored?: [photoId], failed, warnings?: [{path, code: cameraProfile|uprightGeometry|unmappedXmp, profile?, fields?}], moved?: [{from, to, sidecars?}], kept?: [{path, reason}] (move: sources left in place and why), album?}", + "{paths: [file or folder (recursive)], mode?: add|copy|move (add = reference the files in place; copy = into the library's Originals/YYYY/YYYY-MM-DD/; move = as copy, then each original and its XMP sidecars are removed from the source — only after the copy is verified (a hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate and unchecked files keep their sources; a taken name gets -1, -2…; undo removes the photos from the library but leaves the files at the destination), destination?: folder for copies / moves, organize?: date (YYYY/YYYY-MM-DD) | month (YYYY/YYYY-MM) | flat | a folder template, e.g. `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 (the template's `/` make the folders, each level expanded with the rename tokens and made a safe folder name: never outside the destination; must be relative, no `..`; a level with missing metadata is `unknown`) — dated by capture time, else the import time, rename?: file-name template for copies, original extension added (tokens: {name} {num} {seq} {seq:N} {date} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext}; photo.renameTokens explains each; blank = keep names), renameStart?: 1, metadataPreset?: name, dng?: bool (copy raws as DNG; copy only), local?: bool (browsing: the photos stay out of the library, like library.browse; not with move), album?: albumId, albumName?: new album, preset?: presetId, keywords?: [..], onDeleted?: skip|restore|fresh (a file that is in Recently Deleted: skip = leave it there and say so, restore = bring the photo back with its edits, fresh = delete the trashed record and import the file as new)} → {imported, duplicates: [{path, existing, reason, existingDeleted?}], restored?: [photoId], failed, warnings?: [{path, code: cameraProfile|uprightGeometry|unmappedXmp, profile?, mode?: off|auto|level|vertical|full|guided|unknown, fields?}], moved?: [{from, to, sidecars?}], kept?: [{path, reason}] (move: sources left in place and why), album?}", always, |s, p| { let req = import_params(s, p)?; diff --git a/crates/engine/src/cmd/xmp.rs b/crates/engine/src/cmd/xmp.rs index b83e72755..317a87aca 100644 --- a/crates/engine/src/cmd/xmp.rs +++ b/crates/engine/src/cmd/xmp.rs @@ -35,11 +35,13 @@ fn read(s: &mut Session, p: &Value) -> Result { let mut ops = Vec::new(); let mut read = Vec::new(); let mut failed = Vec::new(); + let mut warnings = Vec::new(); for id in s.targets(p) { - match s.read_sidecar_op(id) { - Ok(Some((op, from))) => { + match s.read_sidecar_op_with_warnings(id) { + Ok(Some((op, from, mut photo_warnings))) => { ops.push(op); read.push(json!({"id": id.0, "from": from.display().to_string()})); + warnings.append(&mut photo_warnings); } Ok(None) => failed.push(json!({"id": id.0, "error": "no XMP sidecar or embedded XMP"})), Err(e) => failed.push(json!({"id": id.0, "error": e.to_string()})), @@ -48,7 +50,11 @@ fn read(s: &mut Session, p: &Value) -> Result { if !ops.is_empty() { s.commit("Read Metadata from File", Op::Batch { ops })?; } - Ok(json!({"read": read, "failed": failed})) + let mut result = json!({"read": read, "failed": failed}); + if !warnings.is_empty() { + result["warnings"] = serde_json::to_value(warnings).unwrap_or_default(); + } + Ok(result) } fn prefs(s: &mut Session, p: &Value) -> Result { @@ -84,7 +90,7 @@ pub fn specs() -> Vec { "Read Metadata from File", ["Photo"], None, - "{ids?} — reads each photo's XMP sidecar (or a raw/DNG's embedded XMP): metadata and develop settings, one undo step → {read, failed}", + "{ids?} — reads each photo's XMP sidecar (or a raw/DNG's embedded XMP): metadata and develop settings, one undo step → {read, failed, warnings?: [{path, code, profile?, mode?, fields?}]}", has_selection, read ), diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index ad0ca9344..650ebbda3 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -215,6 +215,19 @@ pub enum ImportWarningCode { UnmappedXmp, } +/// The saved Lightroom Upright mode associated with a foreign geometry transform. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ImportUprightMode { + Off, + Auto, + Level, + Vertical, + Full, + Guided, + Unknown, +} + /// One actionable XMP import diagnostic. `path` is filled when its photo is committed. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -223,6 +236,8 @@ pub struct ImportWarning { pub code: ImportWarningCode, #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub fields: Vec, } diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index 0ad3ec16b..b2c531488 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -172,15 +172,32 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result crate::import::ImportUprightMode::Off, + "1" => crate::import::ImportUprightMode::Auto, + "2" => crate::import::ImportUprightMode::Level, + "3" => crate::import::ImportUprightMode::Vertical, + "4" => crate::import::ImportUprightMode::Full, + "5" => crate::import::ImportUprightMode::Guided, + _ => crate::import::ImportUprightMode::Unknown, + }) + .unwrap_or(crate::import::ImportUprightMode::Unknown); out.warnings.push(crate::import::ImportWarning { path: String::new(), code: crate::import::ImportWarningCode::UprightGeometry, profile: None, + mode: Some(mode), fields: Vec::new(), }); } @@ -198,6 +215,7 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result Result> { + self.read_sidecar_op_with_warnings(id).map(|result| result.map(|(op, from, _)| (op, from))) + } + + /// Like [`Session::read_sidecar_op`], retaining transient diagnostics for the command report. + pub fn read_sidecar_op_with_warnings(&self, id: PhotoId) -> Result)>> { let p = self.catalog.photo(id).ok_or(lightcraft_catalog::CatalogError::NoPhoto(id))?; let Some(orig) = file_path(p) else { return Ok(None) }; let Some((packet, from)) = read_packet(orig, p.kind, self.sidecar_naming(id)) else { return Ok(None) }; let sc = parse_sidecar(&packet, p.kind == MediaKind::Raw) .map_err(|e| EngineError::Other(format!("{}: {e}", from.display())))? .resolve_label(&self.catalog); + let mut warnings = sc.warnings.clone(); + for warning in &mut warnings { + warning.path = orig.to_string(); + } let mut q = (**p).clone(); let develop_changed = merge_into(&mut q, &sc, &(self.clock)()); let mut ops = vec![ @@ -531,7 +558,7 @@ impl Session { if develop_changed { ops.extend(self.develop_op(id, (*q.develop).clone(), "Read Metadata from File")); } - Ok(Some((Op::Batch { ops }, from))) + Ok(Some((Op::Batch { ops }, from, warnings))) } /// Auto-write: sidecars for photos changed by `ops` (errors are logged, not returned). diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index 4a4cad539..40a20f673 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -395,11 +395,17 @@ fn auto_import_watched_folder() { s.execute("library.autoImport", &json!({"folder": dir.to_string_lossy(), "album": "Tethered"})).unwrap(); assert_eq!(s.execute("library.autoImportScan", &json!({})).unwrap()["imported"], json!([])); write_png(&dir.join("one.png"), 1); + std::fs::write( + dir.join("one.xmp"), + r#""#, + ) + .unwrap(); std::fs::write(dir.join("notes.txt"), "not a photo").unwrap(); // first sight: wait (it may still be copying) assert_eq!(s.execute("library.autoImportScan", &json!({})).unwrap()["imported"], json!([])); let r = s.execute("library.autoImportScan", &json!({})).unwrap(); assert_eq!(r["imported"].as_array().unwrap().len(), 1, "{r}"); + assert!(r["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Color"), "{r}"); let album = s.catalog.albums().find(|a| a.name == "Tethered").expect("album").id; assert_eq!(s.catalog.album_count(album), 1); assert!(s.selection.ids.is_empty(), "arrivals don't take the selection"); diff --git a/crates/engine/src/tests_xmp.rs b/crates/engine/src/tests_xmp.rs index 8ed9a4e26..d47647cad 100644 --- a/crates/engine/src/tests_xmp.rs +++ b/crates/engine/src/tests_xmp.rs @@ -170,6 +170,7 @@ fn foreign_crs_sidecar_on_import_and_read_from_file() { std::fs::remove_file(src.join("harbour.xmp")).unwrap(); let r = s.execute("photo.readMetadataFromFile", &json!({})).unwrap(); assert_eq!(r["failed"].as_array().unwrap().len(), 1); + assert!(r.get("warnings").is_none(), "{r}"); let _ = std::fs::remove_dir_all(&src); } @@ -195,12 +196,43 @@ fn foreign_profile_and_upright_import_diagnostics_are_actionable() { let r = s.execute("library.import", &json!({"paths": [photo.to_string_lossy()]})).unwrap(); let warnings = r["warnings"].as_array().unwrap(); assert!(warnings.iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Camera Faithful"), "{r}"); - assert!(warnings.iter().any(|w| w["code"] == "uprightGeometry"), "{r}"); - let p = s.catalog.photos().next().unwrap(); + assert!(warnings.iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == "auto"), "{r}"); + let p = s.catalog.photos().next().unwrap().clone(); assert_eq!(p.develop.profile.id, "lc.color"); assert_eq!(p.develop.light.exposure, 1.68); assert_eq!(std::fs::read(&sidecar).unwrap(), before, "import never rewrites foreign XMP"); + // Metadata reread carries same structured diagnostics, with each saved Upright mode typed. + let photo_path = photo.to_string_lossy().to_string(); + let reread = s.execute("photo.readMetadataFromFile", &json!({"ids": [p.id.0]})).unwrap(); + assert!( + reread["warnings"] + .as_array() + .unwrap() + .iter() + .any(|w| w["code"] == "cameraProfile" && w["profile"] == "Camera Faithful" && w["path"] == photo_path), + "{reread}" + ); + assert!(reread["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == "auto"), "{reread}"); + for (source, mode) in [("0", "off"), ("2", "level"), ("3", "vertical"), ("4", "full"), ("5", "guided"), ("9", "unknown")] { + let mode_xmp = xmp.replace("crs:PerspectiveUpright=\"1\"", &format!("crs:PerspectiveUpright=\"{source}\"")); + std::fs::write(&sidecar, mode_xmp).unwrap(); + let reread = s.execute("photo.readMetadataFromFile", &json!({"ids": [p.id.0]})).unwrap(); + assert!(reread["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == mode), "{source}: {reread}"); + } + + // A foreign name colliding with a built-in profile label remains warning metadata only. + let collision = src.join("collision.png"); + write_png(&collision, 8); + std::fs::write(src.join("collision.xmp"), xmp.replace("Camera Faithful", "Color")).unwrap(); + let collision_report = s.execute("library.import", &json!({"paths": [collision.to_string_lossy()]})).unwrap(); + assert!( + collision_report["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Color"), + "{collision_report}" + ); + let collision_photo = s.catalog.photos().find(|photo| photo.file_name == "collision.png").unwrap(); + assert_eq!(collision_photo.develop.profile.id, "lc.color"); + // A second import still diagnoses the untouched foreign packet, while an XMP-free photo does not. drop(s); let clean = src.join("clean.png"); diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index 58bf68ebb..720bf2697 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -62,7 +62,10 @@ "Import failed: {e}": "Import fehlgeschlagen: {e}", "Import warnings ({n}):\n{warnings}": "Importwarnungen ({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "XMP-Profil \"{profile}\" wurde für {path} nicht angewendet; Farben können von Lightroom abweichen.", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "Lightroom-Geometrie wurde für {path} nicht beibehalten; Upright wird neu berechnet.", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Lightroom-Geometrie wurde für {path} nicht beibehalten; {mode} wird neu berechnet.", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Transformation wird ignoriert, weil Upright deaktiviert ist.", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Guided-Upright-Transformation wird nicht unterstützt.", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Upright-Transformation wird nicht unterstützt.", "XMP adjustments were not imported for {path}: {fields}.": "XMP-Anpassungen wurden für {path} nicht importiert: {fields}.", "Imported {n} point curve preset{}": "Importierte Punktkurvenvorgaben: {n}{:.0}", "Imported {n} preset{}": "Importierte Vorgaben: {n}{:.0}", diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index a28d85db1..32d8d74d8 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -62,7 +62,10 @@ "Import failed: {e}": "Error de importación: {e}", "Import warnings ({n}):\n{warnings}": "Avisos de importación ({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "El perfil XMP \"{profile}\" no se aplicó a {path}; los colores pueden diferir de Lightroom.", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "La geometría de Lightroom no se conservó para {path}; Upright se recalculará.", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La geometría de Lightroom no se conservó para {path}; {mode} se recalculará.", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada se ignora porque Upright está desactivado.", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada de Upright guiado no es compatible.", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada de Upright no es compatible.", "XMP adjustments were not imported for {path}: {fields}.": "Los ajustes XMP no se importaron para {path}: {fields}.", "Imported {n} point curve preset{}": "{n} ajuste{0} preestablecido{0} de curva de puntos importado{0}", "Imported {n} preset{}": "{n} ajuste{0} preestablecido{0} importado{0}", diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index 84f609733..5b598c6b1 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -155,7 +155,10 @@ "Import failed: {e}": "読み込みに失敗しました:{e}", "Import warnings ({n}):\n{warnings}": "読み込みの警告({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "{path} に XMP プロファイル \"{profile}\" を適用できませんでした。色は Lightroom と異なる場合があります。", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "{path} の Lightroom ジオメトリは保持されませんでした。Upright を再計算します。", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "{path} の Lightroom ジオメトリは保持されませんでした。{mode} を再計算します。", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "{path} の Lightroom ジオメトリは保持されませんでした。Upright がオフのため保存された変換は無視されます。", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。ガイド付き Upright の保存された変換はサポートされていません。", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。保存された Upright の変換はサポートされていません。", "XMP adjustments were not imported for {path}: {fields}.": "{path} の XMP 調整は読み込まれませんでした:{fields}。", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles}個のプロファイルを読み込みました{:.0}(プロファイルブラウザー → 各グループ)", "Insert at the cursor (also written {})": "カーソル位置に挿入({}とも表記可)", diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index 15e1f796e..a73bbbb67 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -155,7 +155,10 @@ "Import failed: {e}": "Falha na importação: {e}", "Import warnings ({n}):\n{warnings}": "Avisos de importação ({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "O perfil XMP \"{profile}\" não foi aplicado a {path}; as cores podem diferir do Lightroom.", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "A geometria do Lightroom não foi preservada para {path}; o Upright será recalculado.", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "A geometria do Lightroom não foi preservada para {path}; {mode} será recalculado.", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva é ignorada porque o Upright está desativado.", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright guiado não é compatível.", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright não é compatível.", "XMP adjustments were not imported for {path}: {fields}.": "Os ajustes XMP não foram importados para {path}: {fields}.", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} perfil(is) importado(s){:.0} (Navegador de perfis → seus grupos)", "Insert at the cursor (also written {})": "Inserir no cursor (também escrito {})", diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index b8be34f4b..1e516c4e9 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -65,7 +65,10 @@ "Import failed: {e}": "Ошибка импорта: {e}", "Import warnings ({n}):\n{warnings}": "Предупреждения импорта ({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Профиль XMP \"{profile}\" не применён к {path}; цвета могут отличаться от Lightroom.", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "Геометрия Lightroom для {path} не сохранена; Upright будет пересчитан.", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Геометрия Lightroom для {path} не сохранена; режим {mode} будет пересчитан.", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование игнорируется, поскольку Upright выключен.", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование Guided Upright не поддерживается.", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование Upright не поддерживается.", "XMP adjustments were not imported for {path}: {fields}.": "Коррекции XMP для {path} не импортированы: {fields}.", "Imported {n} point curve preset{}": "Импортировано пресетов точечной кривой: {n}{:.0}", "Imported {n} preset{}": "Импортировано пресетов: {n}{:.0}", diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index 01dba1ab4..4090e92fc 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -155,7 +155,10 @@ "Import failed: {e}": "导入失败:{e}", "Import warnings ({n}):\n{warnings}": "导入警告({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "未对 {path} 应用 XMP 配置文件“{profile}”;颜色可能与 Lightroom 不同。", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "未保留 {path} 的 Lightroom 几何信息;将重新计算 Upright。", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "未保留 {path} 的 Lightroom 几何信息;将重新计算 {mode}。", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "未保留 {path} 的 Lightroom 几何信息;由于 Upright 已关闭,将忽略保存的变换。", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息;不支持保存的引导 Upright 变换。", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息;不支持保存的 Upright 变换。", "XMP adjustments were not imported for {path}: {fields}.": "未导入 {path} 的 XMP 调整:{fields}。", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "已导入 {profiles} 个配置文件{0:.0}(配置文件浏览器 → 各自的分组)", "Insert at the cursor (also written {})": "在光标处插入(也可写作 {0})", diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index ab34b129f..39b8834fe 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -155,7 +155,10 @@ "Import failed: {e}": "匯入失敗:{e}", "Import warnings ({n}):\n{warnings}": "匯入警告({n}):\n{warnings}", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "未將 XMP 描述檔「{profile}」套用至 {path};色彩可能與 Lightroom 不同。", - "Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "未保留 {path} 的 Lightroom 幾何資料;將重新計算 Upright。", + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "未保留 {path} 的 Lightroom 幾何資料;將重新計算 {mode}。", + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "未保留 {path} 的 Lightroom 幾何資料;Upright 已關閉,因此忽略儲存的轉換。", + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料;不支援儲存的引導 Upright 轉換。", + "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料;不支援儲存的 Upright 轉換。", "XMP adjustments were not imported for {path}: {fields}.": "未匯入 {path} 的 XMP 調整:{fields}。", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "已匯入 {profiles} 個描述檔{:.0}(描述檔瀏覽器 → 各自的群組)", "Insert at the cursor (also written {})": "在游標處插入(也可寫成 {})", diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index d8aceb01a..40ee2ba58 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -8,7 +8,9 @@ //! catalog between frames, with a progress window and Cancel; the whole import is one undo step. use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; -use lightcraft_engine::import::{ImportCandidate, ImportWarning, ImportWarningCode, ScanInput, ScanOutput, ScanProgress, scan_with}; +use lightcraft_engine::import::{ + ImportCandidate, ImportUprightMode, ImportWarning, ImportWarningCode, ScanInput, ScanOutput, ScanProgress, scan_with, +}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::sync::atomic::Ordering; @@ -718,9 +720,39 @@ fn xmp_warning_text(warning: &ImportWarning) -> String { profile = warning.profile.as_deref().unwrap_or_default(), path = path, ), - ImportWarningCode::UprightGeometry => { - crate::i18n::tr_format!("Lightroom geometry was not preserved for {path}; Upright will be recalculated.", path = path,) - } + ImportWarningCode::UprightGeometry => match warning.mode.unwrap_or(ImportUprightMode::Unknown) { + ImportUprightMode::Off => crate::i18n::tr_format!( + "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.", + path = path, + ), + ImportUprightMode::Guided => crate::i18n::tr_format!( + "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.", + path = path, + ), + ImportUprightMode::Auto => crate::i18n::tr_format!( + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.", + path = path, + mode = crate::i18n::tr("Auto") + ), + ImportUprightMode::Level => crate::i18n::tr_format!( + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.", + path = path, + mode = crate::i18n::tr("Level") + ), + ImportUprightMode::Vertical => crate::i18n::tr_format!( + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.", + path = path, + mode = crate::i18n::tr("Vertical") + ), + ImportUprightMode::Full => crate::i18n::tr_format!( + "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.", + path = path, + mode = crate::i18n::tr("Full") + ), + ImportUprightMode::Unknown => { + crate::i18n::tr_format!("Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.", path = path,) + } + }, ImportWarningCode::UnmappedXmp => { crate::i18n::tr_format!("XMP adjustments were not imported for {path}: {fields}.", path = path, fields = warning.fields.join(", "),) }