mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-21 14:44:23 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7657c2958 | ||
|
|
f730805dc3 | ||
|
|
0ec463568b |
No files matched your search
@@ -10,6 +10,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::DevelopSettings;
|
||||
|
||||
@@ -185,8 +186,7 @@ fn scale_patch(base: &Value, patch: &Value, t: f64) -> Value {
|
||||
/// Apply a partial settings object with an amount (1.0 = 100 %). Out-of-range values are clamped
|
||||
/// through the control specs.
|
||||
pub fn apply_partial(s: &DevelopSettings, partial: &Value, amount: f64) -> DevelopSettings {
|
||||
let base = s.to_json();
|
||||
let patch = if (amount - 1.0).abs() < 1e-9 { partial.clone() } else { scale_patch(&base, partial, amount) };
|
||||
let patch = if (amount - 1.0).abs() < 1e-9 { Cow::Borrowed(partial) } else { Cow::Owned(scale_patch(&s.to_json(), partial, amount)) };
|
||||
// (a white balance without a scale keeps its old meaning: `DevelopSettings::merged`)
|
||||
let mut out = s.merged(&patch).unwrap_or_else(|_| s.clone());
|
||||
for c in crate::controls::CONTROLS {
|
||||
|
||||
@@ -506,16 +506,22 @@ impl Session {
|
||||
}
|
||||
|
||||
fn apply_catalog(&mut self, op: Op) -> Result<Op> {
|
||||
let inv = self.catalog.apply(op.clone())?;
|
||||
// Retain only cache IDs while the catalog consumes the operation. Cloning an import
|
||||
// batch here also cloned every photo, although commit already keeps the journal copy.
|
||||
let mut invalidated = Vec::new();
|
||||
let mut pending = vec![&op];
|
||||
while let Some(op) = pending.pop() {
|
||||
match op {
|
||||
Op::SetFile { id, .. } | Op::Relink { id, .. } | Op::SetContent { id, .. } | Op::RemovePhoto { id } => self.media.forget(*id),
|
||||
Op::AddPhoto { photo } => self.media.forget(photo.id),
|
||||
Op::SetFile { id, .. } | Op::Relink { id, .. } | Op::SetContent { id, .. } | Op::RemovePhoto { id } => invalidated.push(*id),
|
||||
Op::AddPhoto { photo } => invalidated.push(photo.id),
|
||||
Op::Batch { ops } => pending.extend(ops),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let inv = self.catalog.apply(op)?;
|
||||
for id in invalidated {
|
||||
self.media.forget(id);
|
||||
}
|
||||
Ok(inv)
|
||||
}
|
||||
|
||||
|
||||
@@ -698,14 +698,22 @@ 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 promoted = HashSet::new();
|
||||
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 +744,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,7 +812,21 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex
|
||||
if keep_develop {
|
||||
(p.develop, p.edited) = previous_develop;
|
||||
}
|
||||
// Missing files bypass normal-file preparation, including its Local promotion.
|
||||
// Import still joins the library, and a virtual copy must inherit that membership.
|
||||
let was_local = p.local;
|
||||
p.local = false;
|
||||
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() {
|
||||
if was_local && promoted.insert(p.id) {
|
||||
imported += 1;
|
||||
ops.push(Op::SetLocal { id: p.id, local: false });
|
||||
}
|
||||
ops.extend([
|
||||
Op::SetRating { id: p.id, rating: p.rating },
|
||||
Op::SetFlag { id: p.id, flag: p.flag },
|
||||
@@ -817,10 +836,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 +912,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(),
|
||||
})
|
||||
}
|
||||
@@ -938,12 +957,62 @@ mod tests {
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn offline_local_master_and_copy_join_library_with_one_undo_step() {
|
||||
let mut s = crate::Session::new();
|
||||
let data = sample();
|
||||
let master_id = s.catalog.alloc_photo_id();
|
||||
let mut master = Photo::new(master_id, Source::File { path: "/missing/test.ARW".into() }, "test.ARW", "ARW", 20, 10, "before");
|
||||
master.local = true;
|
||||
s.commit("Browse folder", Op::AddPhoto { photo: Box::new(master) }).unwrap();
|
||||
s.undo.clear();
|
||||
let before = s.catalog.photo(master_id).unwrap().clone();
|
||||
let mut index = ImportIndex::default();
|
||||
let missing_sources = [key("/missing/test.ARW")].into_iter().collect();
|
||||
let mut import = |s: &mut crate::Session| {
|
||||
let undo0 = s.undo.len();
|
||||
apply_prepared(
|
||||
s,
|
||||
data.clone(),
|
||||
ApplyPrepared {
|
||||
update_existing: false,
|
||||
files: Default::default(),
|
||||
index: &mut index,
|
||||
archive_path: None,
|
||||
missing_sources: &missing_sources,
|
||||
undo0,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.report
|
||||
};
|
||||
let first = import(&mut s);
|
||||
assert_eq!(first["mapping"]["1"], master_id.0);
|
||||
assert_eq!(first["imported"], 2, "the promoted master and new copy both join All Photos");
|
||||
assert_eq!(first["missing"], json!(["/missing/test.ARW"]));
|
||||
assert_eq!(s.catalog.len(), 2);
|
||||
assert!(s.catalog.photos().all(|photo| !photo.local));
|
||||
assert_eq!(s.undo.len(), 1);
|
||||
let second = import(&mut s);
|
||||
assert_eq!(second["imported"], 0);
|
||||
assert_eq!(second["mapping"], first["mapping"]);
|
||||
s.undo_step().unwrap();
|
||||
s.undo_step().unwrap();
|
||||
assert_eq!(s.catalog.len(), 1);
|
||||
assert_eq!(s.catalog.photo(master_id), Some(&before));
|
||||
assert!(s.catalog.albums().next().is_none());
|
||||
let third = import(&mut s);
|
||||
assert_eq!(third["imported"], 2);
|
||||
assert!(s.catalog.photos().all(|photo| !photo.local));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_copies_collections_ratings_and_mapped_edits_in_one_undo_step() {
|
||||
let mut s = crate::Session::new();
|
||||
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 +1310,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 +1432,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()));
|
||||
|
||||
@@ -247,23 +247,14 @@ impl LightroomJob {
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
return Err(cancelled());
|
||||
}
|
||||
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();
|
||||
// the catalog step's last unit (validation, and looking for each original), then the photos
|
||||
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.
|
||||
// Complete the catalog step before the separate read/add photo progress starts.
|
||||
self.progress.catalog_done.fetch_add(1, Ordering::Relaxed);
|
||||
self.progress.photos.store(true, Ordering::Relaxed);
|
||||
let normal = self.import.prepare(&paths, cancel);
|
||||
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(cancelled());
|
||||
@@ -323,6 +314,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(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 }
|
||||
|
||||
@@ -6,6 +6,39 @@ fn demo() -> Session {
|
||||
Session::with_demo()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_catalog_operations_invalidate_sources_after_success_and_on_undo_redo() {
|
||||
use crate::media::SourceLevel;
|
||||
use lightcraft_catalog::{Op, PhotoId};
|
||||
let mut s = demo();
|
||||
let id = s.active().unwrap();
|
||||
let image = std::sync::Arc::new(lightcraft_raster::Rgb32f::new(1, 1));
|
||||
let cache = |s: &mut Session| {
|
||||
for level in [SourceLevel::Thumb, SourceLevel::Preview, SourceLevel::Full] {
|
||||
s.media.insert(id, level, image.clone());
|
||||
}
|
||||
};
|
||||
cache(&mut s);
|
||||
// An invalid operation must not throw away usable sources.
|
||||
assert!(s.apply_catalog(Op::Batch { ops: vec![Op::RemovePhoto { id }, Op::RemovePhoto { id: PhotoId(u64::MAX) }] }).is_err());
|
||||
assert!(s.catalog.photo(id).is_some(), "the failed batch restores the removed photo");
|
||||
for level in [SourceLevel::Thumb, SourceLevel::Preview, SourceLevel::Full] {
|
||||
assert!(s.media.has_source(id, level), "the failed batch keeps usable sources");
|
||||
}
|
||||
s.commit("Remove", Op::Batch { ops: vec![Op::Batch { ops: vec![Op::RemovePhoto { id }] }] }).unwrap();
|
||||
for level in [SourceLevel::Thumb, SourceLevel::Preview, SourceLevel::Full] {
|
||||
assert!(!s.media.has_source(id, level));
|
||||
}
|
||||
cache(&mut s);
|
||||
s.undo_step().unwrap();
|
||||
assert!(s.catalog.photo(id).is_some());
|
||||
assert!(!s.media.has_source(id, SourceLevel::Full), "undo AddPhoto must invalidate the source too");
|
||||
cache(&mut s);
|
||||
s.redo_step().unwrap();
|
||||
assert!(s.catalog.photo(id).is_none());
|
||||
assert!(!s.media.has_source(id, SourceLevel::Full));
|
||||
}
|
||||
|
||||
fn active_dev(s: &Session) -> lightcraft_develop::DevelopSettings {
|
||||
(*s.develop_of(s.active().unwrap()).unwrap()).clone()
|
||||
}
|
||||
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} Fotos zur Bearbeitung geöffnet; jedes ist mit seinem Original gestapelt",
|
||||
"{name} opened in the other editor": "{name} im anderen Editor geöffnet",
|
||||
"{n} originals opened in the other editor": "{n} Originale im anderen Editor geöffnet",
|
||||
"· {in_source} identical copies": "· {in_source} identische Kopien"
|
||||
"· {in_source} identical copies": "· {in_source} identische Kopien",
|
||||
"{count} more warnings.": "{count} weitere Warnungen.",
|
||||
"{photos} catalog records processed; {collections} collections.": "{photos} Katalogeinträge verarbeitet; {collections} Sammlungen.",
|
||||
"{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."
|
||||
}
|
||||
@@ -2118,5 +2118,10 @@
|
||||
"Adding to Apple Photos": "Wird zu Apple Fotos hinzugefügt",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Kein Grafikprozessor gefunden: LightCraft rendert auf der CPU, was viel langsamer ist. Details unter Hilfe → Systeminformationen.",
|
||||
"On NixOS, start it from `nix develop`.": "Unter NixOS starten Sie es aus `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Dateien mit demselben Inhalt wie eine andere Datei dieses Imports: Nur die erste wird importiert."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Dateien mit demselben Inhalt wie eine andere Datei dieses Imports: Nur die erste wird importiert.",
|
||||
"Lightroom Catalog Import": "Lightroom-Katalogimport",
|
||||
"Destination library": "Zielbibliothek",
|
||||
"Show in Library": "In Bibliothek anzeigen",
|
||||
"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.",
|
||||
"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."
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abiertas para editar; cada una está apilada con su original",
|
||||
"{name} opened in the other editor": "{name} abierto en el otro editor",
|
||||
"{n} originals opened in the other editor": "{n} originales abiertos en el otro editor",
|
||||
"· {in_source} identical copies": "· {in_source} copias idénticas"
|
||||
"· {in_source} identical copies": "· {in_source} copias idénticas",
|
||||
"{count} more warnings.": "{count} advertencias más.",
|
||||
"{photos} catalog records processed; {collections} collections.": "{photos} registros del catálogo procesados; {collections} colecciones.",
|
||||
"{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."
|
||||
}
|
||||
@@ -1995,5 +1995,10 @@
|
||||
"Adding to Apple Photos": "Añadiendo a Fotos de Apple",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "No se encontró ningún procesador gráfico: LightCraft renderiza en la CPU, lo que es mucho más lento. Detalles en Ayuda → Información del sistema.",
|
||||
"On NixOS, start it from `nix develop`.": "En NixOS, inícielo desde `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Archivos con el mismo contenido que otro archivo de esta importación: solo se importa el primero."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Archivos con el mismo contenido que otro archivo de esta importación: solo se importa el primero.",
|
||||
"Lightroom Catalog Import": "Importación de catálogo de Lightroom",
|
||||
"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.",
|
||||
"Show in Library": "Mostrar en la biblioteca",
|
||||
"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.",
|
||||
"Destination library": "Biblioteca de destino"
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} photos ouvertes pour modification ; chacune est empilée avec son original",
|
||||
"{name} opened in the other editor": "{name} ouvert dans l’autre éditeur",
|
||||
"{n} originals opened in the other editor": "{n} originaux ouverts dans l’autre éditeur",
|
||||
"· {in_source} identical copies": "· {in_source} copies identiques"
|
||||
"· {in_source} identical copies": "· {in_source} copies identiques",
|
||||
"{count} more warnings.": "{count} avertissements supplémentaires.",
|
||||
"{photos} catalog records processed; {collections} collections.": "{photos} entrées du catalogue traitées ; {collections} collections.",
|
||||
"{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."
|
||||
}
|
||||
@@ -1957,5 +1957,10 @@
|
||||
"Adding to Apple Photos": "Ajout à Apple Photos",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Aucun processeur graphique trouvé : LightCraft effectue le rendu sur le processeur, ce qui est beaucoup plus lent. Détails dans Aide → Informations système.",
|
||||
"On NixOS, start it from `nix develop`.": "Sous NixOS, lancez-le depuis `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Fichiers au contenu identique à un autre fichier de cette importation : seul le premier est importé."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Fichiers au contenu identique à un autre fichier de cette importation : seul le premier est importé.",
|
||||
"Lightroom Catalog Import": "Importation de catalogue Lightroom",
|
||||
"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.",
|
||||
"Show in Library": "Afficher dans la bibliothèque",
|
||||
"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éé.",
|
||||
"Destination library": "Bibliothèque de destination"
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています",
|
||||
"{name} opened in the other editor": "{name}を別のエディタで開きました",
|
||||
"{n} originals opened in the other editor": "{n}個の元ファイルを別のエディタで開きました",
|
||||
"· {in_source} identical copies": "・同一のコピー {in_source} 件"
|
||||
"· {in_source} identical copies": "・同一のコピー {in_source} 件",
|
||||
"{count} more warnings.": "ほかに{count}件の警告があります。",
|
||||
"{photos} catalog records processed; {collections} collections.": "カタログの{photos}件のレコードを処理しました。コレクションは{collections}件です。",
|
||||
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing}件のオリジナルが見つかりません。カタログのレコードは利用できます。「見つからない写真を検索」で再リンクしてください。"
|
||||
}
|
||||
@@ -1961,5 +1961,10 @@
|
||||
"Adding to Apple Photos": "Appleの写真に追加中",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "グラフィックプロセッサが見つかりません。LightCraftはCPUで描画するため、かなり遅くなります。詳細は「ヘルプ → システム情報」を参照してください。",
|
||||
"On NixOS, start it from `nix develop`.": "NixOSでは `nix develop` から起動してください。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "この読み込み内の別のファイルと内容が同じファイルです。最初のファイルだけが読み込まれます。"
|
||||
"Files with the same content as another file in this import: only the first is imported.": "この読み込み内の別のファイルと内容が同じファイルです。最初のファイルだけが読み込まれます。",
|
||||
"Lightroom Catalog Import": "Lightroomカタログの読み込み",
|
||||
"These photos are only in memory. Open a library on disk and import the catalog again to save them.": "これらの写真はメモリ内にのみあります。ディスク上のライブラリを開き、カタログを再度読み込んで保存してください。",
|
||||
"Show in Library": "ライブラリに表示",
|
||||
"Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroomカタログは開いているライブラリに読み込まれます。別のカタログフォルダーは作成されません。",
|
||||
"Destination library": "読み込み先ライブラリ"
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abertas para edição; cada uma está empilhada com a original",
|
||||
"{name} opened in the other editor": "{name} aberto no outro editor",
|
||||
"{n} originals opened in the other editor": "{n} originais abertos no outro editor",
|
||||
"· {in_source} identical copies": "· {in_source} cópias idênticas"
|
||||
"· {in_source} identical copies": "· {in_source} cópias idênticas",
|
||||
"{count} more warnings.": "Mais {count} avisos.",
|
||||
"{photos} catalog records processed; {collections} collections.": "{photos} registros do catálogo processados; {collections} coleções.",
|
||||
"{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."
|
||||
}
|
||||
@@ -1998,5 +1998,10 @@
|
||||
"Recognition models": "Modelos de reconhecimento",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Nenhum processador gráfico encontrado: o LightCraft renderiza na CPU, o que é muito mais lento. Detalhes em Ajuda → Informações do sistema.",
|
||||
"On NixOS, start it from `nix develop`.": "No NixOS, inicie-o a partir de `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Arquivos com o mesmo conteúdo de outro arquivo desta importação: apenas o primeiro é importado."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Arquivos com o mesmo conteúdo de outro arquivo desta importação: apenas o primeiro é importado.",
|
||||
"Lightroom Catalog Import": "Importação de catálogo do Lightroom",
|
||||
"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.",
|
||||
"Show in Library": "Mostrar na biblioteca",
|
||||
"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.",
|
||||
"Destination library": "Biblioteca de destino"
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом",
|
||||
"{name} opened in the other editor": "{name} открыт в другом редакторе",
|
||||
"{n} originals opened in the other editor": "Открыто оригиналов в другом редакторе: {n}",
|
||||
"· {in_source} identical copies": "· идентичных копий: {in_source}"
|
||||
"· {in_source} identical copies": "· идентичных копий: {in_source}",
|
||||
"{count} more warnings.": "Других предупреждений: {count}.",
|
||||
"{photos} catalog records processed; {collections} collections.": "Обработано записей каталога: {photos}; коллекций: {collections}.",
|
||||
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "Не найдены оригиналы: {missing}. Их записи каталога доступны; используйте поиск отсутствующих фотографий, чтобы восстановить ссылки."
|
||||
}
|
||||
@@ -2151,5 +2151,10 @@
|
||||
"Adding to Apple Photos": "Добавление в Apple Фото",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Графический процессор не найден: LightCraft выполняет рендеринг на CPU, а это намного медленнее. Подробности: Справка → Сведения о системе.",
|
||||
"On NixOS, start it from `nix develop`.": "В NixOS запускайте его из `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файлы с тем же содержимым, что и другой файл этого импорта: импортируется только первый."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файлы с тем же содержимым, что и другой файл этого импорта: импортируется только первый.",
|
||||
"Lightroom Catalog Import": "Импорт каталога Lightroom",
|
||||
"Destination library": "Библиотека назначения",
|
||||
"Show in Library": "Показать в библиотеке",
|
||||
"Lightroom catalogs import into the open library; no separate catalog folder is created.": "Каталоги Lightroom импортируются в открытую библиотеку; отдельная папка каталога не создаётся.",
|
||||
"These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Эти фотографии находятся только в памяти. Откройте библиотеку на диске и импортируйте каталог повторно, чтобы сохранить их."
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом",
|
||||
"{name} opened in the other editor": "{name} відкрито в іншому редакторі",
|
||||
"{n} originals opened in the other editor": "Відкрито оригіналів в іншому редакторі: {n}",
|
||||
"· {in_source} identical copies": "· ідентичних копій: {in_source}"
|
||||
"· {in_source} identical copies": "· ідентичних копій: {in_source}",
|
||||
"{count} more warnings.": "Інших попереджень: {count}.",
|
||||
"{photos} catalog records processed; {collections} collections.": "Оброблено записів каталогу: {photos}; колекцій: {collections}.",
|
||||
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "Не знайдено оригінали: {missing}. Їхні записи каталогу доступні; скористайтеся пошуком відсутніх фотографій, щоб відновити посилання."
|
||||
}
|
||||
@@ -2151,5 +2151,10 @@
|
||||
"Adding to Apple Photos": "Додавання до Apple Фото",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Графічний процесор не знайдено: LightCraft виконує рендеринг на ЦП, а це набагато повільніше. Докладніше: Довідка → Інформація про систему.",
|
||||
"On NixOS, start it from `nix develop`.": "У NixOS запускайте його з `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файли з тим самим вмістом, що й інший файл цього імпорту: імпортується лише перший."
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файли з тим самим вмістом, що й інший файл цього імпорту: імпортується лише перший.",
|
||||
"Lightroom Catalog Import": "Імпорт каталогу Lightroom",
|
||||
"Destination library": "Бібліотека призначення",
|
||||
"Show in Library": "Показати в бібліотеці",
|
||||
"Lightroom catalogs import into the open library; no separate catalog folder is created.": "Каталоги Lightroom імпортуються у відкриту бібліотеку; окрема папка каталогу не створюється.",
|
||||
"These photos are only in memory. Open a library on disk and import the catalog again to save them.": "Ці фотографії є лише в пам’яті. Відкрийте бібліотеку на диску та імпортуйте каталог повторно, щоб зберегти їх."
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起",
|
||||
"{name} opened in the other editor": "已在其他编辑器中打开 {name}",
|
||||
"{n} originals opened in the other editor": "已在其他编辑器中打开 {n} 个原始文件",
|
||||
"· {in_source} identical copies": "· {in_source} 个相同副本"
|
||||
"· {in_source} identical copies": "· {in_source} 个相同副本",
|
||||
"{count} more warnings.": "另有 {count} 条警告。",
|
||||
"{photos} catalog records processed; {collections} collections.": "已处理 {photos} 条目录记录;{collections} 个收藏集。",
|
||||
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "有 {missing} 个原始文件缺失。其目录记录可用;请使用“查找缺失照片”重新链接文件。"
|
||||
}
|
||||
@@ -1997,5 +1997,10 @@
|
||||
"Adding to Apple Photos": "正在添加到 Apple 照片",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "未找到图形处理器:LightCraft 改用 CPU 渲染,速度会慢很多。详情见“帮助 → 系统信息”。",
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,请从 `nix develop` 启动。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "与本次导入中另一个文件内容相同的文件:只导入第一个。"
|
||||
"Files with the same content as another file in this import: only the first is imported.": "与本次导入中另一个文件内容相同的文件:只导入第一个。",
|
||||
"Lightroom Catalog Import": "Lightroom 目录导入",
|
||||
"These photos are only in memory. Open a library on disk and import the catalog again to save them.": "这些照片仅保存在内存中。请打开磁盘上的图库并再次导入目录以保存照片。",
|
||||
"Show in Library": "在图库中显示",
|
||||
"Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroom 目录会导入到当前打开的图库中,不会创建单独的目录文件夹。",
|
||||
"Destination library": "目标图库"
|
||||
}
|
||||
@@ -247,5 +247,8 @@
|
||||
"{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起",
|
||||
"{name} opened in the other editor": "已在其他編輯器中開啟 {name}",
|
||||
"{n} originals opened in the other editor": "已在其他編輯器中開啟 {n} 個原始檔案",
|
||||
"· {in_source} identical copies": "· {in_source} 個相同副本"
|
||||
"· {in_source} identical copies": "· {in_source} 個相同副本",
|
||||
"{count} more warnings.": "另有 {count} 則警告。",
|
||||
"{photos} catalog records processed; {collections} collections.": "已處理 {photos} 筆目錄記錄;{collections} 個收藏集。",
|
||||
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "有 {missing} 個原始檔案遺失。其目錄記錄可用;請使用「尋找遺失相片」重新連結檔案。"
|
||||
}
|
||||
@@ -1959,5 +1959,10 @@
|
||||
"Adding to Apple Photos": "正在加入 Apple 照片",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "找不到圖形處理器:LightCraft 改用 CPU 算繪,速度會慢很多。詳情請見「說明 → 系統資訊」。",
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,請從 `nix develop` 啟動。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "與本次匯入中另一個檔案內容相同的檔案:只匯入第一個。"
|
||||
"Files with the same content as another file in this import: only the first is imported.": "與本次匯入中另一個檔案內容相同的檔案:只匯入第一個。",
|
||||
"Lightroom Catalog Import": "Lightroom 目錄匯入",
|
||||
"These photos are only in memory. Open a library on disk and import the catalog again to save them.": "這些相片僅保存在記憶體中。請開啟磁碟上的圖庫並再次匯入目錄以儲存相片。",
|
||||
"Show in Library": "在圖庫中顯示",
|
||||
"Lightroom catalogs import into the open library; no separate catalog folder is created.": "Lightroom 目錄會匯入目前開啟的圖庫,不會建立獨立的目錄資料夾。",
|
||||
"Destination library": "目標圖庫"
|
||||
}
|
||||
@@ -263,25 +263,39 @@ mod contact_sheet_tests {
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
// Exercise the menu/picker/background PDF workflow with a small procedural original.
|
||||
// A full-size demo makes this test's deadline depend on unrelated concurrent renders.
|
||||
let demo = lightcraft_engine::Session::with_demo();
|
||||
let id = demo.active().unwrap();
|
||||
let mut photo = (**demo.catalog.photo(id).unwrap()).clone();
|
||||
photo.width = 96;
|
||||
photo.height = 64;
|
||||
let mut session = lightcraft_engine::Session::new();
|
||||
session.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(photo) }).unwrap();
|
||||
session.execute("library.select", &json!({"ids": [id.0], "active": id.0})).unwrap();
|
||||
let mut app = LightcraftApp::new(session, services);
|
||||
app.run("dialog.contactSheet", json!({})).unwrap();
|
||||
let Some(Dialog::ContactSheet { options }) = app.ui.dialog.as_mut() else { panic!("no contact sheet settings") };
|
||||
options.paper = "letter".into();
|
||||
options.landscape = true;
|
||||
let mut h = crate::headless::Headless::new(app, [1000.0, 800.0], 1.0);
|
||||
let dialog = h.app.ui.dialog.take().unwrap();
|
||||
crate::panels::dialogs::confirm_dialog(&mut h.app, &dialog).unwrap();
|
||||
assert_eq!(h.app.pending_picks.len(), 1);
|
||||
let dialog = app.ui.dialog.take().unwrap();
|
||||
crate::panels::dialogs::confirm_dialog(&mut app, &dialog).unwrap();
|
||||
assert_eq!(app.pending_picks.len(), 1);
|
||||
let ctx = egui::Context::default();
|
||||
crate::pick::poll(&mut app, &ctx);
|
||||
assert!(app.pending_picks.is_empty());
|
||||
assert!(app.export.is_some(), "picker did not start export: {:?}", app.ui.toast);
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
loop {
|
||||
// The activity stack lays out text, which egui only allows inside a frame.
|
||||
h.step();
|
||||
if h.app.last_export_result.is_some() {
|
||||
// Poll the real worker without starting unrelated gallery renders each frame.
|
||||
poll(&mut app, &ctx);
|
||||
if app.export.is_none() {
|
||||
break;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "export did not finish");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let result = app.last_export_result.as_ref().unwrap_or_else(|| panic!("export failed: {:?}", app.ui.toast));
|
||||
let req = asked.lock().unwrap();
|
||||
assert_eq!(req[0].kind, PickKind::Save);
|
||||
assert_eq!(req[0].filter.as_ref().unwrap().1, &["pdf"]);
|
||||
@@ -289,7 +303,7 @@ mod contact_sheet_tests {
|
||||
let b = bytes.lock().unwrap();
|
||||
assert!(b.starts_with(b"%PDF-1.4"));
|
||||
assert!(String::from_utf8_lossy(&b).contains("/MediaBox [0 0 792.00 612.00]"));
|
||||
assert_eq!(h.app.last_export_result.as_ref().unwrap()["files"][0]["photos"], 1);
|
||||
assert_eq!(result["files"][0]["photos"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -383,6 +383,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.
|
||||
@@ -476,6 +478,7 @@ impl LightcraftApp {
|
||||
sync_run: None,
|
||||
lightroom: None,
|
||||
lightroom_last: None,
|
||||
lightroom_result: None,
|
||||
shown_library: None,
|
||||
export: None,
|
||||
tasks: Default::default(),
|
||||
|
||||
@@ -243,6 +243,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}"));
|
||||
@@ -280,11 +283,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)?;
|
||||
@@ -348,6 +401,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,
|
||||
@@ -553,6 +607,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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",
|
||||
@@ -345,6 +346,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() {
|
||||
@@ -1199,6 +1214,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
|
||||
@@ -1210,6 +1226,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 {
|
||||
@@ -1227,6 +1244,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",
|
||||
@@ -1552,6 +1570,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
|
||||
|
||||
@@ -956,6 +956,10 @@ 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;
|
||||
if matches!(app.ui.dialog.as_ref(), Some(crate::state::Dialog::LightroomResult { .. })) {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
app.renderer.forget_all();
|
||||
app.ui.compare = None;
|
||||
if let Err(err) = opened {
|
||||
|
||||
@@ -783,6 +783,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()));
|
||||
|
||||
@@ -55,6 +55,26 @@ fn app_in(dir: &std::path::Path) -> LightcraftApp {
|
||||
LightcraftApp::new(session, Services::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_libraries_dismisses_the_previous_lightroom_result() {
|
||||
let dir = temp("switch-lightroom-result");
|
||||
let mut app = app_in(&dir);
|
||||
let result = || crate::state::Dialog::LightroomResult {
|
||||
summary: "Imported into first".into(),
|
||||
library: Some(dir.join("first").to_string_lossy().into_owned()),
|
||||
details: Vec::new(),
|
||||
first: Some(1),
|
||||
};
|
||||
app.ui.dialog = Some(result());
|
||||
app.lightroom_result = Some(result());
|
||||
app.lightroom_last = Some(json!({"imported": 1}));
|
||||
app.run("app.newLibrary", json!({"path": dir.to_string_lossy(), "name": "Second"})).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "the old destination and photo ID must not act on the new library");
|
||||
assert!(app.lightroom_result.is_none());
|
||||
assert!(app.lightroom_last.is_none());
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
fn recent_submenu(app: &LightcraftApp) -> Vec<crate::menubar::MenuNode> {
|
||||
use crate::menubar::MenuNode;
|
||||
let bar = crate::menubar::menu_bar(app);
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
# Gaps: where LightCraft falls short of Lightroom
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** patch (gap 8: Nikon Z 6II / Z 8 / Z f in-file distortion; gaps 10, 22 and 23 listed once again: merges had left two copies of each, and `cargo xtask parity` now fails on a gap listed twice)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** minor (catalog import result, offline Local promotion and reduced copies; gap 8: Nikon Z 6II / Z 8 / Z f in-file distortion; gaps 10, 22 and 23 listed once again: merges had left two copies of each, and `cargo xtask parity` now fails on a gap listed twice)
|
||||
|
||||
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**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). 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); the headroom limit and SDR rendition from `crs` XMP (the HDR switch, `HDREditMode`, is read). HLG AVIF export done; the display's SDR/HDR decision is tested, blocked on eframe's 8-bit surface | 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) |
|
||||
@@ -91,3 +91,6 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
|
||||
| 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-13 | minor | Gap 8 narrows: Nikon Z 6II / Z 8 / Z f NEFs use the camera's in-file distortion correction (`docs/nikon-lens-corrections.md`) |
|
||||
| 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. |
|
||||
| 2026-10-13 | minor | Catalog import review: offline Local records join All Photos, redundant operation/settings copies removed, and result invalidation across library switches covered. Approximate rendering and database/table limits remain. |
|
||||
| 2026-10-14 | patch | LRC-LIB-CATALOG-IMPORT: reconciled with upstream phased progress, cancellation kinds, WAL-only import, translations and current progress tracking. Status and summary counts unchanged. |
|
||||
@@ -15,9 +15,21 @@ 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. Previously browsed Local records join All Photos even when their originals are offline;
|
||||
their promotion is counted once and undone together with the catalog import.
|
||||
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.
|
||||
@@ -32,8 +44,10 @@ retained. Oldest managed archives are retired; legacy `.json` archives remain un
|
||||
`Interop/lightroom-index.json` records imported identities. Source catalogs and originals are
|
||||
never overwritten.
|
||||
|
||||
Native import is bounded to a 1 GiB database/WAL, 16 MiB XMP packets and one million rows per
|
||||
table. The in-tree, read-only Rust SQLite B-tree reader follows SQLite’s public file-format
|
||||
Each native `.lrcat` database and WAL file is independently limited to 1 GiB
|
||||
(1,073,741,824 bytes). This bounds the current whole-file reader; referenced photo sizes and
|
||||
photo counts are not part of that limit. XMP packets are capped at 16 MiB and each table at one million rows.
|
||||
The in-tree, read-only Rust SQLite B-tree reader follows SQLite’s public file-format
|
||||
specification; it introduces no dependency or C runtime. Table payloads are capped at 256 MiB.
|
||||
Optional history, keyword and collection tables that exceed limits are skipped with warnings;
|
||||
required photo tables still report errors. Unrelated unsupported schemas are ignored. A needed
|
||||
@@ -61,6 +75,12 @@ 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.
|
||||
Applying catalog operations collects affected source-cache IDs before consuming the operations,
|
||||
avoiding another full operation copy. Full-strength develop patches reuse their JSON without
|
||||
serializing an unused copy of the current settings.
|
||||
|
||||
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-14 · **Change:** minor (integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (catalog import: persistent result, offline Local promotion and library-switch dismissal; integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **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
|
||||
@@ -692,7 +692,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; each pane says “Rendering…” (widget `compare:rendering:<pane>`) or “This photo can't be opened” (`compare:renderFailed:<pane>`) as the loupe does |
|
||||
| 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 (also a catalog whose tables are all still in its WAL: the main file's schema format 0 is an empty schema), 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 (also WAL-only tables with main schema format 0), 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, dismissed when switching libraries; offline Local records are promoted without probing missing files; 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) |
|
||||
@@ -823,3 +823,6 @@ 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. |
|
||||
| 2026-10-13 | minor | LRC-LIB-CATALOG-IMPORT review: offline Local promotion and result dismissal across library switches covered; redundant operation/settings copies removed. Status and summary counts unchanged. |
|
||||
| 2026-10-14 | patch | LRC-LIB-CATALOG-IMPORT: reconciled with upstream phased progress, cancellation kinds, WAL-only import, translations and current progress tracking. Status and summary counts unchanged. |
|
||||
Reference in new issue
Block a user