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:
+149
-3
@@ -4,6 +4,7 @@ import { LaunchScreen } from "launch.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar } from "library.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow } from "collections.slint";
|
||||
import { SettingsPage } from "settings.slint";
|
||||
|
||||
export { LibraryCell, TimelineBar, CollectionRow }
|
||||
|
||||
@@ -23,6 +24,7 @@ component StatusBar inherits Rectangle {
|
||||
in property <bool> can-return-to-library: false;
|
||||
|
||||
callback back-to-library();
|
||||
callback open-settings();
|
||||
|
||||
// Tall enough for a Button to sit in without the strip growing: the
|
||||
// control height and this strip are both 28px by design.
|
||||
@@ -59,6 +61,14 @@ component StatusBar inherits Rectangle {
|
||||
|
||||
Caption { text: root.layout-class; }
|
||||
|
||||
// Reachable from develop as well as from the grid: export defaults are
|
||||
// most likely to be wanted with a finished photograph on screen, which
|
||||
// is exactly where this bar is and the library header is not.
|
||||
Button {
|
||||
text: "Settings";
|
||||
clicked => { root.open-settings(); }
|
||||
}
|
||||
|
||||
// Degraded performance, so this is a caution rather than an active
|
||||
// state: the frame rate has fallen below what the compositing path is
|
||||
// supposed to sustain, which is exactly the assumption A1 exists to
|
||||
@@ -258,6 +268,12 @@ export component AppWindow inherits Window {
|
||||
callback library-toggle-local-only();
|
||||
callback library-retry-connection();
|
||||
|
||||
// --- pinning a collection offline (FR-NC-6a) ---
|
||||
in property <bool> library-scope-pinned: false;
|
||||
in property <int> library-pin-done: 0;
|
||||
in property <int> library-pin-total: 0;
|
||||
callback library-toggle-pin-scope();
|
||||
|
||||
in property <string> library-root-label: "";
|
||||
in-out property <[TimelineBar]> library-timeline;
|
||||
in property <string> library-timeline-label: "";
|
||||
@@ -403,6 +419,69 @@ export component AppWindow inherits Window {
|
||||
callback curve-reset(int);
|
||||
callback reset-all();
|
||||
|
||||
// --- settings (FR-EXP-1, FR-EXP-3, FR-NC-6a) ---
|
||||
//
|
||||
// A page rather than an overlay, and the outermost of the view conditions
|
||||
// below: it is reachable from the library and from develop, so guarding it
|
||||
// with `!show-library` or `!show-launch` would make which one you came from
|
||||
// decide whether it appears.
|
||||
//
|
||||
// Every control saves on change (see `settings_ui.rs`), so there is no
|
||||
// dirty state here and nothing to confirm on the way out.
|
||||
in-out property <bool> show-settings: false;
|
||||
|
||||
in property <string> settings-original-budget: "";
|
||||
in property <bool> settings-original-unlimited: false;
|
||||
in property <string> settings-thumbnail-budget: "";
|
||||
in property <bool> settings-thumbnail-unlimited: false;
|
||||
in property <bool> settings-keep-opened: true;
|
||||
in property <string> settings-cache-usage: "";
|
||||
|
||||
callback settings-original-budget-changed(string);
|
||||
callback settings-original-unlimited-toggled(bool);
|
||||
callback settings-thumbnail-budget-changed(string);
|
||||
callback settings-thumbnail-unlimited-toggled(bool);
|
||||
callback settings-keep-opened-toggled(bool);
|
||||
|
||||
in property <[string]> settings-format-labels;
|
||||
in property <int> settings-format-selected: 0;
|
||||
in property <int> settings-quality: 90;
|
||||
in property <bool> settings-quality-enabled: true;
|
||||
in property <[string]> settings-colour-labels;
|
||||
in property <int> settings-colour-selected: 0;
|
||||
in property <[string]> settings-sizing-labels;
|
||||
in property <int> settings-sizing-selected: 0;
|
||||
in property <int> settings-sizing-value: 0;
|
||||
in property <bool> settings-sizing-has-value: false;
|
||||
in property <string> settings-sizing-unit: "px";
|
||||
in property <bool> settings-allow-upscaling: false;
|
||||
in property <[string]> settings-sharpening-labels;
|
||||
in property <int> settings-sharpening-selected: 0;
|
||||
in property <string> settings-filename-template: "";
|
||||
in property <[string]> settings-collision-labels;
|
||||
in property <int> settings-collision-selected: 0;
|
||||
in property <bool> settings-strip-location: true;
|
||||
in property <string> settings-destination: "";
|
||||
in property <string> settings-error: "";
|
||||
|
||||
callback settings-format-picked(int);
|
||||
callback settings-quality-changed(int);
|
||||
callback settings-colour-picked(int);
|
||||
callback settings-sizing-picked(int);
|
||||
callback settings-sizing-value-changed(string);
|
||||
callback settings-upscaling-toggled(bool);
|
||||
callback settings-sharpening-picked(int);
|
||||
callback settings-template-changed(string);
|
||||
callback settings-collision-picked(int);
|
||||
callback settings-strip-location-toggled(bool);
|
||||
callback settings-destination-changed(string);
|
||||
callback settings-reset();
|
||||
|
||||
/// Show the settings page. Reads the file first, so a second instance's
|
||||
/// writes are picked up rather than overwritten.
|
||||
callback settings-open();
|
||||
callback settings-close();
|
||||
|
||||
// FR-UI-1: layout class follows window width, not device type. A narrow
|
||||
// desktop window gets the compact layout, exactly as a tablet would.
|
||||
//
|
||||
@@ -418,9 +497,69 @@ export component AppWindow inherits Window {
|
||||
// One-way: report width outward, never read layout back into it.
|
||||
changed width => { root.window-resized(self.width); }
|
||||
|
||||
// Settings, over everything. First in the file and first in z-order so the
|
||||
// conditions below can be read as "and settings is not open".
|
||||
if root.show-settings: SettingsPage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
original-budget: root.settings-original-budget;
|
||||
original-unlimited: root.settings-original-unlimited;
|
||||
thumbnail-budget: root.settings-thumbnail-budget;
|
||||
thumbnail-unlimited: root.settings-thumbnail-unlimited;
|
||||
keep-opened: root.settings-keep-opened;
|
||||
cache-usage: root.settings-cache-usage;
|
||||
|
||||
original-budget-changed(t) => { root.settings-original-budget-changed(t); }
|
||||
original-unlimited-toggled(on) => {
|
||||
root.settings-original-unlimited-toggled(on);
|
||||
}
|
||||
thumbnail-budget-changed(t) => { root.settings-thumbnail-budget-changed(t); }
|
||||
thumbnail-unlimited-toggled(on) => {
|
||||
root.settings-thumbnail-unlimited-toggled(on);
|
||||
}
|
||||
keep-opened-toggled(on) => { root.settings-keep-opened-toggled(on); }
|
||||
|
||||
format-labels: root.settings-format-labels;
|
||||
format-selected: root.settings-format-selected;
|
||||
quality: root.settings-quality;
|
||||
quality-enabled: root.settings-quality-enabled;
|
||||
colour-labels: root.settings-colour-labels;
|
||||
colour-selected: root.settings-colour-selected;
|
||||
sizing-labels: root.settings-sizing-labels;
|
||||
sizing-selected: root.settings-sizing-selected;
|
||||
sizing-value: root.settings-sizing-value;
|
||||
sizing-has-value: root.settings-sizing-has-value;
|
||||
sizing-unit: root.settings-sizing-unit;
|
||||
allow-upscaling: root.settings-allow-upscaling;
|
||||
sharpening-labels: root.settings-sharpening-labels;
|
||||
sharpening-selected: root.settings-sharpening-selected;
|
||||
filename-template: root.settings-filename-template;
|
||||
collision-labels: root.settings-collision-labels;
|
||||
collision-selected: root.settings-collision-selected;
|
||||
strip-location: root.settings-strip-location;
|
||||
destination: root.settings-destination;
|
||||
error: root.settings-error;
|
||||
|
||||
format-picked(i) => { root.settings-format-picked(i); }
|
||||
quality-changed(q) => { root.settings-quality-changed(q); }
|
||||
colour-picked(i) => { root.settings-colour-picked(i); }
|
||||
sizing-picked(i) => { root.settings-sizing-picked(i); }
|
||||
sizing-value-changed(t) => { root.settings-sizing-value-changed(t); }
|
||||
upscaling-toggled(on) => { root.settings-upscaling-toggled(on); }
|
||||
sharpening-picked(i) => { root.settings-sharpening-picked(i); }
|
||||
template-changed(t) => { root.settings-template-changed(t); }
|
||||
collision-picked(i) => { root.settings-collision-picked(i); }
|
||||
strip-location-toggled(on) => { root.settings-strip-location-toggled(on); }
|
||||
destination-changed(t) => { root.settings-destination-changed(t); }
|
||||
|
||||
close() => { root.settings-close(); }
|
||||
reset-defaults() => { root.settings-reset(); }
|
||||
}
|
||||
|
||||
// The launch screen replaces the whole window rather than overlaying it:
|
||||
// there is no library to look at until an account is configured.
|
||||
if root.show-launch: LaunchScreen {
|
||||
if !root.show-settings && root.show-launch: LaunchScreen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
signed-in: root.launch-signed-in;
|
||||
@@ -457,7 +596,7 @@ export component AppWindow inherits Window {
|
||||
// has been opened but no image chosen yet. The collections sidebar and the
|
||||
// grid are siblings here rather than the sidebar living inside the grid,
|
||||
// because the drag that connects them has to be owned above both.
|
||||
if !root.show-launch && root.show-library: Rectangle {
|
||||
if !root.show-settings && !root.show-launch && root.show-library: Rectangle {
|
||||
background: Theme.ground;
|
||||
|
||||
HorizontalLayout {
|
||||
@@ -518,6 +657,11 @@ export component AppWindow inherits Window {
|
||||
local-only: root.library-local-only;
|
||||
local-count: root.library-local-count;
|
||||
toggle-local-only() => { root.library-toggle-local-only(); }
|
||||
|
||||
scope-pinned: root.library-scope-pinned;
|
||||
pin-done: root.library-pin-done;
|
||||
pin-total: root.library-pin-total;
|
||||
toggle-pin-scope() => { root.library-toggle-pin-scope(); }
|
||||
root-label: root.library-root-label;
|
||||
timeline: root.library-timeline;
|
||||
timeline-label: root.library-timeline-label;
|
||||
@@ -554,6 +698,7 @@ export component AppWindow inherits Window {
|
||||
cell-clicked(i) => { root.library-cell-clicked(i); }
|
||||
rescan() => { root.library-rescan(); }
|
||||
change-library() => { root.library-change(); }
|
||||
open-settings() => { root.settings-open(); }
|
||||
|
||||
cell-pressed(i, ctrl, shift) => {
|
||||
root.library-cell-pressed(i, ctrl, shift);
|
||||
@@ -600,7 +745,7 @@ export component AppWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if !root.show-launch && !root.show-library: VerticalLayout {
|
||||
if !root.show-settings && !root.show-launch && !root.show-library: VerticalLayout {
|
||||
StatusBar {
|
||||
adapter: root.adapter;
|
||||
backend: root.backend;
|
||||
@@ -612,6 +757,7 @@ export component AppWindow inherits Window {
|
||||
// grid to return to.
|
||||
can-return-to-library: root.library-total > 0;
|
||||
back-to-library() => { root.back-to-library(); }
|
||||
open-settings() => { root.settings-open(); }
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
|
||||
Reference in New Issue
Block a user