From 0ec463568baa0402f93146664d6d9c702e1cb0e4 Mon Sep 17 00:00:00 2001 From: skylartaylor <26588250+skylartaylor@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:53:04 +0200 Subject: [PATCH] LRC-LIB-CATALOG-IMPORT: report offline records and keep import results readable Avoid repeated missing-original probes and retain staged photo copies only for virtual-copy masters. Preserve the destination and warnings in a result dialog with Show in Library, and reset every filter constraint through the existing commands. Generated with Codex. --- crates/engine/src/lightroom_catalog.rs | 86 +++++++++++++-- crates/engine/src/lightroom_job.rs | 43 +++++--- crates/pipeline/src/spots.rs | 2 +- crates/ui-egui/locales/de-formats.json | 5 +- crates/ui-egui/locales/de.json | 7 +- crates/ui-egui/locales/es-formats.json | 5 +- crates/ui-egui/locales/es.json | 7 +- crates/ui-egui/locales/fr-formats.json | 5 +- crates/ui-egui/locales/fr.json | 7 +- crates/ui-egui/locales/ja-formats.json | 5 +- crates/ui-egui/locales/ja.json | 7 +- crates/ui-egui/locales/pt-br-formats.json | 5 +- crates/ui-egui/locales/pt-br.json | 7 +- crates/ui-egui/locales/ru-formats.json | 5 +- crates/ui-egui/locales/ru.json | 7 +- crates/ui-egui/locales/uk-formats.json | 5 +- crates/ui-egui/locales/uk.json | 7 +- crates/ui-egui/locales/zh-hans-formats.json | 5 +- crates/ui-egui/locales/zh-hans.json | 7 +- crates/ui-egui/locales/zh-hant-formats.json | 5 +- crates/ui-egui/locales/zh-hant.json | 7 +- crates/ui-egui/src/lib.rs | 3 + crates/ui-egui/src/lightroom_import.rs | 116 ++++++++++++++++++++ crates/ui-egui/src/panels/dialogs.rs | 32 ++++++ crates/ui-egui/src/panels/settings.rs | 1 + crates/ui-egui/src/state.rs | 7 ++ crates/ui-egui/src/tests_open_lightroom.rs | 6 +- docs/gaps.md | 5 +- docs/lightroom-catalog-import.md | 15 ++- docs/parity-checklist.md | 5 +- 30 files changed, 382 insertions(+), 47 deletions(-) diff --git a/crates/engine/src/lightroom_catalog.rs b/crates/engine/src/lightroom_catalog.rs index 2519057a..69196153 100644 --- a/crates/engine/src/lightroom_catalog.rs +++ b/crates/engine/src/lightroom_catalog.rs @@ -698,14 +698,21 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex let mut ops = Vec::new(); let now = (s.clock)(); let mut missing = Vec::new(); + // Normal import counts probed originals; catalog import also adds offline records, + // distinct originals with identical bytes, and virtual copies. + let mut imported = files.imported.len(); let mut unmapped = BTreeMap::new(); let mut preserved = 0; // Masters precede their virtual copies. let mut photos: Vec<_> = data.photos.iter().collect(); photos.sort_by_key(|p| number(&p.image, "masterImage") != 0); + let copy_masters: HashSet<_> = data.photos.iter().map(|p| number(&p.image, "masterImage")).filter(|id| *id != 0).collect(); let mut planned: HashMap = HashMap::new(); for src in photos { let master = number(&src.image, "masterImage"); + if master == 0 && missing_sources.contains(&key(&src.path)) { + missing.push(src.path.clone()); + } let indexed = index .photos .get(&identity(&data, src)) @@ -736,9 +743,6 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex } else { let id = s.catalog.alloc_photo_id(); let name = Path::new(&src.path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| src.path.clone()); - if missing_sources.contains(&key(&src.path)) { - missing.push(src.path.clone()); - } let mut p = Photo::new( id, Source::File { path: src.path.clone() }, @@ -807,6 +811,12 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex if keep_develop { (p.develop, p.edited) = previous_develop; } + let photo_id = p.id; + // Only virtual-copy masters need a second owned photo while operations are staged. + // Keeping every photo here doubled the new-record working set on large catalogs. + if copy_masters.contains(&src.source_id) { + planned.insert(photo_id, p.clone()); + } if s.catalog.photo(p.id).is_some() { ops.extend([ Op::SetRating { id: p.id, rating: p.rating }, @@ -817,10 +827,10 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex ]); ops.push(Op::SetDevelop { id: p.id, settings: p.develop.clone(), label: "Lightroom catalog".into(), edited: p.edited.clone() }); } else { - ops.push(Op::AddPhoto { photo: Box::new(p.clone()) }); + imported += 1; + ops.push(Op::AddPhoto { photo: Box::new(p) }); } - by_path.entry(key(&src.path)).or_insert(p.id); - planned.insert(p.id, p); + by_path.entry(key(&src.path)).or_insert(photo_id); } let mut albums = HashMap::new(); for collection in &data.collections { @@ -893,7 +903,7 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex } s.merge_undo(s.undo.len().saturating_sub(undo0), "Import Lightroom Catalog"); Ok(AppliedImport { - report: json!({"source":data.source,"photos":ids.len(),"imported":files.imported.len(),"collections":albums.len(),"preservedExistingEdits":preserved,"missing":missing,"failed":files.failed,"warnings":warnings,"unmapped":unmapped,"archive":archive_path,"mapping":ids}), + report: json!({"source":data.source,"photos":ids.len(),"imported":imported,"library":s.library.as_ref().filter(|l| l.on_disk).map(|l| l.dir.to_string_lossy()),"collections":albums.len(),"preservedExistingEdits":preserved,"missing":missing,"failed":files.failed,"warnings":warnings,"unmapped":unmapped,"archive":archive_path,"mapping":ids}), index: index.clone(), }) } @@ -944,6 +954,7 @@ mod tests { let data = sample(); let r = apply(&mut s, data.clone(), false).unwrap(); assert_eq!(r["photos"], 2); + assert_eq!(r["imported"], 2, "the offline master and virtual copy both count as added"); assert_eq!(r["missing"].as_array().unwrap().len(), 1); let master = s.catalog.photo(PhotoId(r["mapping"]["1"].as_u64().unwrap())).unwrap(); assert_eq!(master.rating, 5); @@ -1241,6 +1252,10 @@ mod tests { s.commit("Changed time", Op::SetCaptured { id, captured: None }).unwrap(); let second = apply(&mut s, data, true).unwrap(); assert_eq!(first["mapping"], second["mapping"]); + assert_eq!(first["imported"], 2); + assert_eq!(second["imported"], 0, "reimport updates records instead of adding them"); + assert_eq!(second["missing"].as_array().unwrap().len(), 1, "offline originals are reported on reimport too"); + assert_eq!(second["library"], dir.to_string_lossy().as_ref()); assert_eq!(s.catalog.len(), 2); assert_eq!(s.catalog.albums().count(), 2); assert_eq!(s.catalog.photo(id).unwrap().captured.as_deref(), Some("2024-01-16T12:34:56")); @@ -1359,6 +1374,63 @@ mod tests { std::fs::remove_file(path).unwrap(); } + #[test] + fn preparation_skips_missing_original_probes_but_keeps_catalog_records() { + let dir = std::env::temp_dir().join(format!("lightcraft-lrcat-offline-probes-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let source = dir.join("source.lrcat"); + let root = dir.join("offline"); + let tables = photo_tables((root.to_str().unwrap(), "")); + std::fs::write(&source, native_catalog(&tables)).unwrap(); + let mut s = crate::Session::new(); + let probes = Arc::new(AtomicUsize::new(0)); + let calls = probes.clone(); + s.media.file_probe = Some(Arc::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + Err("a known-missing original must not be probed".into()) + })); + let mut job = crate::lightroom_job::LightroomJob::new(&mut s, source, false).unwrap(); + let prepared = job.prepare(&AtomicBool::new(false)).unwrap(); + assert_eq!(probes.load(Ordering::Relaxed), 0); + let result = crate::lightroom_job::commit_prepared(&mut s, prepared).unwrap(); + assert_eq!(result.report["imported"], 1); + assert_eq!(result.report["failed"], json!([])); + assert_eq!(result.report["missing"].as_array().unwrap().len(), 1); + let photo = s.catalog.photos().next().unwrap(); + assert_eq!(photo.rating, 5); + assert_eq!((photo.width, photo.height), (20, 10)); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(s.catalog.is_empty()); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn probed_original_is_counted_once_and_reimport_adds_nothing() { + let dir = std::env::temp_dir().join(format!("lightcraft-lrcat-present-probes-{}", std::process::id())); + std::fs::create_dir_all(dir.join("Photos")).unwrap(); + let source = dir.join("source.lrcat"); + std::fs::write(dir.join("Photos/test.ARW"), b"original").unwrap(); + std::fs::write(&source, native_catalog(&photo_tables((dir.to_str().unwrap(), "")))).unwrap(); + let mut s = crate::Session::new(); + let probes = Arc::new(AtomicUsize::new(0)); + let calls = probes.clone(); + s.media.file_probe = Some(Arc::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + Ok(crate::media::ProbeInfo { format: "ARW".into(), width: 20, height: 10, ..Default::default() }) + })); + for expected in [1, 0] { + let mut job = crate::lightroom_job::LightroomJob::new(&mut s, source.clone(), false).unwrap(); + let prepared = job.prepare(&AtomicBool::new(false)).unwrap(); + let completion = crate::lightroom_job::commit_prepared(&mut s, prepared).unwrap(); + assert_eq!(completion.report["imported"], expected); + assert_eq!(completion.report["missing"], json!([])); + assert_eq!(completion.report["failed"], json!([])); + assert_eq!(s.catalog.len(), 1); + } + assert_eq!(probes.load(Ordering::Relaxed), 1); + std::fs::remove_dir_all(dir).unwrap(); + } + #[test] fn a_root_missing_here_is_found_relative_to_the_catalog() { let dir = std::env::temp_dir().join(format!("lightcraft-relocated-catalog-{}", std::process::id())); diff --git a/crates/engine/src/lightroom_job.rs b/crates/engine/src/lightroom_job.rs index 33ed64a2..bf613ccc 100644 --- a/crates/engine/src/lightroom_job.rs +++ b/crates/engine/src/lightroom_job.rs @@ -190,20 +190,11 @@ impl LightroomJob { return Err(error("Lightroom import cancelled")); } self.import.done.fetch_add(1, Ordering::Relaxed); - let paths: Vec = data - .photos - .iter() - .filter(|photo| lightroom_catalog::number_for_job(&photo.image, "masterImage") == 0) - .map(|photo| photo.path.clone()) - .collect(); - let missing_sources = data - .photos - .iter() - .filter(|photo| lightroom_catalog::number_for_job(&photo.image, "masterImage") == 0) - .filter(|photo| !Path::new(&photo.path).is_file()) - .map(|photo| normalize(&photo.path)) - .collect(); - let normal = self.import.prepare(&paths, cancel); + let (paths, missing_sources) = original_paths(&data, cancel)?; + // Catalog entries name files, never folders. Missing originals already have enough + // catalog metadata for placeholders; probing them again only adds failed I/O and errors. + self.import.total.fetch_add(paths.len(), Ordering::Relaxed); + let normal = self.import.prepare_files(paths, cancel); if cancel.load(Ordering::Relaxed) { normal.rollback(); return Err(error("Lightroom import cancelled")); @@ -263,6 +254,30 @@ pub fn commit_prepared(s: &mut crate::Session, mut prepared: PreparedLightroom) Ok(LightroomCompletion { report: applied.report, finalization: LightroomArchiveFinalization { path, bytes } }) } +fn original_paths(data: &CatalogImport, cancel: &AtomicBool) -> crate::Result<(Vec, HashSet)> { + let mut paths = Vec::new(); + let mut missing = HashSet::new(); + let mut seen = HashSet::new(); + for photo in &data.photos { + if cancel.load(Ordering::Relaxed) { + return Err(error("Lightroom import cancelled")); + } + if lightroom_catalog::number_for_job(&photo.image, "masterImage") != 0 { + continue; + } + let key = normalize(&photo.path); + if !seen.insert(key.clone()) { + continue; + } + if Path::new(&photo.path).is_file() { + paths.push(photo.path.clone()); + } else { + missing.insert(key); + } + } + Ok((paths, missing)) +} + fn normalize(path: &str) -> String { let path = path.replace('\\', "/"); if cfg!(windows) { path.to_lowercase() } else { path } diff --git a/crates/pipeline/src/spots.rs b/crates/pipeline/src/spots.rs index 483f83f3..32f55a3c 100644 --- a/crates/pipeline/src/spots.rs +++ b/crates/pipeline/src/spots.rs @@ -247,7 +247,7 @@ fn fill_box(src: &mut [[f32; 3]], tgt: &[[f32; 3]], alpha: &[f32], bw: usize, bh let flat: Vec = tgt.iter().flatten().copied().collect(); let out = lightcraft_inpaint::complete(bw, bh, 3, &flat, &hole, &lightcraft_inpaint::CompleteParams::default()) .unwrap_or_else(|| lightcraft_inpaint::membrane_fill(bw, bh, 3, &flat, &hole)); - for (d, v) in src.iter_mut().zip(out.chunks_exact(3)) { + for (d, v) in src.iter_mut().zip(out.as_chunks::<3>().0) { *d = [v[0], v[1], v[2]]; } } diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index 7bcaf9f2..1d8edbd5 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "nicht zu Apple Fotos hinzugefügt: {e}", "{n} added to Apple Photos album “{album}”": "{n} zum Apple-Fotos-Album „{album}“ hinzugefügt", "{n} added to Apple Photos": "{n} zu Apple Fotos hinzugefügt", - "{n} photos opened for editing; each is stacked with its original": "{n} Fotos zur Bearbeitung geöffnet; jedes ist mit seinem Original gestapelt" + "{n} photos opened for editing; each is stacked with its original": "{n} Fotos zur Bearbeitung geöffnet; jedes ist mit seinem Original gestapelt", + "{count} more warnings.": "{count} weitere Warnungen.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing} Originale fehlen. Ihre Katalogeinträge sind verfügbar; verwende „Fehlende Fotos suchen“, um sie neu zu verknüpfen.", + "{photos} catalog records processed; {collections} collections.": "{photos} Katalogeinträge verarbeitet; {collections} Sammlungen." } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 10cc7e0e..ba52e3d5 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -2109,5 +2109,10 @@ "Adding to Apple Photos…": "Wird zu Apple Fotos hinzugefügt…", "not added to Apple Photos (cancelled)": "nicht zu Apple Fotos hinzugefügt (abgebrochen)", "The first time, macOS asks whether LightCraft may control Photos.": "Beim ersten Mal fragt macOS, ob LightCraft Fotos steuern darf.", - "Adding to Apple Photos": "Wird zu Apple Fotos hinzugefügt" + "Adding to Apple Photos": "Wird zu Apple Fotos hinzugefügt", + "Destination library": "Zielbibliothek", + "Lightroom Catalog Import": "Lightroom-Katalogimport", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroom-Kataloge werden in die geöffnete Bibliothek importiert; es wird kein eigener Katalogordner erstellt.", + "Show in Library": "In Bibliothek anzeigen", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Diese Fotos befinden sich nur im Arbeitsspeicher. Öffne eine Bibliothek auf dem Datenträger und importiere den Katalog erneut, um sie zu speichern." } diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index 3dd94a93..a5177644 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "no se añadieron a Fotos de Apple: {e}", "{n} added to Apple Photos album “{album}”": "{n} añadidas al álbum “{album}” de Fotos de Apple", "{n} added to Apple Photos": "{n} añadidas a Fotos de Apple", - "{n} photos opened for editing; each is stacked with its original": "{n} fotos abiertas para editar; cada una está apilada con su original" + "{n} photos opened for editing; each is stacked with its original": "{n} fotos abiertas para editar; cada una está apilada con su original", + "{count} more warnings.": "{count} advertencias más.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "Faltan {missing} originales. Sus registros del catálogo están disponibles; usa Buscar fotos que faltan para volver a vincularlos.", + "{photos} catalog records processed; {collections} collections.": "{photos} registros del catálogo procesados; {collections} colecciones." } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index ff672e88..1f93ec1d 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1951,5 +1951,10 @@ "Adding to Apple Photos…": "Añadiendo a Fotos de Apple…", "not added to Apple Photos (cancelled)": "no se añadieron a Fotos de Apple (cancelado)", "The first time, macOS asks whether LightCraft may control Photos.": "La primera vez, macOS pregunta si LightCraft puede controlar Fotos.", - "Adding to Apple Photos": "Añadiendo a Fotos de Apple" + "Adding to Apple Photos": "Añadiendo a Fotos de Apple", + "Destination library": "Biblioteca de destino", + "Lightroom Catalog Import": "Importación de catálogo de Lightroom", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Los catálogos de Lightroom se importan a la biblioteca abierta; no se crea una carpeta de catálogo separada.", + "Show in Library": "Mostrar en la biblioteca", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Estas fotos solo están en memoria. Abre una biblioteca en disco e importa el catálogo de nuevo para guardarlas." } diff --git a/crates/ui-egui/locales/fr-formats.json b/crates/ui-egui/locales/fr-formats.json index 65cf9731..5e3567c1 100644 --- a/crates/ui-egui/locales/fr-formats.json +++ b/crates/ui-egui/locales/fr-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "non ajouté à Apple Photos : {e}", "{n} added to Apple Photos album “{album}”": "{n} ajouté(s) à l'album Apple Photos « {album} »", "{n} added to Apple Photos": "{n} ajouté(s) à Apple Photos", - "{n} photos opened for editing; each is stacked with its original": "{n} photos ouvertes pour modification ; chacune est empilée avec son original" + "{n} photos opened for editing; each is stacked with its original": "{n} photos ouvertes pour modification ; chacune est empilée avec son original", + "{count} more warnings.": "{count} avertissements supplémentaires.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing} originaux sont manquants. Leurs entrées de catalogue sont disponibles ; utilisez Rechercher les photos manquantes pour rétablir les liens.", + "{photos} catalog records processed; {collections} collections.": "{photos} entrées du catalogue traitées ; {collections} collections." } diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index f7c469ff..7649bdfa 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -1951,5 +1951,10 @@ "Adding to Apple Photos…": "Ajout à Apple Photos…", "not added to Apple Photos (cancelled)": "non ajouté à Apple Photos (annulé)", "The first time, macOS asks whether LightCraft may control Photos.": "La première fois, macOS demande si LightCraft peut contrôler Photos.", - "Adding to Apple Photos": "Ajout à Apple Photos" + "Adding to Apple Photos": "Ajout à Apple Photos", + "Destination library": "Bibliothèque de destination", + "Lightroom Catalog Import": "Importation de catalogue Lightroom", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Les catalogues Lightroom sont importés dans la bibliothèque ouverte ; aucun dossier de catalogue distinct n’est créé.", + "Show in Library": "Afficher dans la bibliothèque", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Ces photos sont uniquement en mémoire. Ouvrez une bibliothèque sur disque et importez à nouveau le catalogue pour les enregistrer." } diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index ca286386..62ad15a7 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "Appleの写真に追加されませんでした:{e}", "{n} added to Apple Photos album “{album}”": "{n}件をAppleの写真のアルバム「{album}」に追加しました", "{n} added to Apple Photos": "{n}件をAppleの写真に追加しました", - "{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています" + "{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています", + "{count} more warnings.": "ほかに{count}件の警告があります。", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing}件のオリジナルが見つかりません。カタログのレコードは利用できます。「見つからない写真を検索」で再リンクしてください。", + "{photos} catalog records processed; {collections} collections.": "カタログの{photos}件のレコードを処理しました。コレクションは{collections}件です。" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 9b9ae4c3..c86f0b77 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1953,5 +1953,10 @@ "Adding to Apple Photos…": "Appleの写真に追加中…", "not added to Apple Photos (cancelled)": "Appleの写真には追加されませんでした(キャンセル)", "The first time, macOS asks whether LightCraft may control Photos.": "初回は、LightCraftに写真の操作を許可するかmacOSが確認します。", - "Adding to Apple Photos": "Appleの写真に追加中" + "Adding to Apple Photos": "Appleの写真に追加中", + "Destination library": "読み込み先ライブラリ", + "Lightroom Catalog Import": "Lightroomカタログの読み込み", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroomカタログは開いているライブラリに読み込まれます。別のカタログフォルダーは作成されません。", + "Show in Library": "ライブラリに表示", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "これらの写真はメモリ内にのみあります。ディスク上のライブラリを開き、カタログを再度読み込んで保存してください。" } diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index 0b3ebe80..5cd1fc18 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "não adicionadas ao Fotos da Apple: {e}", "{n} added to Apple Photos album “{album}”": "{n} adicionadas ao álbum “{album}” do Fotos da Apple", "{n} added to Apple Photos": "{n} adicionadas ao Fotos da Apple", - "{n} photos opened for editing; each is stacked with its original": "{n} fotos abertas para edição; cada uma está empilhada com a original" + "{n} photos opened for editing; each is stacked with its original": "{n} fotos abertas para edição; cada uma está empilhada com a original", + "{count} more warnings.": "Mais {count} avisos.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing} originais estão ausentes. Seus registros no catálogo estão disponíveis; use Encontrar fotos ausentes para vinculá-los novamente.", + "{photos} catalog records processed; {collections} collections.": "{photos} registros do catálogo processados; {collections} coleções." } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 9c16f111..ff4738da 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1951,5 +1951,10 @@ "Adding to Apple Photos…": "Adicionando ao Fotos da Apple…", "not added to Apple Photos (cancelled)": "não adicionadas ao Fotos da Apple (cancelado)", "The first time, macOS asks whether LightCraft may control Photos.": "Na primeira vez, o macOS pergunta se o LightCraft pode controlar o Fotos.", - "Adding to Apple Photos": "Adicionando ao Fotos da Apple" + "Adding to Apple Photos": "Adicionando ao Fotos da Apple", + "Destination library": "Biblioteca de destino", + "Lightroom Catalog Import": "Importação de catálogo do Lightroom", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Os catálogos do Lightroom são importados para a biblioteca aberta; nenhuma pasta de catálogo separada é criada.", + "Show in Library": "Mostrar na biblioteca", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Estas fotos estão apenas na memória. Abra uma biblioteca no disco e importe o catálogo novamente para salvá-las." } diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index 11651aaa..869484aa 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "не добавлено в Apple Фото: {e}", "{n} added to Apple Photos album “{album}”": "Добавлено в альбом «{album}» Apple Фото: {n}", "{n} added to Apple Photos": "Добавлено в Apple Фото: {n}", - "{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом" + "{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом", + "{count} more warnings.": "Других предупреждений: {count}.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "Не найдены оригиналы: {missing}. Их записи каталога доступны; используйте поиск отсутствующих фотографий, чтобы восстановить ссылки.", + "{photos} catalog records processed; {collections} collections.": "Обработано записей каталога: {photos}; коллекций: {collections}." } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index a84297a0..d1c7bed0 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -2143,5 +2143,10 @@ "Adding to Apple Photos…": "Добавление в Apple Фото…", "not added to Apple Photos (cancelled)": "не добавлено в Apple Фото (отменено)", "The first time, macOS asks whether LightCraft may control Photos.": "В первый раз macOS спросит, может ли LightCraft управлять приложением «Фото».", - "Adding to Apple Photos": "Добавление в Apple Фото" + "Adding to Apple Photos": "Добавление в Apple Фото", + "Destination library": "Библиотека назначения", + "Lightroom Catalog Import": "Импорт каталога Lightroom", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Каталоги Lightroom импортируются в открытую библиотеку; отдельная папка каталога не создаётся.", + "Show in Library": "Показать в библиотеке", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Эти фотографии находятся только в памяти. Откройте библиотеку на диске и импортируйте каталог повторно, чтобы сохранить их." } diff --git a/crates/ui-egui/locales/uk-formats.json b/crates/ui-egui/locales/uk-formats.json index 7aca485e..9376708d 100644 --- a/crates/ui-egui/locales/uk-formats.json +++ b/crates/ui-egui/locales/uk-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "не додано до Apple Фото: {e}", "{n} added to Apple Photos album “{album}”": "Додано до альбому Apple Фото «{album}»: {n}", "{n} added to Apple Photos": "Додано до Apple Фото: {n}", - "{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом" + "{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом", + "{count} more warnings.": "Інших попереджень: {count}.", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "Не знайдено оригінали: {missing}. Їхні записи каталогу доступні; скористайтеся пошуком відсутніх фотографій, щоб відновити посилання.", + "{photos} catalog records processed; {collections} collections.": "Оброблено записів каталогу: {photos}; колекцій: {collections}." } diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 839de65f..b0026f6f 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -2145,5 +2145,10 @@ "Adding to Apple Photos…": "Додавання до Apple Фото…", "not added to Apple Photos (cancelled)": "не додано до Apple Фото (скасовано)", "The first time, macOS asks whether LightCraft may control Photos.": "Першого разу macOS запитає, чи може LightCraft керувати програмою «Фото».", - "Adding to Apple Photos": "Додавання до Apple Фото" + "Adding to Apple Photos": "Додавання до Apple Фото", + "Destination library": "Бібліотека призначення", + "Lightroom Catalog Import": "Імпорт каталогу Lightroom", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Каталоги Lightroom імпортуються у відкриту бібліотеку; окрема папка каталогу не створюється.", + "Show in Library": "Показати в бібліотеці", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Ці фотографії є лише в пам’яті. Відкрийте бібліотеку на диску та імпортуйте каталог повторно, щоб зберегти їх." } diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index bf27ca6b..bc1d223b 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "未添加到 Apple 照片:{e}", "{n} added to Apple Photos album “{album}”": "已将 {n} 个添加到 Apple 照片相册“{album}”", "{n} added to Apple Photos": "已将 {n} 个添加到 Apple 照片", - "{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起" + "{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起", + "{count} more warnings.": "另有 {count} 条警告。", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "有 {missing} 个原始文件缺失。其目录记录可用;请使用“查找缺失照片”重新链接文件。", + "{photos} catalog records processed; {collections} collections.": "已处理 {photos} 条目录记录;{collections} 个收藏集。" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 2b48b8e9..06516342 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1989,5 +1989,10 @@ "Adding to Apple Photos…": "正在添加到 Apple 照片…", "not added to Apple Photos (cancelled)": "未添加到 Apple 照片(已取消)", "The first time, macOS asks whether LightCraft may control Photos.": "首次使用时,macOS 会询问是否允许 LightCraft 控制“照片”。", - "Adding to Apple Photos": "正在添加到 Apple 照片" + "Adding to Apple Photos": "正在添加到 Apple 照片", + "Destination library": "目标图库", + "Lightroom Catalog Import": "Lightroom 目录导入", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroom 目录会导入到当前打开的图库中,不会创建单独的目录文件夹。", + "Show in Library": "在图库中显示", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "这些照片仅保存在内存中。请打开磁盘上的图库并再次导入目录以保存照片。" } diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index 37b2db2b..14b852d3 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -244,5 +244,8 @@ "not added to Apple Photos: {e}": "未加入 Apple 照片:{e}", "{n} added to Apple Photos album “{album}”": "已將 {n} 個加入 Apple 照片相簿「{album}」", "{n} added to Apple Photos": "已將 {n} 個加入 Apple 照片", - "{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起" + "{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起", + "{count} more warnings.": "另有 {count} 則警告。", + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "有 {missing} 個原始檔案遺失。其目錄記錄可用;請使用「尋找遺失相片」重新連結檔案。", + "{photos} catalog records processed; {collections} collections.": "已處理 {photos} 筆目錄記錄;{collections} 個收藏集。" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 0e81c6e9..a5e0275a 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1951,5 +1951,10 @@ "Adding to Apple Photos…": "正在加入 Apple 照片…", "not added to Apple Photos (cancelled)": "未加入 Apple 照片(已取消)", "The first time, macOS asks whether LightCraft may control Photos.": "第一次使用時,macOS 會詢問是否允許 LightCraft 控制「照片」。", - "Adding to Apple Photos": "正在加入 Apple 照片" + "Adding to Apple Photos": "正在加入 Apple 照片", + "Destination library": "目標圖庫", + "Lightroom Catalog Import": "Lightroom 目錄匯入", + "Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroom 目錄會匯入目前開啟的圖庫,不會建立獨立的目錄資料夾。", + "Show in Library": "在圖庫中顯示", + "These photos are only in memory. Open a library on disk and import the catalog again to save them.": "這些相片僅保存在記憶體中。請開啟磁碟上的圖庫並再次匯入目錄以儲存相片。" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9a387c77..29ebfb42 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -362,6 +362,8 @@ pub struct LightcraftApp { pub lightroom: Option, /// Last terminal Lightroom result, exposed by the command's status/wait response. pub lightroom_last: Option, + /// A completed import result waiting for another modal dialog to close. + pub(crate) lightroom_result: Option, /// The library generation the window title and the recent list were last updated for. pub shown_library: Option, /// A background export in progress. @@ -452,6 +454,7 @@ impl LightcraftApp { sync_run: None, lightroom: None, lightroom_last: None, + lightroom_result: None, shown_library: None, export: None, tasks: Default::default(), diff --git a/crates/ui-egui/src/lightroom_import.rs b/crates/ui-egui/src/lightroom_import.rs index 815e9857..2d9e0c4f 100644 --- a/crates/ui-egui/src/lightroom_import.rs +++ b/crates/ui-egui/src/lightroom_import.rs @@ -232,6 +232,9 @@ fn terminal(app: &mut LightcraftApp, ctx: &egui::Context, task: &LightroomTask, let kind = task.kind; if let Some(o) = value.as_object_mut() { o.entry("path").or_insert_with(|| json!(task.path)); + if let Some(library) = app.session.library.as_ref().filter(|l| l.on_disk) { + o.entry("library").or_insert_with(|| json!(library.dir.to_string_lossy())); + } } let message = if let Some(e) = value.get("error").and_then(Value::as_str) { let warning = value.get("indexWarning").and_then(Value::as_str).map_or(String::new(), |w| format!("; index warning: {w}")); @@ -269,11 +272,61 @@ fn terminal(app: &mut LightcraftApp, ctx: &egui::Context, task: &LightroomTask, crate::i18n::tr_format!("Lightroom import complete: {imported} photos added, {failed} failed", imported = imported, failed = failed) } }; + if matches!(kind, Kind::Import) && value.get("cancelled") != Some(&Value::Bool(true)) { + app.lightroom_result = Some(result_dialog(&value, &message)); + show_result(app); + } app.lightroom_last = Some(value); app.lightroom = None; app.toast_for(ctx, message, 6.0); } +fn show_result(app: &mut LightcraftApp) { + if app.ui.dialog.is_none() { + app.ui.dialog = app.lightroom_result.take(); + } +} + +fn result_dialog(value: &Value, summary: &str) -> crate::state::Dialog { + let report = value.get("report").unwrap_or(value); + let mut details = Vec::new(); + if let Some(photos) = report["photos"].as_u64() { + details.push(crate::i18n::tr_format!( + "{photos} catalog records processed; {collections} collections.", + photos = photos, + collections = report["collections"].as_u64().unwrap_or(0) + )); + } + if let Some(failures) = report["failed"].as_array() { + details + .extend(failures.iter().take(4).map(|failure| format!("{}: {}", failure[0].as_str().unwrap_or(""), failure[1].as_str().unwrap_or("")))); + } + let missing = report["missing"].as_array().map_or(0, Vec::len); + if missing > 0 { + details.push(crate::i18n::tr_format!( + "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.", + missing = missing + )); + } + if let Some(warning) = value.get("indexWarning").and_then(Value::as_str) { + details.push(warning.to_string()); + } + if let Some(warnings) = report["warnings"].as_array() { + // Results can contain one warning per image; keep the dialog bounded. + details.extend(warnings.iter().take(8).filter_map(Value::as_str).map(str::to_string)); + if warnings.len() > 8 { + details.push(crate::i18n::tr_format!("{count} more warnings.", count = warnings.len() - 8)); + } + } + let first = value.get("error").is_none().then(|| report["mapping"].as_object()?.values().filter_map(Value::as_u64).min()).flatten(); + crate::state::Dialog::LightroomResult { + summary: summary.to_string(), + library: report["library"].as_str().or_else(|| value["library"].as_str()).map(str::to_string), + details, + first, + } +} + #[cfg(not(target_arch = "wasm32"))] fn start_inspect(app: &mut LightcraftApp, path: PathBuf, ctx: &egui::Context) -> Result { busy(app)?; @@ -339,6 +392,7 @@ pub fn command(app: &mut LightcraftApp, id: &str, p: &Value, ctx: &egui::Context /// Poll owner-thread work, commit a prepared import, and finish its archive index. pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { + show_result(app); let Some(mut task) = app.lightroom.take() else { return }; let message = match task.rx.try_recv() { Ok(m) => m, @@ -518,6 +572,68 @@ mod tests { assert!(h.app.session.activity.list().is_empty()); } + #[test] + fn lightroom_result_stays_readable_and_shows_photos_hidden_by_a_filter() { + let mut h = demo(); + let id = h.app.session.catalog.photos().next().unwrap().id.0; + h.app.session.source = lightcraft_engine::LibrarySource::Picks; + h.app.session.filter.text = "no-photo-has-this-name".into(); + h.app.session.filter.only = vec![lightcraft_catalog::PhotoId(u64::MAX)]; + h.app.session.filter.labels = vec![lightcraft_catalog::ColorLabel::Red]; + h.app.session.filter.rule_set = Some(lightcraft_catalog::RuleSet { + mode: lightcraft_catalog::rules::Match::All, + rules: vec![lightcraft_catalog::rules::Rule::Field { field: "rating".into(), op: "is".into(), value: json!(5) }], + }); + let (task, _tx, _, _, _) = fake_import(&h); + let value = json!({"photos": 1, "imported": 1, "collections": 0, "missing": ["D:/Photos/a.NEF"], "mapping": {"1": id}, "library": "C:/Pictures/LightCraft Library"}); + terminal(&mut h.app, &h.view.ctx, &task, value); + assert!(h.app.ui.toast.as_ref().unwrap().0.contains("1 photos added")); + h.step(); + h.step(); + let text = h.painted_text().join(" "); + assert!(text.contains("Destination library"), "{text}"); + assert!(text.contains("C:/Pictures/LightCraft Library"), "{text}"); + assert!(text.contains("1 originals are missing"), "{text}"); + assert!(text.contains("Show in Library"), "{text}"); + // The activity and toast can go away; the modal result remains until dismissed. + h.app.ui.toast = None; + h.step(); + assert!(h.app.ui.dialog.is_some()); + let dialog = h.app.ui.dialog.clone().unwrap(); + crate::panels::dialogs::confirm_dialog(&mut h.app, &dialog).unwrap(); + assert_eq!(h.app.session.source, lightcraft_engine::LibrarySource::All); + assert_eq!(h.app.session.filter, lightcraft_catalog::Filter::default()); + assert_eq!(h.app.session.selection.active.map(|p| p.0), Some(id)); + assert_eq!(h.app.ui.view, crate::state::ViewMode::PhotoGrid); + assert!(!h.app.session.visible().is_empty()); + } + + #[test] + fn lightroom_failure_stays_readable_without_offering_a_success_action() { + let mut h = demo(); + let (task, _tx, _, _, _) = fake_import(&h); + terminal(&mut h.app, &h.view.ctx, &task, json!({"error": "catalog or WAL exceeds 1 GiB import limit"})); + h.step(); + h.step(); + assert!(h.painted_text().join(" ").contains("exceeds 1 GiB import limit")); + assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::LightroomResult { first: None, .. }))); + assert!(!h.app.widgets.iter().any(|(id, _)| id == "button:dialogCancel")); + } + + #[test] + fn lightroom_result_waits_for_an_existing_dialog() { + let mut h = demo(); + h.app.ui.dialog = Some(crate::state::Dialog::About); + let (task, _tx, _, _, _) = fake_import(&h); + terminal(&mut h.app, &h.view.ctx, &task, json!({"error": "catalog could not be read"})); + assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::About))); + assert!(h.app.lightroom_result.is_some()); + h.app.ui.dialog = None; + tick(&mut h.app, &h.view.ctx); + assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::LightroomResult { .. }))); + assert!(h.app.lightroom_result.is_none()); + } + #[test] fn inspect_report_keeps_empty_catalog_shape() { let report = inspect_report(CatalogImport { diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 72528fca..b5cd98ee 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -226,6 +226,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { Dialog::RenameAlbum { .. } => "Rename Album", Dialog::Rename { .. } => "Rename Photos", Dialog::Import { .. } => "Import Photos", + Dialog::LightroomResult { .. } => "Lightroom Catalog Import", Dialog::LabelNames { .. } => "Edit Color Label Names", Dialog::CaptureTime { .. } => "Edit Capture Time", Dialog::RenameKeyword { .. } => "Rename Keyword", @@ -344,6 +345,20 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { } Dialog::DenoiseModel { info, accepted } => crate::panels::denoise::model_dialog(app, ui, &t, info, accepted), Dialog::FaceModel { info, accepted, .. } => crate::panels::faces::model_dialog(app, ui, &t, info, accepted), + Dialog::LightroomResult { summary, library, details, .. } => { + ui.label(summary.as_str()); + ui.add_space(8.0); + if let Some(library) = library { + ui.label(crate::i18n::tr("Destination library")); + ui.add(egui::Label::new(library.as_str()).wrap().selectable(true)); + ui.label(crate::i18n::tr("Lightroom catalogs import into the open library; no separate catalog folder is created.")); + } else { + ui.label(crate::i18n::tr("These photos are only in memory. Open a library on disk and import the catalog again to save them.")); + } + for detail in details.iter() { + ui.add(egui::Label::new(detail).wrap().selectable(true)); + } + } Dialog::SystemInfo { rows } => { egui::Grid::new("sysinfo").num_columns(2).spacing([16.0, 4.0]).striped(true).show(ui, |ui| { for (k, v) in rows.iter() { @@ -1174,6 +1189,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { // only shows something (one Close, no Cancel / OK) let unusable_model = matches!(&dlg, Dialog::DenoiseModel { info, .. } | Dialog::FaceModel { info, .. } if info["kind"] == "unsupported"); let informational = unusable_model + || matches!(dlg, Dialog::LightroomResult { first: None, .. }) || matches!( dlg, Dialog::About | Dialog::Shortcuts | Dialog::Settings { .. } | Dialog::SystemInfo { .. } | Dialog::AllMetadata { .. } | Dialog::WhatsNew @@ -1185,6 +1201,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { let cancel = match &dlg { Dialog::SamModel { .. } if sam_running || sam_installed || sam_nowhere => "Close", Dialog::SamModel { .. } => "Not Now", + Dialog::LightroomResult { .. } => "Close", _ => "Cancel", }; if !informational { @@ -1202,6 +1219,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { add_label = crate::i18n::tr_format!("{verb} {n} Photo{}", if n == 1 { "" } else { "s" }, verb = crate::i18n::tr(verb), n = n); add_label.as_str() } + Dialog::LightroomResult { first: Some(_), .. } => "Show in Library", Dialog::ContactSheet { .. } => "Choose PDF…", Dialog::Merge { .. } => "Merge", Dialog::DenoiseModel { info, .. } if info["download"].is_string() => "Accept & Download", @@ -1506,6 +1524,20 @@ pub(crate) fn album_entries(cat: &lightcraft_catalog::Catalog, editing: Option Result { match dlg { + Dialog::LightroomResult { first, .. } => { + if let Some(first) = first { + app.run("library.source", json!({"kind": "all"}))?; + let mut filter = serde_json::to_value(lightcraft_catalog::Filter::default()).map_err(|e| e.to_string())?; + // The filter command merges a patch; these defaults are omitted by serde. + filter["only"] = json!([]); + filter["labels"] = json!([]); + filter["ruleSet"] = serde_json::Value::Null; + app.run("library.filter", filter)?; + app.run("library.select", json!({"ids": [first]}))?; + app.ui.view = crate::state::ViewMode::PhotoGrid; + } + Ok(serde_json::Value::Null) + } Dialog::SamModel { then, .. } => { if app.session.segmenter.installed() { // installed: start what the user was doing diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index 3c7261e2..28f7076d 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -945,6 +945,7 @@ fn switch_library(app: &mut LightcraftApp, path: &str) -> Result app.session.close_library().map_err(|e| e.to_string())?; let opened = app.session.open_library(&dir, false).map(|_| ()).map_err(|e| format!("{path}: {e}")); app.lightroom_last = None; + app.lightroom_result = None; app.renderer.forget_all(); app.ui.compare = None; if let Err(err) = opened { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index b76066d5..7bcaa0d7 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -763,6 +763,13 @@ pub struct NameEdit { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum Dialog { + /// A Lightroom import result stays readable after its activity row disappears. + LightroomResult { + summary: String, + library: Option, + details: Vec, + first: Option, + }, ContactSheet { options: lightcraft_engine::contact_sheet::Options, }, diff --git a/crates/ui-egui/src/tests_open_lightroom.rs b/crates/ui-egui/src/tests_open_lightroom.rs index 028801b5..cddfa2be 100644 --- a/crates/ui-egui/src/tests_open_lightroom.rs +++ b/crates/ui-egui/src/tests_open_lightroom.rs @@ -88,8 +88,10 @@ fn open_library_on_a_lightroom_catalog_offers_its_import() { // library open now. (This fake catalog then fails to read: only its path matters here.) assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T)["ok"], true); h.step(); - assert!(h.app.ui.dialog.is_none()); - h.step_until(T, |h| h.app.lightroom.is_none()); + // A fast read failure can already have replaced the offer with its lasting result. + assert!(!matches!(h.app.ui.dialog, Some(Dialog::LightroomCatalog { .. }))); + assert!(h.step_until(T, |h| h.app.lightroom.is_none())); + assert!(matches!(&h.app.ui.dialog, Some(Dialog::LightroomResult { summary, first: None, .. }) if summary.contains("failed"))); let last = h.app.lightroom_last.clone().unwrap_or_default(); assert_eq!(last["path"], json!(lrcat), "{last}"); assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.clone()), Some(lib.clone())); diff --git a/docs/gaps.md b/docs/gaps.md index 7c4f5622..c2c465a3 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps: where LightCraft falls short of Lightroom -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (catalog import: destination and offline counts stay readable; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is @@ -27,7 +27,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app | 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 235 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) | | 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) | | 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) | -| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) | +| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data; offline record counts and destination are now shown in a lasting import result | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) | | 14 | **HDR display** (EDR / Windows HDR); HLG; the headroom limit and SDR rendition from `crs` XMP (the HDR switch, `HDREditMode`, is read) | HDR rows 70%; LR-VIEW-HDR-DISPLAY ⬜ | HDR editing works blind on HDR monitors | 12–25 | [hardware-parity](hardware-parity.md) | | 15 | **Print module**: custom packages, printer colour management (contact-sheet PDF only) | LRC-PRINT rows | Classic users who print | 25–40 | [target-app-parity](target-app-parity.md#features) | | 16 | **Map view, Book, Slideshow module, publish services** | LRC-MAP / BOOK / SS rows ⬜ | Classic users; publish services matter for Flickr / SmugMug workflows | 60–100 | [target-app-parity](target-app-parity.md#features) | @@ -81,3 +81,4 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | 2026-10-10 | minor | RAW gaps re-measured model by model (raw-parity.md): CR3, compressed ORF and verification entries carry measured shares; HE NEF no longer a beta blocker (+1.1 points for 20–40 h); verification entry absorbs the cheap per-body fixes (#769, #702, A200 series) | | 2026-10-10 | minor | Beta total raised to 340–600 h to include the ~92 open core-path user issues | | 2026-10-10 | major | Created from the checklist's Top gaps (whose items 1–2 had been duplicated by a merge) and ROADMAP's Where we stand; re-ranked; added evidence, impact, hours and beta flags; added platform-CI, lens-database, HE NEF and localization gaps | +| 2026-10-12 | minor | Catalog migration reports offline records as additions, keeps the destination/result readable, skips missing-original probes and stages only masters needed for virtual copies. Approximate rendering and database/table limits remain. | diff --git a/docs/lightroom-catalog-import.md b/docs/lightroom-catalog-import.md index 878603e0..56e8d79c 100644 --- a/docs/lightroom-catalog-import.md +++ b/docs/lightroom-catalog-import.md @@ -15,9 +15,19 @@ lightcraft-cli run --library "LightCraft Library" library.inspectLightroom path= lightcraft-cli run --library "LightCraft Library" library.importLightroom path="Lightroom Catalog.lrcat" ``` +Import adds records to the currently open LightCraft library; it does not create a separate +folder named after the Lightroom catalog. The desktop result stays open until dismissed and +shows the destination library, the number of records added, missing originals and warnings. +**Show in Library** opens All Photos, clears filters and selects an imported record. Errors stay +readable in the same result window. If another dialog is open, the result waits until that dialog closes. The result +is also available through `library.importLightroom {"status":true}` during that session. + Original files are referenced in place. Ratings, picks/rejects, color labels, XMP metadata, hierarchical keywords, regular collections/collection sets, virtual copies and mapped develop -settings are imported. Missing originals remain catalogued for relinking. Existing LightCraft +settings are imported. Missing originals remain catalogued for relinking, count as added records and are reported +separately from failures reading files that are present. Known-missing originals are not probed +again. Virtual copies count as added records too; reimport reports zero additions when all +records already exist. Existing LightCraft edits are preserved by default; `updateExisting=true` explicitly replaces them. Persistent import identity prevents reimport from duplicating virtual copies and collections. One undo step reverses catalog changes; any saved recovery archives remain available. @@ -60,6 +70,9 @@ Lightroom's `-999999` deferred-adjustment sentinel is omitted from both catalog it is reported, included in any saved archive, and never clamped into a real slider value. Deferred Adobe Auto Tone is not evaluated by the importer; LightCraft's Auto control remains available after migration. +New photo records are moved into staged operations; only masters needed for virtual copies +retain a second owned record while the import commits. + Native desktop inspection/import runs on a cancellable background worker with progress. Only prepared catalog operations commit on the owner thread; archive index writing stays on a worker. Late results from a closed or switched library are rejected, including reopening the same path. diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 77ae26e4..1192cb92 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (catalog import: lasting result with destination and accurate offline counts; rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -689,7 +689,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- | LRC-LIB-SURVEY | Survey view | P2 | ✅ | `cmd:view.survey` | N | | LRC-LIB-REFVIEW | Reference view | P2 | ✅ | `cmd:view.reference`, `cmd:photo.setReference`, `crates/ui-egui/src/panels/compare.rs` (`show_reference`) | View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo | | LRC-LIB-CATALOG-PANEL | Catalog sets | P1 | ✅ | `cmd:library.source` (`missing`) | all, recently added, picks, recently deleted, missing photos (when any) | -| LRC-LIB-CATALOG-IMPORT | Native Lightroom Classic catalog import | P1 | 🟡 | `cmd:library.inspectLightroom`, `cmd:library.importLightroom`, `crates/engine/src/lightroom_catalog.rs`, `docs/lightroom-catalog-import.md` | read-only `.lrcat` plus committed WAL pages, in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, a root folder not found at its recorded path looked up relative to the catalog (Lightroom's `relativePathFromCatalog`, so a project folder copied from another computer stays linked; reported as a warning), existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Custom white balance on relative-WB raws (`RAW` in Lightroom's terms) stays As Shot and is reported, since the catalog keeps no as-shot white to shift from (issue #510). Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs. Open Library given a Lightroom catalog folder, an `.lrdata` / `.lrcat-data` folder or the `.lrcat` makes no library there and offers this import with the catalog found (`cmd:app.openLibrary`, `crates/engine/src/lightroom_folder.rs`; issue #891) | +| LRC-LIB-CATALOG-IMPORT | Native Lightroom Classic catalog import | P1 | 🟡 | `cmd:library.inspectLightroom`, `cmd:library.importLightroom`, `crates/engine/src/lightroom_catalog.rs`, `docs/lightroom-catalog-import.md` | read-only `.lrcat` plus committed WAL pages, in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; persistent result with destination library, offline-original counts and Show in Library; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, a root folder not found at its recorded path looked up relative to the catalog (Lightroom's `relativePathFromCatalog`, so a project folder copied from another computer stays linked; reported as a warning), existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Custom white balance on relative-WB raws (`RAW` in Lightroom's terms) stays As Shot and is reported, since the catalog keeps no as-shot white to shift from (issue #510). Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs. Open Library given a Lightroom catalog folder, an `.lrdata` / `.lrcat-data` folder or the `.lrcat` makes no library there and offers this import with the catalog found (`cmd:app.openLibrary`, `crates/engine/src/lightroom_folder.rs`; issue #891) | | LRC-LIB-CATALOG-LOCK | Catalog open in one program at a time | P1 | ✅ | `cmd:app.openLibrary`, `crates/catalog/src/lock.rs`, `crates/engine/src/library.rs` (`open_library`) | exclusive OS lock on `catalog.lock` for the session (app and `lightcraft-cli --library`); a second opener is refused with who has it (`catalog.lock.owner`: program, process, computer); released by the OS on a crash, so never stale; file systems without locks open unprotected with a warning (the app's notices, `library.info` → `settingsWarnings`, and the CLI's stderr; issue #171); no read-only mode; the browser build (OPFS) has no guard across tabs yet. A library that can't be opened at launch (locked, unreadable, newer format, drive missing) shows a window — Try Again, Choose Another Library…, Continue Without Saving (then a banner; nothing written), Quit — instead of a silent demo session (`crates/ui-egui/src/panels/library_problem.rs`); the lock is released explicitly when the library closes, not by letting its file handle close, so a program another thread is starting at that moment (which holds a copy of every file handle until it is launched) cannot keep the library "already open" for a moment (issue #680) | | LRC-LIB-CATALOG-SWITCH | New / Open Recent catalog, catalog name shown | P1 | ✅ | `cmd:app.newLibrary`, `cmd:app.openLibrary`, `cmd:app.recentLibraries`, `cmd:app.clearRecentLibraries`, `crates/ui-egui/src/panels/settings.rs`, `crates/ui-egui/src/panels/left.rs` | File ▸ New Library… (name, then location), File ▸ Open Recent Library (last 8; missing ones greyed out, dropped when opened; Clear), the open library's name under My Photos and in the window title (issue #902); a library that can't be opened leaves the current one open | | LRC-LIB-FOLDERS | Disk folder tree | P1 | ✅ | `cmd:library.browse`, `cmd:folder.rename`, `cmd:folder.move`, `crates/ui-egui/src/panels/left.rs` (`local_section`, `folder_tree`) | standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; one undo step each: undo / redo move the folder back and forth, refused when the destination is taken) | @@ -794,3 +794,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-10 | patch | LR-BEHAV-UNDO: file/XMP repair retries and merged rename Undo/Redo evidence; status and summary counts unchanged. | | 2026-10-10 | minor | Renamed from `parity.md` (progress-docs standard); Top gaps moved to gaps.md (its duplicated items 1–2 from a bad merge resolved there), shortcut notes moved to ui-parity.md. Rows unchanged; a recount of the rows gives 81.1% of 517 in-scope rows (P0 98.0%, P1 95.1%, P2 46.9%), so the generated summary above is due a `cargo xtask parity --write` | | 2026-09-30 – 2026-10-11 | — | Rows added and updated with each feature (see `git log -- docs/parity.md docs/parity-checklist.md`) | +| 2026-10-12 | minor | LRC-LIB-CATALOG-IMPORT: lasting destination/result, offline records and virtual copies count as added; redundant missing-file probes and staged photo copies removed. Status and summary counts unchanged. |