diff --git a/crates/engine/src/cmd/reload.rs b/crates/engine/src/cmd/reload.rs index 9fc0a590f..0c1cb964c 100644 --- a/crates/engine/src/cmd/reload.rs +++ b/crates/engine/src/cmd/reload.rs @@ -3,6 +3,10 @@ //! the command ran. [`plan`] lists the photos to re-read (no file-system calls), [`probe`] reads //! their files without the session (cancellable, with progress), and [`apply`] commits what came //! of it as one undo step. The command itself (agents, CLI, MCP) does all three at once. +//! +//! Reloads can overlap (the user picks Reload from Disk again while a first one still reads), so +//! each [`plan`] takes a ticket and [`apply`] drops a photo's result when a reload planned later +//! has already been applied to it: an older read never replaces a newer one. use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -18,14 +22,28 @@ use crate::{Result, Session}; pub struct Item { pub id: PhotoId, pub path: String, + /// The [`plan`] it came from, in the order reloads were started (see [`Tickets`]). + pub ticket: u64, } -/// The photos `p` targets (`ids`, else the selection) that have a file. No file-system calls. -pub fn plan(s: &Session, p: &Value) -> Result> { - Ok(s.targets(p)? +/// Which reload each photo last took its file information from, so a reload started earlier but +/// finishing later doesn't put back what its file was before (session memory, not saved). +#[derive(Debug, Default)] +pub struct Tickets { + last: u64, + applied: std::collections::BTreeMap, +} + +/// The photos `p` targets (`ids`, else the selection) that have a file, under a new ticket. No +/// file-system calls. +pub fn plan(s: &mut Session, p: &Value) -> Result> { + let ids = s.targets(p)?; + s.reload_tickets.last = s.reload_tickets.last.saturating_add(1); + let ticket = s.reload_tickets.last; + Ok(ids .into_iter() .filter_map(|id| match s.catalog.photo(id).map(|ph| &ph.source) { - Some(Source::File { path }) => Some(Item { id, path: path.clone() }), + Some(Source::File { path }) => Some(Item { id, path: path.clone(), ticket }), _ => None, }) .collect()) @@ -66,16 +84,19 @@ pub fn probe( /// Commit [`probe`]'s results as one undo step ("Reload Changed Files"): each photo and its virtual /// copies get what their file is now. A file that couldn't be read changes nothing; a photo removed -/// or relinked since it was listed is skipped. Returns the photos that changed. +/// or relinked since it was listed, or already given a newer reload's result, is skipped. Returns +/// the photos that changed. pub fn apply(s: &mut Session, items: Vec, results: Vec>) -> Result> { let mut ops = Vec::new(); let mut reloaded = Vec::new(); for (item, info) in items.into_iter().zip(results) { let Ok(info) = info else { continue }; let current = s.catalog.photo(item.id).is_some_and(|ph| matches!(&ph.source, Source::File { path } if *path == item.path)); - if !current { + let superseded = s.reload_tickets.applied.get(&item.id).is_some_and(|t| *t > item.ticket); + if !current || superseded { continue; } + s.reload_tickets.applied.insert(item.id, item.ticket); crate::cmd::convert::reload_ops(&s.catalog, item.id, &info, &mut ops, &mut reloaded); } for id in &reloaded { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 25e6ef52f..ab6c048f9 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -240,6 +240,8 @@ pub struct Session { /// Set by a command whose change must not rewrite the photo's XMP sidecar even with auto-write on /// (a catalog-only edit of data the sidecar writer does not emit); consumed when the command ends. pub(crate) skip_auto_write: bool, + /// The order of Reload from Disk runs, so an older one finishing late is dropped ([`cmd::reload`]). + pub(crate) reload_tickets: cmd::reload::Tickets, /// Photos whose automatic XMP sidecar write failed (their folder offline, read-only or full), written again by /// [`Session::retry_unsaved_sidecars`] once their file is reachable. pub(crate) unsaved_sidecars: std::collections::BTreeSet, @@ -434,6 +436,7 @@ impl Session { pending_sidecar_repairs: Vec::new(), interaction: None, skip_auto_write: false, + reload_tickets: Default::default(), unsaved_sidecars: Default::default(), denoise: Default::default(), face_models_dir: None, diff --git a/crates/ui-egui/src/reload.rs b/crates/ui-egui/src/reload.rs index 32823cf16..94c9510ab 100644 --- a/crates/ui-egui/src/reload.rs +++ b/crates/ui-egui/src/reload.rs @@ -1,7 +1,8 @@ //! Reload from Disk in the app: the files are read on a worker thread (a raw's decode takes a //! second or more, and a selection of them froze the window), with a row in the activity stack -//! that counts the files and can stop. What came of it is applied back here, as one undo step, -//! unless another library was opened meanwhile. Agents get the same with `wait: true`, which runs +//! that counts the files and can stop. What came of it is applied back here through the session's +//! command wrapper (one undo step, saved, sidecars auto-written), unless another library was opened +//! meanwhile or a reload started later has already been applied to the photo. Agents get the same with `wait: true`, which runs //! the engine's command in place. use std::sync::Arc; @@ -22,7 +23,7 @@ pub fn command(app: &mut LightcraftApp, p: &Value) -> Option items, Err(e) => return Some(Err(format!("photo.reload: {e}"))), }; @@ -47,7 +48,8 @@ pub fn command(app: &mut LightcraftApp, p: &Value) -> Option **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (LRC-PRINT-PAGE: Print Page Options) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (LR-BEHAV-BG-TASKS: Reload from Disk in the background) · **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 @@ -797,7 +797,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | Date | Change | Summary | |---|---|---| -| 2026-10-15 | minor | LR-BEHAV-BG-TASKS: Reload from Disk reads its files on a worker thread with a cancellable activity row instead of freezing the window on a selection of raws; status unchanged (✅) | +| 2026-10-15 | minor | LR-BEHAV-BG-TASKS: Reload from Disk reads its files on a worker thread with a cancellable activity row instead of freezing the window on a selection of raws; its result is committed through the command wrapper (XMP sidecars auto-written as before) and an older reload finishing late no longer overwrites a newer one; status unchanged (✅) | | 2026-10-14 | patch | LR-EXP-TYPE: opaque 8-bit PNG/TIFF/WebP use RGB; transparent exports retain alpha (#1670). Statuses and feature counts unchanged. | | 2026-10-15 | minor | LRC-PRINT-PAGE: Page Options (Page Numbers, Page Info, Crop Marks) on Single Image and Contact Sheet pages, in the Print dialog, `print.set` and templates; still 🟡 (no identity plate, watermark, photo info or Cut Guides) | | 2026-10-15 | minor | LR-IMP-FORMATS: Hasselblad 3FR and FFF show their uncompressed RGB preview (no JPEG in these files) in the grid and loupe until developed, and as the stand-in when a file fails to decode; status unchanged (🟡) |