Compare commits

...
Author SHA1 Message Date
skylartaylor 0ec463568b 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.
2026-10-13 04:53:04 +02:00
30 changed files with 382 additions and 47 deletions

No files matched your search

+79 -7
View File
@@ -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<PhotoId, Photo> = 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()));
+29 -14
View File
@@ -190,20 +190,11 @@ impl LightroomJob {
return Err(error("Lightroom import cancelled"));
}
self.import.done.fetch_add(1, Ordering::Relaxed);
let paths: Vec<String> = 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<String>, HashSet<String>)> {
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 }
+1 -1
View File
@@ -247,7 +247,7 @@ fn fill_box(src: &mut [[f32; 3]], tgt: &[[f32; 3]], alpha: &[f32], bw: usize, bh
let flat: Vec<f32> = 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]];
}
}
+4 -1
View File
@@ -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."
}
+6 -1
View File
@@ -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."
}
+4 -1
View File
@@ -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."
}
+6 -1
View File
@@ -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."
}
+4 -1
View File
@@ -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."
}
+6 -1
View File
@@ -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."
}
+4 -1
View File
@@ -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}件です。"
}
+6 -1
View File
@@ -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.": "これらの写真はメモリ内にのみあります。ディスク上のライブラリを開き、カタログを再度読み込んで保存してください。"
}
+4 -1
View File
@@ -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."
}
+6 -1
View File
@@ -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."
}
+4 -1
View File
@@ -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}."
}
+6 -1
View File
@@ -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.": "Эти фотографии находятся только в памяти. Откройте библиотеку на диске и импортируйте каталог повторно, чтобы сохранить их."
}
+4 -1
View File
@@ -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}."
}
+6 -1
View File
@@ -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.": "Ці фотографії є лише в пам’яті. Відкрийте бібліотеку на диску та імпортуйте каталог повторно, щоб зберегти їх."
}
+4 -1
View File
@@ -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} 个收藏集。"
}
+6 -1
View File
@@ -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.": "这些照片仅保存在内存中。请打开磁盘上的图库并再次导入目录以保存照片。"
}
+4 -1
View File
@@ -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} 個收藏集。"
}
+6 -1
View File
@@ -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.": "這些相片僅保存在記憶體中。請開啟磁碟上的圖庫並再次匯入目錄以儲存相片。"
}
+3
View File
@@ -362,6 +362,8 @@ pub struct LightcraftApp {
pub lightroom: Option<lightroom_import::LightroomTask>,
/// Last terminal Lightroom result, exposed by the command's status/wait response.
pub lightroom_last: Option<Value>,
/// A completed import result waiting for another modal dialog to close.
pub(crate) lightroom_result: Option<state::Dialog>,
/// The library generation the window title and the recent list were last updated for.
pub shown_library: Option<u64>,
/// 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(),
+116
View File
@@ -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<Value, String> {
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 {
+32
View File
@@ -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<u
pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result<serde_json::Value, String> {
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
+1
View File
@@ -945,6 +945,7 @@ fn switch_library(app: &mut LightcraftApp, path: &str) -> Result<Value, String>
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 {
+7
View File
@@ -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<String>,
details: Vec<String>,
first: Option<u64>,
},
ContactSheet {
options: lightcraft_engine::contact_sheet::Options,
},
+4 -2
View File
@@ -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()));
+3 -2
View File
@@ -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. |
+14 -1
View File
@@ -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.
+3 -2
View File
@@ -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. |