Open the catalog on a worker, so a launch is not a page-by-page read
The second thing standing between `android_main` and the first `poll_events`, and the one that grows with the library rather than with the APK. `library_ui::open` called `show_catalog_now`, which called `Catalog::open_verified`. That runs `PRAGMA quick_check`, which reads every page of the database, and then `Catalog::open`, which takes a full SQLite backup of the file before a migration and rewrites its structure afterwards. On a 50,000-image library that is tens of megabytes of I/O on a tablet's flash, and it happened before the window had painted anything — so on Android it was counted against the five seconds the input dispatcher allows, and on the desktop it was a launch that sat on a blank window. `dr_catalog::recovery`'s own module documentation says the check is affordable "at startup, where a failure has a user in front of it who can answer a question". That was the intent and it was not true: there was no interface yet in which to ask. Now there is, because the open happens on a worker and the answer arrives on a channel drained by a timer — the same shape the scan, the thumbnails and the login already use. The gate the synchronous call provided is kept, and is the reason the scan moved with it. `Catalog::open` succeeds on a damaged file whose header survived, so a scan running beside an unanswered recovery question writes ETags and image rows into damaged pages and turns a catalog that had a backup into one where the backup is the only copy left. So the scan now starts from the drain, on the two answers that permit it, and not at all on `Corrupt`. `library-scanning` stays true throughout, which hides the Rescan button and stops the gate being merely advisory. What the user sees while it runs is a third empty state. The grid already refused to conflate "still scanning" with "scanned, found nothing"; "opening the library" is a third answer and it gets its own sentence, because a grid saying "Scanning…" while nothing is on the network is the same kind of lie the other two were separated to avoid. `show_catalog_now` stays, unchanged and blocking, for `recovery_ui`. That call site has the event loop running, has just replaced the file under a `forget_catalog`, and has `recovery-busy` on screen — the same reasoning `recovery_ui::answer` already gives for doing its file copy in place. The part both paths share is now `adopt_catalog`. One consequence worth naming: the cache-usage figure on the settings page was read at startup from a catalog that is no longer open by then. It moves to the page's `on_open` closure, beside the face coverage, which is read there for exactly the same reason — it is only ever looked at while that page is on screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -294,6 +294,15 @@ export component AppWindow inherits Window {
|
||||
callback activity-clear-finished();
|
||||
|
||||
in property <bool> library-scanning: false;
|
||||
/// The catalog on this device is being opened and checked, on a worker,
|
||||
/// before the scan starts (`library_ui::open_catalog_soon`).
|
||||
///
|
||||
/// Separate from `library-scanning` because they are different answers to
|
||||
/// the same question: this one is reading a file that is already here, and
|
||||
/// that one is walking a tree over the network. Both are true at once for
|
||||
/// the first moment of a launch, and only the more specific of them is
|
||||
/// worth putting on an empty grid.
|
||||
in property <bool> library-opening: false;
|
||||
in property <string> library-status: "";
|
||||
in property <string> library-error: "";
|
||||
|
||||
@@ -1517,6 +1526,7 @@ in property <bool> panel-visible: true;
|
||||
cells: root.library-cells;
|
||||
total: root.library-total;
|
||||
scanning: root.library-scanning;
|
||||
opening: root.library-opening;
|
||||
scan-status: root.library-status;
|
||||
scan-error: root.library-error;
|
||||
|
||||
|
||||
@@ -982,6 +982,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <[LibraryCell]> cells;
|
||||
in property <int> total: 0;
|
||||
in property <bool> scanning: false;
|
||||
/// The catalog on this device is being opened and checked before the scan
|
||||
/// starts. Distinct from `scanning` for the reason the empty state below
|
||||
/// gives: they are two different waits and only one of them is the network.
|
||||
in property <bool> opening: false;
|
||||
in property <string> scan-status: "";
|
||||
in property <string> scan-error: "";
|
||||
/// Which folder is being shown. Visible at all times: two similarly-named
|
||||
@@ -2415,9 +2419,20 @@ export component LibraryGrid inherits Rectangle {
|
||||
//
|
||||
// "Still scanning" and "scanned, found nothing" are different answers.
|
||||
// Conflating them is how a working scan looks broken.
|
||||
//
|
||||
// And "still opening" is a third, which is the state a launch is in
|
||||
// for as long as it takes to read and check the catalog on this
|
||||
// device. It used to be spent before the window had painted at all,
|
||||
// where it read as a frozen application — on Android, as an ANR.
|
||||
// Now it is a worker, so it needs a sentence: a grid saying
|
||||
// "Scanning…" while nothing is on the network is the same kind of
|
||||
// lie the two answers below were separated to avoid.
|
||||
if root.total == 0: EmptyState {
|
||||
headline: root.scanning ? "Scanning…" : "No images found";
|
||||
detail: root.scanning ? root.scan-status
|
||||
headline: root.opening
|
||||
? "Opening the library…"
|
||||
: (root.scanning ? "Scanning…" : "No images found");
|
||||
detail: (root.opening || root.scanning)
|
||||
? root.scan-status
|
||||
: "Check the library folder and which formats are ticked.";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user