diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index 8821fac..806034d 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -706,6 +706,10 @@ pub fn open( path.display() ); + // Before the scan, not after it: the grid can be filled from disk now and + // the scan is only ever going to add to it. + show_catalog_now(window, &ctl, &path, &coll_ctl); + let rx = library::spawn_scan( creds, session.user_id.clone(), @@ -822,14 +826,32 @@ fn drain_scan( job.finish(status.clone()); w.set_library_status(status.into()); - match Catalog::open(&catalog_path) { - Ok(cat) => { + // Usually already open — `show_catalog_now` opened it + // before this scan started, so the grid has been + // showing the previous run's catalog all along and the + // rows this scan found are new rows in that same file. + // Only a first run, where there was nothing to open, + // reaches the second arm. + let opened = if ctl.catalog.borrow().is_some() { + Ok(()) + } else { + Catalog::open(&catalog_path).map(|cat| { + *ctl.catalog.borrow_mut() = Some(cat); + }) + }; + + match opened { + Ok(()) => { // The sidebar is built before the grid: the // grid's badges read collection membership, and // the tree is where the catalog handle first // becomes available to it. - crate::collections_ui::refresh_tree(&w, &coll_ctl, &cat); - *ctl.catalog.borrow_mut() = Some(cat); + { + let borrow = ctl.catalog.borrow(); + if let Some(cat) = borrow.as_ref() { + crate::collections_ui::refresh_tree(&w, &coll_ctl, cat); + } + } load_window(&w, ctl); // Everything the grid did not touch: the rest // of the library gets a thumbnail and a date, @@ -1586,6 +1608,53 @@ fn open_catalog_for_offline( } } +/// Show what the catalog already holds, without waiting for the scan. +/// +/// # Why a launch should not be a scan +/// +/// A launch does not have to discover the library. The catalog from the last +/// run is on disk, complete, with its thumbnails in the shards beside it — +/// which is precisely the state [`open_catalog_for_offline`] already relies on +/// when the server cannot be reached. Every launch that *could* reach the +/// server threw that away and sat on "Scanning…" over an empty grid for as long +/// as a recursive WebDAV walk of the whole tree takes. On a real library, and +/// especially on a phone's connection, that walk is the entire startup time, +/// spent hiding a grid that was ready before it began. +/// +/// The scan still runs and still replaces this the moment it lands. What it no +/// longer does is gate the first paint on the network. +/// +/// Silent when there is no catalog yet: that is a genuine first run, the empty +/// state already says "Scanning…", and an error here would contradict a scan +/// that is working perfectly. `Catalog::open` creates the file in that case, so +/// what the grid reads is an empty catalog rather than a failure. +fn show_catalog_now( + window: &AppWindow, + ctl: &Rc, + catalog_path: &std::path::Path, + coll_ctl: &Rc, +) { + if ctl.catalog.borrow().is_some() { + return; + } + + let cat = match Catalog::open(catalog_path) { + Ok(cat) => cat, + Err(e) => { + // Not surfaced: the scan is the thing that has to work, and it is + // still running. If it fails too, it reports for both of them. + log::info!("no catalog to show before the scan: {e}"); + return; + } + }; + + // The sidebar before the grid, because the grid's badges read collection + // membership — the same order the scan's completion uses. + crate::collections_ui::refresh_tree(window, coll_ctl, &cat); + *ctl.catalog.borrow_mut() = Some(cat); + load_window(window, ctl); +} + /// What one cell of the outgoing model is worth keeping. #[derive(Clone)] struct Held {