Keep originals on this device, by pin and by use
Fills in `image_cache`, which the previous commit's "On this device" filter read but nothing wrote. Also carries in-flight work that shared these files: the Android TLS root store, the settings page, and a regenerated traceability report. # Two populations, deliberately separate An original is kept here for one of two reasons, and conflating them produces the exact failure the feature exists to prevent. **Pinned** originals were asked for. Pinning a collection before a trip is a promise, so pinned rows are never evicted and never counted against the budget — a cap that could silently delete a pinned trip would make pinning worthless, because it could not be relied on without checking. **Passively cached** originals are a side effect of working: develop already downloads the whole file, so keeping it costs no bandwidth and saves the entire transfer next time. This population is what the budget bounds, evicted least-recently-used, because it otherwise grows until a day of culling fills a disk. Sharing one budget would let a large pin starve the passive cache, or let browsing evict a pin. They are separate. # What was built `dr_catalog::cache` owns the bookkeeping — held tier, size, last use, pinned — and writes the bytes; deciding to download stays with the caller, which is what keeps a crate with no network out of the network's business. Files are written to a temporary and renamed, so a dropped connection cannot leave a truncated file recorded as a complete original. They are named by image id, not filename: `Photos/IMG_0001.CR2` and `Trips/IMG_0001.CR2` are different photographs, and a flat cache keyed on the name would serve one for the other. `spawn_full_fetch` became read-through. A hit is a disk read; a miss stores what it downloads and enforces the budget. A cache that cannot be opened is a miss, not a failure to open the photograph. Pinning writes intent — `tier_desired` — without downloading, so the button responds immediately, and `spawn_pin_fetch` fills it in sequentially afterwards. Sequential because these are tens of megabytes each: the lanes that make the thumbnail sweep fast buy little against one connection's bandwidth and cost a great deal of memory. A pin interrupted by a lost connection resumes from where it stopped. Schema v5 adds `pinned` and `path`. `pinned` is a column rather than something inferred from `pinned_by_rule`, which is ON DELETE SET NULL and so cannot answer for an image whose rule was deleted. A v4 catalog migrates in place; existing rows default to unpinned, the safe direction. The budget and "keep opened originals" come from the settings page rather than a constant, and are applied at startup rather than only on change — a cache capped at 2 GB last session would otherwise spend this one filling to the default. Turning off keeping leaves what is already cached readable: those bytes are paid for, and refusing them would re-download images sitting right there, including pinned ones. Also removes a doubled `#[test]` introduced in the previous commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -583,6 +583,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
callback rescan();
|
||||
/// Back to the launch screen, to change library or account.
|
||||
callback change-library();
|
||||
/// Open the settings page.
|
||||
callback open-settings();
|
||||
|
||||
// --- selection and drag ---
|
||||
//
|
||||
@@ -646,6 +648,17 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <string> offline-reason: "";
|
||||
in property <string> offline-since: "";
|
||||
callback retry-connection();
|
||||
|
||||
// --- pinning (FR-NC-6a) -----------------------------------------------
|
||||
//
|
||||
// Whether the collection the grid is scoped to is kept offline, and how
|
||||
// far the download has got. Progress is shown because pinning a trip is
|
||||
// gigabytes of transfer — a button that appeared to do nothing for twenty
|
||||
// minutes would read as broken.
|
||||
in property <bool> scope-pinned: false;
|
||||
in property <int> pin-done: 0;
|
||||
in property <int> pin-total: 0;
|
||||
callback toggle-pin-scope();
|
||||
/// Narrow to images whose RAW is stored locally — the ones openable now.
|
||||
in property <bool> local-only: false;
|
||||
in property <int> local-count: 0;
|
||||
@@ -767,6 +780,19 @@ export component LibraryGrid inherits Rectangle {
|
||||
clicked => { root.change-library(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// Keep this collection offline. Only offered when the grid is
|
||||
// scoped to one: "pin the whole library" is a different and
|
||||
// much more expensive request, and a button that meant either
|
||||
// depending on invisible state would be a trap.
|
||||
if root.scope-label != "": Button {
|
||||
text: root.scope-pinned ? "Pinned ✓" : "Pin offline";
|
||||
active: root.scope-pinned;
|
||||
enabled: !root.scanning;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.toggle-pin-scope(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
// Shares the finished index so a second device inherits it
|
||||
// rather than repeating hours of range fetches.
|
||||
@@ -782,6 +808,16 @@ export component LibraryGrid inherits Rectangle {
|
||||
visible: !root.scanning;
|
||||
clicked => { root.rescan(); }
|
||||
}
|
||||
|
||||
// Last in the row, and unconditional. The buttons before it
|
||||
// come and go with what the grid is showing; settings is
|
||||
// always reachable, and a control that moved as its
|
||||
// neighbours appeared would be hunted for each time.
|
||||
Button {
|
||||
text: "Settings";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.open-settings(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,6 +952,38 @@ export component LibraryGrid inherits Rectangle {
|
||||
: 0;
|
||||
}
|
||||
|
||||
// --- pinning ------------------------------------------------------
|
||||
//
|
||||
// Its own line rather than the shared progress bar above: that one is
|
||||
// driven by the scan and the sweep, and a pin runs alongside both.
|
||||
if root.pin-total > 0: Rectangle {
|
||||
height: 34px;
|
||||
background: Theme.surface;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Caption {
|
||||
text: "Downloading for offline — " + root.pin-done + " of " + root.pin-total;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
fraction: root.pin-done / max(1, root.pin-total);
|
||||
y: (parent.height - self.height) / 2;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
y: parent.height - 1px;
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
}
|
||||
|
||||
// --- offline ------------------------------------------------------
|
||||
//
|
||||
// Above the scan error, and it suppresses it: when the server is
|
||||
|
||||
Reference in New Issue
Block a user