From e076f65df8bf8ce9d9e7f9dd4dac216b729e35c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20L?= Date: Sun, 4 Oct 2026 10:32:18 +0200 Subject: [PATCH] Sidebar: count missing files on a worker thread; read browsed folders in smaller batches The Missing count stat()ed every catalog photo on the UI thread, which on a network share (and with browsed NAS photos in the catalog) froze the window for seconds during a folder browse. It now runs in the background and shows the last finished count. Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/import.rs | 3 +- crates/ui-egui/src/panels/left.rs | 49 +++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index a78dbcff5..8bf00e3ec 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -291,7 +291,8 @@ pub fn start(app: &mut LightcraftApp, d: &ImportDialog) -> Result /// Run one batch of the import in progress (called every frame). pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { let Some(task) = app.import.as_mut() else { return }; - let n = task.queue.len().min(BATCH); + // (browsing reads a sidecar per file, slow on a network share: keep each frame short) + let n = task.queue.len().min(if task.browse { 2 } else { BATCH }); let batch: Vec = task.queue.drain(..n).collect(); let mut p = task.params.clone(); p["paths"] = json!(batch); diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 219aaacae..cf89b9dac 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -146,20 +146,51 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { }); } -/// How many library photos have no file (cached in egui memory, refreshed every 5 s). +/// How many library photos have no file. Checking stats every file, which on a network share +/// takes seconds, so it runs on a worker thread: the count shown is the last finished one, +/// refreshed at most every 5 s (or when the catalog changed). fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; + #[derive(Clone, Default)] + struct Job { + n: std::sync::Arc, + running: std::sync::Arc, + at: f64, + rev: u64, + } let id = egui::Id::new("missing-count"); let now = ui.input(|i| i.time); let rev = app.session.catalog.revision; - if let Some((n, at, r)) = ui.data(|d| d.get_temp::<(usize, f64, u64)>(id)) - && now - at < 5.0 - && r == rev - { - return n; + let mut job: Job = ui.data(|d| d.get_temp(id)).unwrap_or(Job { rev: u64::MAX, at: f64::MIN, ..Default::default() }); + let fresh = now - job.at < 5.0 && job.rev == rev; + if !fresh && !job.running.load(Relaxed) { + let paths: Vec = app + .session + .catalog + .photos() + .filter(|p| !p.deleted) + .filter_map(|p| match &p.source { + lightcraft_catalog::Source::File { path } => Some(path.clone()), + _ => None, + }) + .collect(); + job.running.store(true, Relaxed); + job.at = now; + job.rev = rev; + let (n, running, ctx) = (job.n.clone(), job.running.clone(), ui.ctx().clone()); + let work = move || { + let missing = if cfg!(target_arch = "wasm32") { 0 } else { paths.iter().filter(|p| !std::path::Path::new(p).exists()).count() }; + n.store(missing, Relaxed); + running.store(false, Relaxed); + ctx.request_repaint(); + }; + #[cfg(not(target_arch = "wasm32"))] + std::thread::spawn(work); + #[cfg(target_arch = "wasm32")] + work(); + ui.data_mut(|d| d.insert_temp(id, job.clone())); } - let n = lightcraft_engine::cmd::missing::missing(&app.session).len(); - ui.data_mut(|d| d.insert_temp(id, (n, now, rev))); - n + job.n.load(Relaxed) } /// Folders on this computer to browse without adding (Lightroom's Local): Pictures, Desktop,