Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec463568b |
No files matched your search
@@ -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()));
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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}件です。"
|
||||
}
|
||||
@@ -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.": "これらの写真はメモリ内にのみあります。ディスク上のライブラリを開き、カタログを再度読み込んで保存してください。"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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}."
|
||||
}
|
||||
@@ -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.": "Эти фотографии находятся только в памяти. Откройте библиотеку на диске и импортируйте каталог повторно, чтобы сохранить их."
|
||||
}
|
||||
@@ -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}."
|
||||
}
|
||||
@@ -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.": "Ці фотографії є лише в пам’яті. Відкрийте бібліотеку на диску та імпортуйте каталог повторно, щоб зберегти їх."
|
||||
}
|
||||
@@ -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} 个收藏集。"
|
||||
}
|
||||
@@ -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.": "这些照片仅保存在内存中。请打开磁盘上的图库并再次导入目录以保存照片。"
|
||||
}
|
||||
@@ -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} 個收藏集。"
|
||||
}
|
||||
@@ -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.": "這些相片僅保存在記憶體中。請開啟磁碟上的圖庫並再次匯入目錄以儲存相片。"
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
Reference in new issue
Block a user