mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-18 13:17:01 -04:00
LRC-LIB-CATALOG-IMPORT: address Astra review and trim import work
Promote offline Local masters and virtual copies, count each promotion once, and dismiss visible import results when switching libraries. Avoid cloning catalog operations solely for cache invalidation and borrow full-strength develop patches without serializing unused settings. Cover promotion/undo/reimport, cache rollback and undo/redo, and visible result dismissal. Bound the contact-sheet workflow fixture and poll its real worker directly, preserving PDF assertions and the 20-second deadline while reporting worker errors clearly. Generated with Codex.
This commit is contained in:
1 parent
0ec463568b
commit
f730805dc3
10 files changed
+163
-20
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 {
|
||||
|
||||
@@ -494,16 +494,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -701,6 +701,7 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex
|
||||
// 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.
|
||||
@@ -811,6 +812,10 @@ 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.
|
||||
@@ -818,6 +823,10 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, data: CatalogImport, contex
|
||||
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 },
|
||||
@@ -948,6 +957,55 @@ 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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -946,6 +946,9 @@ fn switch_library(app: &mut LightcraftApp, path: &str) -> Result<Value, 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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# Gaps: where LightCraft falls short of Lightroom
|
||||
|
||||
> **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
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (catalog import: offline Local promotion and redundant operation/settings copies addressed; 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
|
||||
@@ -82,3 +82,4 @@ 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-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. |
|
||||
@@ -26,7 +26,9 @@ Original files are referenced in place. Ratings, picks/rejects, color labels, XM
|
||||
hierarchical keywords, regular collections/collection sets, virtual copies and mapped develop
|
||||
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
|
||||
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
|
||||
@@ -42,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
|
||||
@@ -72,6 +76,9 @@ is not evaluated by the importer; LightCraft's Auto control remains available af
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **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
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (catalog import: offline Local promotion and library-switch result invalidation covered; 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; 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-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, 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) |
|
||||
@@ -795,3 +795,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 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. |
|
||||
Reference in new issue
Block a user