Import: a deferred review belongs to the library that scanned it

A scan can finish while Settings is open, and Settings can then open
another library. The review records the library generation it was made
in: a review waiting for a dialog to close is dropped once its library
is gone, and confirming one in another library imports nothing.
This commit is contained in:
aiongg
2026-10-24 03:34:40 +00:00
parent 496291276c
commit 9c9f87e589
2 changed files with 48 additions and 1 deletions
+21 -1
View File
@@ -91,6 +91,10 @@ pub struct ImportDialog {
/// The candidate clicked last: where a Shift-click range starts ([`ImportDialog::click`]).
#[serde(skip)]
pub last_clicked: Option<usize>,
/// The library the scan was made in ([`lightcraft_engine::Session::library_generation`]): its duplicate
/// decisions and album ids belong to that catalog, so the review is not shown or confirmed in another.
#[serde(skip)]
pub library: Option<u64>,
}
/// A candidate's file type, as the review groups them: its format, or its extension when the
@@ -114,6 +118,11 @@ impl ImportDialog {
let checked = candidates.iter().map(|c| c.duplicate.is_none() && c.error.is_none()).collect();
ImportDialog { candidates, checked, on_deleted: "restore".into(), ..Default::default() }
}
/// Whether the library the scan was made in is still the open one (a review that names none counts).
fn in_library(&self, app: &LightcraftApp) -> bool {
self.library.is_none_or(|g| g == app.session.library_generation())
}
/// Mark the candidates whose photo is in Recently Deleted. They are checked when the choice
/// for them (`on_deleted`) imports them — by default it restores them, so a photo deleted by
/// mistake comes back by importing its file again (issue #706).
@@ -627,16 +636,24 @@ pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) {
// only an album the review offers (it may have been deleted while the scan ran)
d.album = task.album.filter(|id| app.session.catalog.album(lightcraft_catalog::AlbumId(*id)).is_some_and(|a| !a.folder && !a.is_smart()));
d.sources = task.sources;
d.library = Some(app.session.library_generation());
app.review_pending = Some(crate::state::Dialog::Import { opts: Box::new(d) });
show_pending_review(app, ctx);
}
/// Open the review of a finished scan once no other dialog is open (a face model's licence, Settings…
/// opened while the scan ran stays), checked again shortly while it waits.
/// opened while the scan ran stays), checked again shortly while it waits. A review whose library was
/// closed meanwhile (Settings can open another) is dropped.
fn show_pending_review(app: &mut LightcraftApp, ctx: &egui::Context) {
if app.review_pending.is_none() {
return;
}
if let Some(crate::state::Dialog::Import { opts }) = &app.review_pending
&& !opts.in_library(app)
{
app.review_pending = None;
return;
}
if app.ui.dialog.is_none() {
app.ui.dialog = app.review_pending.take();
} else {
@@ -676,6 +693,9 @@ impl ScanTask {
/// Start importing the dialog's checked files (the dialog's OK / `ui.dialog.confirm`).
pub fn start(app: &mut LightcraftApp, d: &ImportDialog) -> Result<Value, String> {
busy_synchronizing(app)?;
if !d.in_library(app) {
return Err("another library was opened since the photos were scanned: nothing was imported".into());
}
let queue = d.selected_paths();
if queue.is_empty() {
return Err("no photos selected".into());
+27
View File
@@ -166,6 +166,33 @@ fn a_finished_scan_waits_for_the_open_dialog_before_opening_the_review() {
assert_eq!(opts.candidates.len(), 2, "the scan's photos were kept");
}
/// A scan that finishes behind Settings, which then opens another library: the review of the first
/// library's scan doesn't open over the second one once Settings closes, and nothing is imported into it.
#[test]
fn a_waiting_review_is_dropped_when_another_library_opens() {
let dir = Scratch::new("switch");
let mut h = app(&dir);
crate::import::open(&mut h.app, vec![dir.path("trip")]).unwrap();
h.app.ui.dialog = Some(Dialog::Settings { tab: "general".into() });
assert!(h.step_until(SETTLE, |h| h.app.scan.is_none()), "the scan ends");
h.step();
let stale = h.app.review_pending.clone().expect("the review waits behind Settings");
h.app.run("app.openLibrary", json!({"path": dir.path("other")})).unwrap();
h.app.ui.dialog = None;
for _ in 0..10 {
h.step();
}
assert!(h.app.ui.dialog.is_none(), "no review from the other library opens");
assert!(h.app.review_pending.is_none());
// a review confirmed after the switch (an agent holding it) imports nothing either
assert!(crate::panels::dialogs::confirm_dialog(&mut h.app, &stale).is_err());
for _ in 0..10 {
h.step();
}
assert!(h.app.import.is_none(), "nothing is imported");
assert_eq!(photos(&h), 0);
}
/// A drop while an import runs would open a review whose OK replaces the running import: it is
/// refused instead, and the card says so.
#[test]