From 54aee505396f2524a76a3bb805962ca26b0f31ef Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 26 Sep 2026 07:00:55 -0400 Subject: [PATCH] Count duplicate originals once the sweep has dated them A copy becomes a duplicate only once its capture time is read, and on a fresh library that is the sweep, not the scan: the sidebar row stayed hidden until the next launch. The count is refreshed when a sweep that dated anything finishes. Also says on a group left out of the plan that nothing will move, drops an unused method, and names the review's completion callback type for clippy. --- ui/dr-ui/src/duplicates.rs | 59 ++++++++++++++++++++++++--------- ui/dr-ui/src/duplicates_ui.rs | 43 +++++++++++++++++------- ui/dr-ui/src/lib.rs | 4 +-- ui/dr-ui/src/library_ui/sync.rs | 7 ++++ 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/ui/dr-ui/src/duplicates.rs b/ui/dr-ui/src/duplicates.rs index 43a4a59..f6cd07d 100644 --- a/ui/dr-ui/src/duplicates.rs +++ b/ui/dr-ui/src/duplicates.rs @@ -139,7 +139,10 @@ pub fn summarise(review: &[Reviewed]) -> Summary { #[derive(Debug)] pub enum DupMessage { /// One group checked, by its index in the review. - Checked { index: usize, check: Check }, + Checked { + index: usize, + check: Check, + }, /// One group consolidated. Consolidated { index: usize, @@ -147,11 +150,19 @@ pub enum DupMessage { outcome: Outcome, }, /// One group could not be consolidated and was left as it was. - Failed { index: usize, reason: String }, - Progress { done: usize, total: usize }, + Failed { + index: usize, + reason: String, + }, + Progress { + done: usize, + total: usize, + }, /// The job ended. `stopped` when the server went away or the review /// was closed. - Finished { stopped: Option }, + Finished { + stopped: Option, + }, } // ── the edit ───────────────────────────────────────────────────────────── @@ -748,8 +759,7 @@ pub fn spawn_consolidate( ) -> Receiver { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let stopped = - run_consolidate(&conn, &catalog_path, cache_dir, plans, &stop, &tx).err(); + let stopped = run_consolidate(&conn, &catalog_path, cache_dir, plans, &stop, &tx).err(); let _ = tx.send(DupMessage::Finished { stopped }); }); rx @@ -922,7 +932,6 @@ mod tests { // ── end to end, on a folder library ──────────────────────────────── - /// A folder library holding real files, and a catalog describing them. struct Library { dir: PathBuf, @@ -1092,7 +1101,11 @@ mod tests { assert_eq!(review.len(), 3); lib.check(&mut review); - assert!(matches!(review[0].check, Check::Same(_)), "{:?}", review[0].check); + assert!( + matches!(review[0].check, Check::Same(_)), + "{:?}", + review[0].check + ); assert_eq!( review[1].check, Check::Skip("The files differ in their first or last megabyte".into()) @@ -1114,7 +1127,11 @@ mod tests { ); // What was read is kept: a second review needs no bytes. - assert!(lib.review()[0].group.copies.iter().all(|c| c.probe.is_some())); + assert!(lib.review()[0] + .group + .copies + .iter() + .all(|c| c.probe.is_some())); let (done, failed) = lib.consolidate(&review); assert_eq!((done, failed), (1, Vec::::new())); @@ -1137,10 +1154,9 @@ mod tests { assert_eq!(trashed(&cat), 2); // The edit the backup copy carried now lives beside the survivor. - let carried = std::fs::read_to_string( - lib.lib.join(sidecar_path("2023/2023-06-24/_MG_4623.CR2")), - ) - .unwrap(); + let carried = + std::fs::read_to_string(lib.lib.join(sidecar_path("2023/2023-06-24/_MG_4623.CR2"))) + .unwrap(); let carried = Sidecar::parse(&carried).unwrap(); assert_eq!( edit_key(&carried), @@ -1165,7 +1181,10 @@ mod tests { assert_eq!(restored, 2); assert!(lib.exists("2023/bck/_MG_4623.CR2")); assert!(lib.exists("alps trip/Raw/20230628_0642.CR2")); - assert_eq!(std::fs::read(lib.lib.join("2023/bck/_MG_4623.CR2")).unwrap(), frame); + assert_eq!( + std::fs::read(lib.lib.join("2023/bck/_MG_4623.CR2")).unwrap(), + frame + ); assert_eq!(trashed(&lib.catalog()), 0); } @@ -1193,7 +1212,11 @@ mod tests { assert_eq!(done, 0); assert_eq!(failed.len(), 1, "{failed:?}"); - for path in ["2023/bck/IMG_0001.CR2", "2023/a/IMG_0001.CR2", "2023/b/IMG_0001-2.CR2"] { + for path in [ + "2023/bck/IMG_0001.CR2", + "2023/a/IMG_0001.CR2", + "2023/b/IMG_0001-2.CR2", + ] { assert!(lib.exists(path), "{path} is back where it was"); } assert!(!lib.exists(".darkroom-trash/1-IMG_0001.CR2")); @@ -1217,7 +1240,11 @@ mod tests { let mut review = lib.review(); lib.check(&mut review); - assert!(matches!(review[0].check, Check::Same(_)), "{:?}", review[0].check); + assert!( + matches!(review[0].check, Check::Same(_)), + "{:?}", + review[0].check + ); let (done, failed) = lib.consolidate(&review); assert_eq!((done, failed), (1, Vec::::new())); assert_eq!(trashed(&lib.catalog()), 1); diff --git a/ui/dr-ui/src/duplicates_ui.rs b/ui/dr-ui/src/duplicates_ui.rs index 7376003..91afba3 100644 --- a/ui/dr-ui/src/duplicates_ui.rs +++ b/ui/dr-ui/src/duplicates_ui.rs @@ -29,6 +29,10 @@ use crate::{AppWindow, DuplicateCopy, DuplicateRow, Duplicates, Library, Page}; /// of the library. pub const PAGE: usize = 30; +/// What runs when a consolidation ends: the grid, the sidebar and the trash +/// brought up to date. +pub type After = Rc; + /// How a group ended in this session. #[derive(Debug, Clone)] enum Ended { @@ -79,9 +83,7 @@ impl DuplicatesController { /// can change the answer. pub fn refresh_count(window: &AppWindow, catalog: &Catalog) { let (groups, _) = dr_catalog::duplicates::count(catalog.connection()).unwrap_or((0, 0)); - window - .global::() - .set_group_count(groups as i32); + window.global::().set_group_count(groups as i32); } /// Wire the page. `after` runs when a consolidation ends, to bring the grid, @@ -90,7 +92,7 @@ pub fn wire( window: &AppWindow, ctl: Rc, library: Rc, - after: Rc, + after: After, ) { let g = window.global::(); @@ -130,8 +132,12 @@ pub fn wire( }); } { - let (weak, ctl, library, after) = - (window.as_weak(), ctl.clone(), library.clone(), after.clone()); + let (weak, ctl, library, after) = ( + window.as_weak(), + ctl.clone(), + library.clone(), + after.clone(), + ); g.on_confirm(move || { let Some(w) = weak.upgrade() else { return }; start_consolidate(&w, &ctl, &library, after.clone()); @@ -166,7 +172,9 @@ pub fn wire( let index = ctl.page.get() * PAGE + row.max(0) as usize; { let mut review = ctl.review.borrow_mut(); - let Some(r) = review.get_mut(index) else { return }; + let Some(r) = review.get_mut(index) else { + return; + }; if (copy as usize) < r.group.copies.len() { r.survivor = copy as usize; } @@ -372,6 +380,10 @@ fn show(window: &AppWindow, ctl: &Rc, library: &Rc (format!("Left as it was: {s}"), true), (None, Check::Unchecked) => ("Not checked yet".to_string(), false), (None, Check::Skip(why)) => (format!("Skipped: {why}"), true), + (None, Check::Same(_)) if !r.include => ( + "The same file · left out: nothing will move".to_string(), + false, + ), (None, Check::Same(edits)) => ( if edits.edited.iter().any(|e| *e) { "The same file, with one edit between them".to_string() @@ -533,7 +545,11 @@ pub fn describe(o: &Outcome, carried_edit: Option<&str>) -> String { out } -fn start_check(window: &AppWindow, ctl: &Rc, library: &Rc) { +fn start_check( + window: &AppWindow, + ctl: &Rc, + library: &Rc, +) { if ctl.running.get() { return; } @@ -566,7 +582,10 @@ fn start_check(window: &AppWindow, ctl: &Rc, library: &Rc< ); let job = ctl.activity.begin( crate::activity::Kind::Download, - format!("Checking {}", plural(count, "duplicate group", "duplicate groups")), + format!( + "Checking {}", + plural(count, "duplicate group", "duplicate groups") + ), ); ctl.consolidating.set(false); ctl.counts.set((0, 0, 0)); @@ -577,7 +596,7 @@ fn start_consolidate( window: &AppWindow, ctl: &Rc, library: &Rc, - after: Rc, + after: After, ) { if ctl.running.get() { return; @@ -643,7 +662,7 @@ fn drain( library: &Rc, rx: Receiver, job: Activity, - after: Option>, + after: Option, ) { ctl.running.set(true); let g = window.global::(); @@ -749,7 +768,7 @@ fn finish( library: &Rc, job: &Activity, stopped: Option, - after: Option<&Rc>, + after: Option<&After>, ) { ctl.running.set(false); let g = w.global::(); diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 7d421e0..87ee9bf 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -26,12 +26,12 @@ mod bursts; mod collections_ui; #[cfg(test)] mod decoder_seam; -mod duplicates; -mod duplicates_ui; mod derived_sync; mod develop; mod develop_ui; mod display_ui; +mod duplicates; +mod duplicates_ui; mod export; pub mod faces; pub use library::render_native; diff --git a/ui/dr-ui/src/library_ui/sync.rs b/ui/dr-ui/src/library_ui/sync.rs index 349dfc7..6a6d0bc 100644 --- a/ui/dr-ui/src/library_ui/sync.rs +++ b/ui/dr-ui/src/library_ui/sync.rs @@ -357,6 +357,13 @@ pub(super) fn start_sweep(window: &AppWindow, ctl: &Rc) { let borrow = ctl_cb.catalog.borrow(); if let Some(catalog) = borrow.as_ref() { refresh_timeline(&w, catalog, &ctl_cb); + // A capture time is half of what makes two + // files one photograph, and the sweep is + // what reads it: a copy is a duplicate only + // once it is dated. + if dated > 0 { + crate::duplicates_ui::refresh_count(&w, catalog); + } } } // TRACES: NFR-R2