Show the library on launch instead of scanning first

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 —
exactly the state offline mode already leans on when the server cannot be
reached.

Every launch that *could* reach the server threw that away. The catalog
handle was only opened when the scan reported Done, so the grid sat on
"Scanning…" over an empty EmptyState for as long as a recursive WebDAV
walk of the whole tree takes. On a real library that walk is essentially
the whole startup time, and it was spent hiding a grid that was ready
before it began.

The catalog is now opened and the first window loaded before the scan
thread is spawned — before, so the schema migration cannot race the
worker opening the same file, and so the first thumbnail batch is already
in flight while the walk runs. The scan still replaces all of it the
moment it lands; it just no longer gates the first paint on the network.

A first run has nothing to open, which stays silent: `Catalog::open`
creates the file, the grid reads an empty catalog, and the empty state
goes on saying "Scanning…" — which is true, and which an error here would
contradict.
This commit is contained in:
2026-08-21 20:35:54 +02:00
parent 80f1a210cc
commit 1980fda737
+73 -4
View File
@@ -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<LibraryController>,
catalog_path: &std::path::Path,
coll_ctl: &Rc<crate::collections_ui::CollectionsController>,
) {
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 {