Thumbnail the whole library on request, and send the shards up

The grid fetches a preview only for cells that are actually browsed, which is
the right posture over a link that must not be saturated to show one screen
(FR-NC-3). The cost is that the thumbnail store ends up holding the fraction of
the library someone happened to scroll past — and that store is the one derived
thing worth syncing, since a second device that downloads the shards gets a
full grid without touching a single RAW. So the complete set is worth an hour
of range fetches paid once, deliberately, on a machine that can afford it.

That is what this adds: a pass over every visible image with an `oc:fileid`,
launched from the settings page and reported in the activity register like
every other background job. The work list is what the store lacks rather than a
flag in the catalog, so it is resumable by construction and safe to press
twice.

Lane-parallel like the metadata sweep, but it does what that sweep declined to.
The store is `&mut` and cannot cross lanes, which is why dating the library
skips thumbnails entirely; here the lanes fetch, decode and *encode*, and only
the ~20 KB result crosses back to the one thread that owns the store and writes
the chunk. The parallelism is real and the single-writer rule is not bent.

Grid class only. The large class is ~860 MB of shards against ~200 MB on the
reference library, paid by every device that syncs them; a photograph looked at
closely still gets its large thumbnail from the interactive path.

Dates come free — the header a preview needs is the header EXIF lives in — and
the pass ends by pushing the shards to the server. A filled store that never
leaves this device would be most of the cost for none of the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 19:34:59 +02:00
co-authored by Claude Opus 5
parent 28325af448
commit d91f1ec277
3 changed files with 681 additions and 42 deletions
+40
View File
@@ -69,6 +69,14 @@ export component SettingsPage inherits Rectangle {
/// they are read.
in property <int> activity-kept: 0;
callback clear-finished();
/// TRACES: FR-CAT-3 | FR-NC-3
/// Whether the whole-library thumbnail pass is running, and whether there
/// is a library for it to run over. Without the second, the button would
/// be offered on a settings page reached before signing in, where pressing
/// it could do nothing at all.
in property <bool> thumbnailing: false;
in property <bool> library-open: false;
callback thumbnail-library();
// --- cache ---------------------------------------------------------
in-out property <string> original-budget;
@@ -305,6 +313,38 @@ export component SettingsPage inherits Rectangle {
job: row;
}
// TRACES: FR-CAT-3 | FR-NC-3 | FR-NC-7
// The one job here that is started rather than
// observed, and it belongs on this page rather than
// in the header: it runs for an hour, it is asked for
// once, and its progress appears in the list directly
// above.
if root.library-open: Rectangle {
height: Theme.gap-sm;
}
if root.library-open: Caption {
text: "Thumbnails are built for photographs as you "
+ "browse them. This builds the rest in one "
+ "pass and sends them to the server, so "
+ "your other devices show a full library "
+ "without downloading anything themselves.";
wrap: word-wrap;
}
if root.library-open: Rectangle {
height: Theme.control-height;
Button {
x: 0;
text: root.thumbnailing
? "Thumbnailing…"
: "Thumbnail the whole library";
enabled: !root.thumbnailing;
clicked => { root.thumbnail-library(); }
}
}
if root.activity-kept > 0: Rectangle {
height: Theme.control-height;