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.
This commit is contained in:
+43
-16
@@ -139,7 +139,10 @@ pub fn summarise(review: &[Reviewed]) -> Summary {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DupMessage {
|
pub enum DupMessage {
|
||||||
/// One group checked, by its index in the review.
|
/// One group checked, by its index in the review.
|
||||||
Checked { index: usize, check: Check },
|
Checked {
|
||||||
|
index: usize,
|
||||||
|
check: Check,
|
||||||
|
},
|
||||||
/// One group consolidated.
|
/// One group consolidated.
|
||||||
Consolidated {
|
Consolidated {
|
||||||
index: usize,
|
index: usize,
|
||||||
@@ -147,11 +150,19 @@ pub enum DupMessage {
|
|||||||
outcome: Outcome,
|
outcome: Outcome,
|
||||||
},
|
},
|
||||||
/// One group could not be consolidated and was left as it was.
|
/// One group could not be consolidated and was left as it was.
|
||||||
Failed { index: usize, reason: String },
|
Failed {
|
||||||
Progress { done: usize, total: usize },
|
index: usize,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
Progress {
|
||||||
|
done: usize,
|
||||||
|
total: usize,
|
||||||
|
},
|
||||||
/// The job ended. `stopped` when the server went away or the review
|
/// The job ended. `stopped` when the server went away or the review
|
||||||
/// was closed.
|
/// was closed.
|
||||||
Finished { stopped: Option<String> },
|
Finished {
|
||||||
|
stopped: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── the edit ─────────────────────────────────────────────────────────────
|
// ── the edit ─────────────────────────────────────────────────────────────
|
||||||
@@ -748,8 +759,7 @@ pub fn spawn_consolidate(
|
|||||||
) -> Receiver<DupMessage> {
|
) -> Receiver<DupMessage> {
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let stopped =
|
let stopped = run_consolidate(&conn, &catalog_path, cache_dir, plans, &stop, &tx).err();
|
||||||
run_consolidate(&conn, &catalog_path, cache_dir, plans, &stop, &tx).err();
|
|
||||||
let _ = tx.send(DupMessage::Finished { stopped });
|
let _ = tx.send(DupMessage::Finished { stopped });
|
||||||
});
|
});
|
||||||
rx
|
rx
|
||||||
@@ -922,7 +932,6 @@ mod tests {
|
|||||||
|
|
||||||
// ── end to end, on a folder library ────────────────────────────────
|
// ── end to end, on a folder library ────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
/// A folder library holding real files, and a catalog describing them.
|
/// A folder library holding real files, and a catalog describing them.
|
||||||
struct Library {
|
struct Library {
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
@@ -1092,7 +1101,11 @@ mod tests {
|
|||||||
assert_eq!(review.len(), 3);
|
assert_eq!(review.len(), 3);
|
||||||
lib.check(&mut 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
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
review[1].check,
|
review[1].check,
|
||||||
Check::Skip("The files differ in their first or last megabyte".into())
|
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.
|
// 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);
|
let (done, failed) = lib.consolidate(&review);
|
||||||
assert_eq!((done, failed), (1, Vec::<String>::new()));
|
assert_eq!((done, failed), (1, Vec::<String>::new()));
|
||||||
@@ -1137,10 +1154,9 @@ mod tests {
|
|||||||
assert_eq!(trashed(&cat), 2);
|
assert_eq!(trashed(&cat), 2);
|
||||||
|
|
||||||
// The edit the backup copy carried now lives beside the survivor.
|
// The edit the backup copy carried now lives beside the survivor.
|
||||||
let carried = std::fs::read_to_string(
|
let carried =
|
||||||
lib.lib.join(sidecar_path("2023/2023-06-24/_MG_4623.CR2")),
|
std::fs::read_to_string(lib.lib.join(sidecar_path("2023/2023-06-24/_MG_4623.CR2")))
|
||||||
)
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
let carried = Sidecar::parse(&carried).unwrap();
|
let carried = Sidecar::parse(&carried).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edit_key(&carried),
|
edit_key(&carried),
|
||||||
@@ -1165,7 +1181,10 @@ mod tests {
|
|||||||
assert_eq!(restored, 2);
|
assert_eq!(restored, 2);
|
||||||
assert!(lib.exists("2023/bck/_MG_4623.CR2"));
|
assert!(lib.exists("2023/bck/_MG_4623.CR2"));
|
||||||
assert!(lib.exists("alps trip/Raw/20230628_0642.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);
|
assert_eq!(trashed(&lib.catalog()), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,7 +1212,11 @@ mod tests {
|
|||||||
assert_eq!(done, 0);
|
assert_eq!(done, 0);
|
||||||
assert_eq!(failed.len(), 1, "{failed:?}");
|
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(path), "{path} is back where it was");
|
||||||
}
|
}
|
||||||
assert!(!lib.exists(".darkroom-trash/1-IMG_0001.CR2"));
|
assert!(!lib.exists(".darkroom-trash/1-IMG_0001.CR2"));
|
||||||
@@ -1217,7 +1240,11 @@ mod tests {
|
|||||||
|
|
||||||
let mut review = lib.review();
|
let mut review = lib.review();
|
||||||
lib.check(&mut 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);
|
let (done, failed) = lib.consolidate(&review);
|
||||||
assert_eq!((done, failed), (1, Vec::<String>::new()));
|
assert_eq!((done, failed), (1, Vec::<String>::new()));
|
||||||
assert_eq!(trashed(&lib.catalog()), 1);
|
assert_eq!(trashed(&lib.catalog()), 1);
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ use crate::{AppWindow, DuplicateCopy, DuplicateRow, Duplicates, Library, Page};
|
|||||||
/// of the library.
|
/// of the library.
|
||||||
pub const PAGE: usize = 30;
|
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<dyn Fn(&AppWindow)>;
|
||||||
|
|
||||||
/// How a group ended in this session.
|
/// How a group ended in this session.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Ended {
|
enum Ended {
|
||||||
@@ -79,9 +83,7 @@ impl DuplicatesController {
|
|||||||
/// can change the answer.
|
/// can change the answer.
|
||||||
pub fn refresh_count(window: &AppWindow, catalog: &Catalog) {
|
pub fn refresh_count(window: &AppWindow, catalog: &Catalog) {
|
||||||
let (groups, _) = dr_catalog::duplicates::count(catalog.connection()).unwrap_or((0, 0));
|
let (groups, _) = dr_catalog::duplicates::count(catalog.connection()).unwrap_or((0, 0));
|
||||||
window
|
window.global::<Duplicates>().set_group_count(groups as i32);
|
||||||
.global::<Duplicates>()
|
|
||||||
.set_group_count(groups as i32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wire the page. `after` runs when a consolidation ends, to bring the grid,
|
/// Wire the page. `after` runs when a consolidation ends, to bring the grid,
|
||||||
@@ -90,7 +92,7 @@ pub fn wire(
|
|||||||
window: &AppWindow,
|
window: &AppWindow,
|
||||||
ctl: Rc<DuplicatesController>,
|
ctl: Rc<DuplicatesController>,
|
||||||
library: Rc<LibraryController>,
|
library: Rc<LibraryController>,
|
||||||
after: Rc<dyn Fn(&AppWindow)>,
|
after: After,
|
||||||
) {
|
) {
|
||||||
let g = window.global::<Duplicates>();
|
let g = window.global::<Duplicates>();
|
||||||
|
|
||||||
@@ -130,8 +132,12 @@ pub fn wire(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let (weak, ctl, library, after) =
|
let (weak, ctl, library, after) = (
|
||||||
(window.as_weak(), ctl.clone(), library.clone(), after.clone());
|
window.as_weak(),
|
||||||
|
ctl.clone(),
|
||||||
|
library.clone(),
|
||||||
|
after.clone(),
|
||||||
|
);
|
||||||
g.on_confirm(move || {
|
g.on_confirm(move || {
|
||||||
let Some(w) = weak.upgrade() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
start_consolidate(&w, &ctl, &library, after.clone());
|
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 index = ctl.page.get() * PAGE + row.max(0) as usize;
|
||||||
{
|
{
|
||||||
let mut review = ctl.review.borrow_mut();
|
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() {
|
if (copy as usize) < r.group.copies.len() {
|
||||||
r.survivor = copy as usize;
|
r.survivor = copy as usize;
|
||||||
}
|
}
|
||||||
@@ -372,6 +380,10 @@ fn show(window: &AppWindow, ctl: &Rc<DuplicatesController>, library: &Rc<Library
|
|||||||
(Some(Ended::Failed(s)), _) => (format!("Left as it was: {s}"), true),
|
(Some(Ended::Failed(s)), _) => (format!("Left as it was: {s}"), true),
|
||||||
(None, Check::Unchecked) => ("Not checked yet".to_string(), false),
|
(None, Check::Unchecked) => ("Not checked yet".to_string(), false),
|
||||||
(None, Check::Skip(why)) => (format!("Skipped: {why}"), true),
|
(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)) => (
|
(None, Check::Same(edits)) => (
|
||||||
if edits.edited.iter().any(|e| *e) {
|
if edits.edited.iter().any(|e| *e) {
|
||||||
"The same file, with one edit between them".to_string()
|
"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
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_check(window: &AppWindow, ctl: &Rc<DuplicatesController>, library: &Rc<LibraryController>) {
|
fn start_check(
|
||||||
|
window: &AppWindow,
|
||||||
|
ctl: &Rc<DuplicatesController>,
|
||||||
|
library: &Rc<LibraryController>,
|
||||||
|
) {
|
||||||
if ctl.running.get() {
|
if ctl.running.get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,7 +582,10 @@ fn start_check(window: &AppWindow, ctl: &Rc<DuplicatesController>, library: &Rc<
|
|||||||
);
|
);
|
||||||
let job = ctl.activity.begin(
|
let job = ctl.activity.begin(
|
||||||
crate::activity::Kind::Download,
|
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.consolidating.set(false);
|
||||||
ctl.counts.set((0, 0, 0));
|
ctl.counts.set((0, 0, 0));
|
||||||
@@ -577,7 +596,7 @@ fn start_consolidate(
|
|||||||
window: &AppWindow,
|
window: &AppWindow,
|
||||||
ctl: &Rc<DuplicatesController>,
|
ctl: &Rc<DuplicatesController>,
|
||||||
library: &Rc<LibraryController>,
|
library: &Rc<LibraryController>,
|
||||||
after: Rc<dyn Fn(&AppWindow)>,
|
after: After,
|
||||||
) {
|
) {
|
||||||
if ctl.running.get() {
|
if ctl.running.get() {
|
||||||
return;
|
return;
|
||||||
@@ -643,7 +662,7 @@ fn drain(
|
|||||||
library: &Rc<LibraryController>,
|
library: &Rc<LibraryController>,
|
||||||
rx: Receiver<DupMessage>,
|
rx: Receiver<DupMessage>,
|
||||||
job: Activity,
|
job: Activity,
|
||||||
after: Option<Rc<dyn Fn(&AppWindow)>>,
|
after: Option<After>,
|
||||||
) {
|
) {
|
||||||
ctl.running.set(true);
|
ctl.running.set(true);
|
||||||
let g = window.global::<Duplicates>();
|
let g = window.global::<Duplicates>();
|
||||||
@@ -749,7 +768,7 @@ fn finish(
|
|||||||
library: &Rc<LibraryController>,
|
library: &Rc<LibraryController>,
|
||||||
job: &Activity,
|
job: &Activity,
|
||||||
stopped: Option<String>,
|
stopped: Option<String>,
|
||||||
after: Option<&Rc<dyn Fn(&AppWindow)>>,
|
after: Option<&After>,
|
||||||
) {
|
) {
|
||||||
ctl.running.set(false);
|
ctl.running.set(false);
|
||||||
let g = w.global::<Duplicates>();
|
let g = w.global::<Duplicates>();
|
||||||
|
|||||||
+2
-2
@@ -26,12 +26,12 @@ mod bursts;
|
|||||||
mod collections_ui;
|
mod collections_ui;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod decoder_seam;
|
mod decoder_seam;
|
||||||
mod duplicates;
|
|
||||||
mod duplicates_ui;
|
|
||||||
mod derived_sync;
|
mod derived_sync;
|
||||||
mod develop;
|
mod develop;
|
||||||
mod develop_ui;
|
mod develop_ui;
|
||||||
mod display_ui;
|
mod display_ui;
|
||||||
|
mod duplicates;
|
||||||
|
mod duplicates_ui;
|
||||||
mod export;
|
mod export;
|
||||||
pub mod faces;
|
pub mod faces;
|
||||||
pub use library::render_native;
|
pub use library::render_native;
|
||||||
|
|||||||
@@ -357,6 +357,13 @@ pub(super) fn start_sweep(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
let borrow = ctl_cb.catalog.borrow();
|
let borrow = ctl_cb.catalog.borrow();
|
||||||
if let Some(catalog) = borrow.as_ref() {
|
if let Some(catalog) = borrow.as_ref() {
|
||||||
refresh_timeline(&w, catalog, &ctl_cb);
|
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
|
// TRACES: NFR-R2
|
||||||
|
|||||||
Reference in New Issue
Block a user