LR-IMP-ADD-DIALOG: folder scan follow-ups (Cancel, busy guard, inspect)

- Cancel drops the scan at once (the worker stops at its next file) and
  has a widget id (button:scanCancel) for agents.
- Add from Device while a scan runs no longer flips the running scan to
  copy-into-library.
- ui.inspect reports the scan's {done, total}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 18:20:07 -04:00
parent fc66026ef0
commit 8146a11cec
4 changed files with 57 additions and 2 deletions

View File

@ -81,6 +81,7 @@ pub fn inspect(app: &LightcraftApp, ctx: &egui::Context) -> Value {
"loupe": app.loupe_shown.map(|(p, src)| json!({"photo": p.0, "source": src, "pending": app.renderer.is_pending(crate::render::Slot::Main)})),
"hoverPreview": app.hover_preview.as_ref().map(|h| h.label.clone()),
"status": app.ui.status,
"scan": app.scan.as_ref().map(crate::import::ScanTask::status),
"export": {"running": app.export.as_ref().map(crate::export_task::ExportTask::status), "last": app.last_export_result},
"memory": memory(app),
})

View File

@ -829,6 +829,46 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// Cancel closes the scan at once and opens no review; Add from Device while a scan runs is
/// refused and leaves the running scan's options alone.
#[test]
fn folder_scan_cancel_and_busy() {
let mut h = demo([1200.0, 900.0]);
let t = Duration::from_secs(10);
let dir = std::env::temp_dir().join(format!("lc-scancancel-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let img = lightcraft_raster::Rgba8::from_fn(8, 8, |x, y| [(x * 9) as u8, (y * 12) as u8, 80, 255]);
let png = lightcraft_engine::export::encode_image(
&img,
&lightcraft_engine::export::ExportOptions { format: lightcraft_engine::export::ExportFormat::Png, ..Default::default() },
)
.unwrap();
std::fs::write(dir.join("a.png"), &png).unwrap();
// a scan that can't finish until the test lets it: the probe waits on a flag
let gate = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let g = gate.clone();
h.app.session.media.file_probe = Some(std::sync::Arc::new(move |_: &str| {
while !g.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(5));
}
Ok(lightcraft_engine::media::ProbeInfo { format: "PNG".into(), ..Default::default() })
}));
let r = h.request("engine.execute", json!({"command": "file.addFolder", "params": {"path": dir.to_string_lossy()}}), t);
assert_eq!(r["result"]["scanning"], true, "{r}");
let r = h.request("engine.execute", json!({"command": "file.addFromDevice", "params": {"path": dir.to_string_lossy()}}), t);
assert_ne!(r["ok"], true, "a second scan is refused: {r}");
assert!(!h.app.scan.as_ref().unwrap().copy, "the running scan keeps its options");
let r = h.request("ui.inspect", json!({}), t);
assert!(r["result"]["scan"].is_object(), "{r}");
let r = h.request("ui.clickWidget", json!({"id": "button:scanCancel"}), t);
assert_eq!(r["ok"], true, "{r}");
assert!(h.app.scan.is_none(), "Cancel closes the scan at once");
gate.store(true, std::sync::atomic::Ordering::Relaxed);
h.settle(SETTLE);
assert!(h.app.ui.dialog.is_none(), "a cancelled scan opens no review");
let _ = std::fs::remove_dir_all(&dir);
}
/// ⇧⌘V opens Paste Selected Settings (prefilled with the copied groups); unchecking a group
/// leaves it alone. ⌘F puts typing into the search field.
#[test]

View File

@ -150,11 +150,22 @@ pub fn scan_progress(app: &mut LightcraftApp, ctx: &egui::Context) {
|ui| {
ui.label(egui::RichText::new(text).color(t.text));
ui.add(egui::ProgressBar::new(done as f32 / total.max(1) as f32).desired_width(320.0));
cancel = ui.button("Cancel").clicked();
let r = ui.button("Cancel");
register(ui.ctx(), "button:scanCancel", r.rect);
cancel = r.clicked();
},
);
if cancel {
// the worker stops at its next file; don't wait for it (a NAS read can take a while)
task.progress.cancel.store(true, Ordering::Relaxed);
app.scan = None;
}
}
impl ScanTask {
/// `{done, total}` for `ui.inspect` (total is 0 while the folders are still being listed).
pub fn status(&self) -> Value {
json!({"done": self.progress.done.load(Ordering::Relaxed), "total": self.progress.total.load(Ordering::Relaxed)})
}
}

View File

@ -894,7 +894,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
},
};
let r = crate::import::open(app, vec![path]);
if let Some(t) = &mut app.scan {
// only the scan just started (an error means another scan is running)
if r.is_ok()
&& let Some(t) = &mut app.scan
{
t.copy = true;
}
r