Merge: the library grid on its own Slint global, the last of the screen state off the root

This commit is contained in:
2026-09-20 22:43:09 +02:00
24 changed files with 2124 additions and 1833 deletions
+126 -391
View File
@@ -7,7 +7,7 @@ import { CropOverlay } from "crop.slint";
import { HistoryPanel, HistoryRow, Steps } from "history.slint";
import { LaunchScreen } from "launch.slint";
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip } from "library.slint";
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip, Library } from "library.slint";
import { GestureRow } from "gestures.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
@@ -32,6 +32,7 @@ export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
// panels take their wiring this way and why there is one global per family.
export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
export { Collections }
export { Library }
/// Which top-level screen is on display. Exactly one at a time — replacing
/// the `show-launch` / `show-library` / `show-identity` booleans, with
@@ -288,8 +289,6 @@ export component AppWindow inherits Window {
/// the two can drift apart again.
out property <bool> library-visible:
root.active-page == Page.none && root.active-view == View.library;
in-out property <[LibraryCell]> library-cells;
in property <int> library-total: 0;
// --- background activity (FR-CAT-1, FR-NC-6c) ---
//
// Every background job in one register, filled by `activity.rs`. The
@@ -311,74 +310,6 @@ export component AppWindow inherits Window {
in property <int> activity-kept: 0;
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: "";
// --- offline mode (FR-CAT-9) ---
//
// The library keeps working from local data when the server cannot be
// reached: thumbnails come from the shards, and rating and sorting are
// catalog operations that never needed the network. What stops is opening
// an original that was never stored locally.
//
// `offline-reason` carries the transport's own message ("connection
// refused", "dns error") because it is usually specific enough to act on,
// where a bare "offline" leaves the user guessing whether it is their wifi
// or the server.
in property <bool> library-offline: false;
in property <string> library-offline-reason: "";
in property <string> library-offline-since: "";
// Narrows the grid to images whose RAW is stored locally — the ones that
// can actually be opened while offline. Off by default: the catalog is the
// library, and hiding most of it the moment a connection drops would read
// as data loss rather than as a filter.
in-out property <bool> library-local-only: false;
in property <int> library-local-count: 0;
callback library-toggle-local-only();
/// Whether the grid is narrowed to the timeline's visible span.
in-out property <bool> library-range-active: false;
callback library-toggle-date-range();
/// TRACES: FR-CAT-5
callback library-clear-selection();
callback library-select-all();
/// TRACES: FR-CAT-7
/// Whether the grid is showing something with a manual order to change —
/// a single manual collection, not a set and not a saved filter.
in property <bool> library-reorderable: false;
/// Move the selection beside the photograph at this row of the loaded
/// window: after it where the drop landed on the cell's trailing half,
/// before it otherwise.
callback library-reorder-to(int, bool);
callback library-collection-from-selection(string);
/// TRACES: FR-CAT-6
in property <string> library-range-from;
in property <string> library-range-to;
in property <bool> library-range-invalid: false;
callback library-range-edited(string, string);
/// The same range as a band on the capture-time axis, and the ends of it
/// dragged. Fractions of the span the axis is drawn over — Rust owns the
/// conversion both ways, so the band lands where the finger left it.
in property <float> library-range-from-fraction: -1;
in property <float> library-range-to-fraction: -1;
callback library-timeline-range-changed(float, float);
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();
// The question a held collection asks: keep these on the device, or give
// the disk back. Driven entirely from Rust — the counts and the byte sizes
// in the labels are catalog reads, and an empty `title` is what closes it.
@@ -408,32 +339,6 @@ export component AppWindow inherits Window {
callback recovery-rebuild();
callback recovery-dismiss();
in property <string> library-root-label: "";
in-out property <[TimelineBar]> library-timeline;
in property <string> library-timeline-label: "";
in property <string> library-window-label: "";
in property <int> library-offset: 0;
in property <int> library-sweep-done: 0;
in property <int> library-sweep-total: 0;
in property <int> library-current-bucket: 0;
in property <float> library-current-fraction: -1;
in property <bool> library-timeline-anchored: false;
callback library-scrub-fraction(float);
callback library-timeline-pinch(float);
callback library-timeline-pan(float);
callback library-timeline-zoom(int);
in-out property <int> library-columns: 1;
in property <bool> library-syncing: false;
/// TRACES: FR-CAT-3 | FR-NC-3
/// The whole-library thumbnail pass, launched from the settings page.
/// `library-open` gates the button on there being a library to sweep —
/// the page is reachable from the launch screen too.
in property <bool> library-thumbnailing: false;
in property <bool> library-open: false;
// --- people and faces (FR-CULL-8 … FR-CULL-12) ---
//
// A third top-level screen rather than a panel: naming a cluster and
@@ -495,46 +400,9 @@ export component AppWindow inherits Window {
callback identity-stop-indexing();
callback identity-check-coverage();
callback identity-delete-all();
callback library-thumbnail-all();
in-out property <length> library-cell-size: 180px;
/// Whether this session is a touch one, which decides if the rating strip
/// waits to be hovered or stands open. Seeded from the platform by Rust
/// and latched by the grid on the first finger it sees.
in-out property <bool> library-touched: false;
callback library-zoom-cells(int);
/// A pinch step on the grid: the ratio since the last update.
callback library-pinch-cells(float);
/// A pinch on the grid has begun, so the press that opened it was not one.
callback library-pinch-started();
in property <int> library-scroll-to: 0;
in property <int> library-scroll-token: 0;
/// Which row of the loaded window is the photograph currently open in
/// develop, so the roll can mark it. `-1` when it is not in the window.
in property <int> library-roll-current: -1;
/// Whether the photo roll is out. Owned here rather than by the roll, so
/// it survives the develop view being torn down and rebuilt.
in-out property <bool> roll-open: false;
/// Centre the roll on the open photograph the next time the strip settles.
///
/// Raised by Rust when a develop session begins — a photograph opened from
/// the grid — and cleared by the strip itself once honoured. Owned here for
/// the same reason `roll-open` is: the develop view is rebuilt whenever an
/// overlay screen covers it, and a request raised before that must survive
/// it. See `PhotoRoll::centre-request`.
in-out property <bool> library-roll-centre: false;
/// A photograph was chosen from the roll: the row within the loaded
/// window, which is what a cell click reports too.
callback library-roll-pick(int);
callback library-sync-now();
callback library-columns-changed();
callback library-scrolled(int);
/// How many cells the grid's viewport shows at once. Rust sizes the
/// loaded window from it — see `library_ui::window_move`.
callback library-viewport-cells(int);
callback library-cell-clicked(int);
callback library-rescan();
/// Grid → launch screen, to pick a different folder or account.
callback library-change();
/// Develop → grid.
callback back-to-library();
@@ -559,129 +427,6 @@ export component AppWindow inherits Window {
/// Ask about a collection's local copies: the tray on a row, or a long
/// press anywhere on it.
callback collection-offline-menu(int);
/// TRACES: FR-CAT-7 | FR-UI-4
/// File the grid's selection in a collection without dragging it there:
/// the target's id, and whether to take the images out of the collection
/// currently being shown.
callback library-file-in-collection(int, bool);
/// TRACES: FR-CAT-5 | FR-CAT-6
/// Keywording the grid's selection. The catalog has been searchable by
/// keyword since it existed and there was nowhere to type one; this is it.
///
/// The vocabulary arrives already answered against the selection — each row
/// says how many of the selected photographs carry that word — because only
/// Rust knows what is selected, and a `.slint` file counting it would need
/// the selection as a second model that could disagree with the first.
in property <[KeywordRow]> library-keywords;
/// The sheet is opening: recompute the rows against the selection as it
/// stands now. Pulled rather than pushed, because the selection changes on
/// every arrow key and the sheet is shut for almost all of them.
callback library-keywords-opened();
/// Put a keyword on the selection, creating it if it is new. By name, so a
/// word typed into the field and a word tapped in the list are one path.
callback library-assign-keyword(string);
/// Take a keyword off the selection. Never deletes the keyword itself —
/// it stays in the vocabulary and on every other photograph that carries it.
callback library-unassign-keyword(string);
/// TRACES: FR-UI-2
/// Whether a tap in the grid selects rather than opens, and the button
/// that turns it on. The long press does the same thing without it.
in property <bool> library-select-mode: false;
callback library-toggle-select-mode();
/// TRACES: FR-CAT-7 | FR-UI-4
/// The row a press has held long enough to pick up, or `-1`.
///
/// Set by the same hold that turns on selection mode and cleared by the
/// grid when the press ends — `in-out` because both ends write it. See
/// `held-row` in `library.slint` for what it draws and what it stops.
in-out property <int> library-held-row: -1;
/// A press on a cell ended, so the long-press timer can be cancelled.
callback library-cell-press-ended();
// --- drag and drop ---
//
// Slint's own `DragArea`/`DropArea` carry this: pointer capture, the
// click-versus-drag threshold, arbitration against the grid's Flickable,
// and the image drawn under the cursor. So there is no badge to position
// and no hover state to mirror here — only the payload, which Rust builds
// from the selection, and the count the header shows.
/// The drag payload, built by Rust from the current selection.
pure callback library-drag-payload() -> data-transfer;
/// The bitmap that travels under the cursor: one thumbnail, or a fanned
/// stack where several images are being dragged. Composited in Rust.
in property <image> library-drag-image;
/// A drag began on a cell, so an unselected one can be promoted before the
/// payload is read.
callback library-drag-started(int);
callback library-drag-finished();
in property <int> library-selected-count: 0;
callback library-cell-pressed(int, bool, bool);
callback library-remove-from-collection();
// --- where the selection is filed (FR-CAT-7) ---------------------------
//
// `library-remove-from-collection` can only ever mean the collection the
// grid is scoped to — it is the only one that button can name. Opens the
// membership sheet, whose state (`Collections.membership-open` and
// `.membership-rows`) is on the sidebar's own global — only the library
// side knows the selection has changed enough to be worth recomputing
// the sheet against.
callback library-open-membership();
// The keyboard cursor: where the arrows are in the library, as an image
// ordinal. Rust owns it — clamping it needs the library's length, and
// moving it may have to swap the loaded window underneath.
in property <int> library-cursor: -1;
callback library-move-cursor(int, bool);
callback library-open-cursor();
// --- ratings and flags (FR-CAT-5, FR-CULL-4) ---
//
// Stars and pick/reject, set from the grid and persisted to the catalog
// and the sidecar. Every image enters unrated, which is a state of its
// own rather than a zero score.
/// A star was clicked on a cell: row, then the rating 0..5.
callback library-cell-rated(int, int);
/// The trash target was clicked on one cell, by row.
callback library-cell-trashed(int);
/// The burst mark was clicked on one cell, by row (FR-CULL-5).
callback library-burst-toggled(int);
/// One frame of an open burst was named as the one the group folds to, by
/// row (FR-CULL-5).
callback library-burst-representative-chosen(int);
/// Move the grid selection to the trash — the `Delete` key.
callback library-trash-selection();
/// A judgement key was pressed, applying to the whole selection. One of
/// the two arguments is -1, saying which axis was *not* meant.
callback library-judged(int, int);
/// Whose photographs the grid is narrowed to, one entry each.
in property <[PersonChip]> library-filter-people;
/// Whether those people are an intersection rather than a union.
in property <bool> library-filter-people-all: false;
callback library-filter-person-cleared(int);
callback library-filter-people-mode-toggled();
/// TRACES: FR-CULL-8a | FR-CULL-13
/// Whether the grid is narrowed to photographs with nobody blinking.
in property <bool> library-filter-eyes-open: false;
callback library-filter-eyes-open-toggled();
/// Everyone the library knows, for the filter bar's people tray.
/// TRACES: FR-UI-4
/// The gesture reference's rows, read from the generated table.
in property <[GestureRow]> library-gestures;
in property <[PersonChip]> library-people;
callback library-people-listed();
callback library-filter-person-toggled(int);
in property <int> library-filter-min-rating: 0;
in property <bool> library-filter-unjudged: false;
in property <int> library-filter-flag: 0;
/// Images per star count, index 0 unrated, for the filter chips.
in-out property <[int]> library-rating-counts;
callback library-filter-min-rating-changed(int);
callback library-filter-unjudged-changed(bool);
callback library-filter-flag-changed(int);
callback next-image();
callback prev-image();
@@ -809,8 +554,6 @@ export component AppWindow inherits Window {
in property <float> merge-fill-mirror: 48;
in property <float> merge-fill-feather: 48;
in property <bool> merge-done: false;
/// Merge the grid's selection: opens the page and starts the alignment.
callback library-merge-selection();
/// The confirmation (FR-MRG-1). Nothing is written before this.
callback merge-confirm();
callback merge-abandon();
@@ -1098,13 +841,6 @@ in property <bool> panel-visible: true;
/// composes its own label around a selection count — so it is given the
/// fact and not the sentence.
in property <bool> export-to-server: false;
/// Whether a batch is running. The grid's export button becomes its cancel
/// button; there is nowhere else a long batch can be stopped from.
in property <bool> library-exporting: false;
/// Export the grid's selection in the background (FR-EXP-7).
callback library-export-selection();
/// TRACES: NFR-ARCH-3
callback library-cancel-export();
/// Show or hide the develop column.
callback toggle-panel();
/// Export the image on screen, using the settings as they stand.
@@ -1410,7 +1146,7 @@ in property <bool> panel-visible: true;
activity-running: root.activity-running;
activity-kept: root.activity-kept;
clear-finished() => { root.activity-clear-finished(); }
thumbnailing: root.library-thumbnailing;
thumbnailing: Library.library-thumbnailing;
// The face pass reuses the Identity Manager's own state rather
// than duplicating it: one job, startable from either place, and
// a button that cannot disagree with the screen about whether it
@@ -1421,8 +1157,8 @@ in property <bool> panel-visible: true;
face-detector-labels: root.settings-face-detector-labels;
face-detector-selected: root.settings-face-detector-selected;
face-detector-picked(i) => { root.settings-face-detector-picked(i); }
library-open: root.library-open;
thumbnail-library() => { root.library-thumbnail-all(); }
library-open: Library.library-open;
thumbnail-library() => { Library.library-thumbnail-all(); }
index-faces() => { root.identity-index(); }
reindex-faces() => { root.identity-reindex(); }
@@ -1471,7 +1207,7 @@ in property <bool> panel-visible: true;
confirm-all() => { root.identity-confirm-all(); }
split-picked() => { root.identity-split-picked(); }
compact: !root.expanded;
photos-filtered: root.library-filter-people.length > 0;
photos-filtered: Library.library-filter-people.length > 0;
regrouping: root.identity-regrouping;
regroup-status: root.identity-regroup-status;
merge-probability: root.identity-merge-probability;
@@ -1560,7 +1296,7 @@ in property <bool> panel-visible: true;
height: 100%;
rows: Collections.collection-rows;
selected-id: root.collection-selected;
total-images: root.library-total;
total-images: Library.library-total;
error: Collections.collection-error;
// TRACES: FR-NC-6a
@@ -1568,10 +1304,10 @@ in property <bool> panel-visible: true;
// referred to the tree rather than to the library.
// They read the same window state the header did — one
// source per fact — and call the same handlers.
scanning: root.library-scanning;
scope-pinned: root.library-scope-pinned;
toggle-pin-scope() => { root.library-toggle-pin-scope(); }
change-library() => { root.library-change(); }
scanning: Library.library-scanning;
scope-pinned: Library.library-scope-pinned;
toggle-pin-scope() => { Library.library-toggle-pin-scope(); }
change-library() => { Library.library-change(); }
select(id) => { root.collection-select(id); }
toggle(id) => { Collections.collection-toggle(id); }
@@ -1592,7 +1328,7 @@ in property <bool> panel-visible: true;
rename-cancel() => { Collections.collection-rename-cancel(); }
trash-count: Collections.trash-count;
trash-label: Collections.trash-label;
selected-count: root.library-selected-count;
selected-count: Library.library-selected-count;
dropped-on(id) => { Collections.collection-dropped(id); }
collection-drag-payload(id) => { return Collections.collection-drag-payload(id); }
@@ -1610,50 +1346,50 @@ in property <bool> panel-visible: true;
// 768 logical pixels, which fits the title and the status
// line but not the library's buttons beside them.
expanded: root.expanded;
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;
cells: Library.library-cells;
total: Library.library-total;
scanning: Library.library-scanning;
opening: Library.library-opening;
scan-status: Library.library-status;
scan-error: Library.library-error;
offline: root.library-offline;
offline-reason: root.library-offline-reason;
offline-since: root.library-offline-since;
retry-connection() => { root.library-retry-connection(); }
local-only: root.library-local-only;
local-count: root.library-local-count;
toggle-local-only() => { root.library-toggle-local-only(); }
range-active: root.library-range-active;
toggle-date-range() => { root.library-toggle-date-range(); }
clear-selection() => { root.library-clear-selection(); }
select-all() => { root.library-select-all(); }
reorderable: root.library-reorderable;
reorder-to(row, after) => { root.library-reorder-to(row, after); }
offline: Library.library-offline;
offline-reason: Library.library-offline-reason;
offline-since: Library.library-offline-since;
retry-connection() => { Library.library-retry-connection(); }
local-only: Library.library-local-only;
local-count: Library.library-local-count;
toggle-local-only() => { Library.library-toggle-local-only(); }
range-active: Library.library-range-active;
toggle-date-range() => { Library.library-toggle-date-range(); }
clear-selection() => { Library.library-clear-selection(); }
select-all() => { Library.library-select-all(); }
reorderable: Library.library-reorderable;
reorder-to(row, after) => { Library.library-reorder-to(row, after); }
collection-from-selection(name) => {
root.library-collection-from-selection(name);
Library.library-collection-from-selection(name);
}
range-from: root.library-range-from;
range-to: root.library-range-to;
range-invalid: root.library-range-invalid;
range-edited(a, b) => { root.library-range-edited(a, b); }
range-from-fraction: root.library-range-from-fraction;
range-to-fraction: root.library-range-to-fraction;
range-from: Library.library-range-from;
range-to: Library.library-range-to;
range-invalid: Library.library-range-invalid;
range-edited(a, b) => { Library.library-range-edited(a, b); }
range-from-fraction: Library.library-range-from-fraction;
range-to-fraction: Library.library-range-to-fraction;
timeline-range-changed(a, b) => {
root.library-timeline-range-changed(a, b);
Library.library-timeline-range-changed(a, b);
}
collections-visible: root.collections-visible;
toggle-collections() => { root.toggle-collections(); }
pin-done: root.library-pin-done;
pin-total: root.library-pin-total;
root-label: root.library-root-label;
timeline: root.library-timeline;
timeline-label: root.library-timeline-label;
window-label: root.library-window-label;
offset: root.library-offset;
selected-count: root.library-selected-count;
pin-done: Library.library-pin-done;
pin-total: Library.library-pin-total;
root-label: Library.library-root-label;
timeline: Library.library-timeline;
timeline-label: Library.library-timeline-label;
window-label: Library.library-window-label;
offset: Library.library-offset;
selected-count: Library.library-selected-count;
scope-label: Collections.collection-scope-label;
settings-armed: Transfer.armed;
paste-settings-to-selection => {
@@ -1664,108 +1400,108 @@ in property <bool> panel-visible: true;
// sheet says it would act on and what the apply handler
// reads back to decide it means the batch.
open-presets => {
root.preset-apply-count = root.library-selected-count;
root.preset-apply-count = Library.library-selected-count;
root.preset-can-save = false;
root.presets-open = true;
}
// TRACES: FR-EXP-7
exporting: root.library-exporting;
exporting: Library.library-exporting;
export-to-server: root.export-to-server;
export-selection => { root.library-export-selection(); }
cancel-export => { root.library-cancel-export(); }
export-selection => { Library.library-export-selection(); }
cancel-export => { Library.library-cancel-export(); }
// TRACES: FR-MRG-1
merge-selection => { root.library-merge-selection(); }
merge-selection => { Library.library-merge-selection(); }
sweep-done: root.library-sweep-done;
sweep-total: root.library-sweep-total;
sweep-done: Library.library-sweep-done;
sweep-total: Library.library-sweep-total;
current-bucket: root.library-current-bucket;
current-bucket-fraction: root.library-current-fraction;
timeline-anchored: root.library-timeline-anchored;
current-bucket: Library.library-current-bucket;
current-bucket-fraction: Library.library-current-fraction;
timeline-anchored: Library.library-timeline-anchored;
scrub-fraction(f) => { root.library-scrub-fraction(f); }
timeline-pinch(r) => { root.library-timeline-pinch(r); }
timeline-pan(d) => { root.library-timeline-pan(d); }
timeline-zoom(d) => { root.library-timeline-zoom(d); }
syncing: root.library-syncing;
requested-cell-size: root.library-cell-size;
touched <=> root.library-touched;
zoom-cells(d) => { root.library-zoom-cells(d); }
pinch-cells(r) => { root.library-pinch-cells(r); }
pinch-started() => { root.library-pinch-started(); }
scroll-to: root.library-scroll-to;
scroll-token: root.library-scroll-token;
sync-now() => { root.library-sync-now(); }
scrub-fraction(f) => { Library.library-scrub-fraction(f); }
timeline-pinch(r) => { Library.library-timeline-pinch(r); }
timeline-pan(d) => { Library.library-timeline-pan(d); }
timeline-zoom(d) => { Library.library-timeline-zoom(d); }
syncing: Library.library-syncing;
requested-cell-size: Library.library-cell-size;
touched <=> Library.library-touched;
zoom-cells(d) => { Library.library-zoom-cells(d); }
pinch-cells(r) => { Library.library-pinch-cells(r); }
pinch-started() => { Library.library-pinch-started(); }
scroll-to: Library.library-scroll-to;
scroll-token: Library.library-scroll-token;
sync-now() => { Library.library-sync-now(); }
columns-changed(n) => {
root.library-columns = n;
Library.library-columns = n;
// Which cell begins a row just changed, and month headings sit on
// row-leading cells — so they must be recomputed, not just moved.
root.library-columns-changed();
Library.library-columns-changed();
}
scrolled(i) => { root.library-scrolled(i); }
viewport-cells-changed(n) => { root.library-viewport-cells(n); }
cell-clicked(i) => { root.library-cell-clicked(i); }
rescan() => { root.library-rescan(); }
scrolled(i) => { Library.library-scrolled(i); }
viewport-cells-changed(n) => { Library.library-viewport-cells(n); }
cell-clicked(i) => { Library.library-cell-clicked(i); }
rescan() => { Library.library-rescan(); }
can-import: root.import-supported;
open-import() => { root.import-open(); }
open-settings() => { root.settings-open(); }
open-people() => { root.identity-open(); }
cell-pressed(i, ctrl, shift) => {
root.library-cell-pressed(i, ctrl, shift);
Library.library-cell-pressed(i, ctrl, shift);
}
cell-press-ended() => { root.library-cell-press-ended(); }
select-mode: root.library-select-mode;
toggle-select-mode() => { root.library-toggle-select-mode(); }
held-row <=> root.library-held-row;
cell-press-ended() => { Library.library-cell-press-ended(); }
select-mode: Library.library-select-mode;
toggle-select-mode() => { Library.library-toggle-select-mode(); }
held-row <=> Library.library-held-row;
// The sidebar's rows, not a second model: the sheet files
// into the same tree the sidebar draws.
collections: Collections.collection-rows;
file-in-collection(id, moves) => {
root.library-file-in-collection(id, moves);
Library.library-file-in-collection(id, moves);
}
keywords: root.library-keywords;
keywords-opened() => { root.library-keywords-opened(); }
assign-keyword(word) => { root.library-assign-keyword(word); }
unassign-keyword(word) => { root.library-unassign-keyword(word); }
cursor: root.library-cursor;
keywords: Library.library-keywords;
keywords-opened() => { Library.library-keywords-opened(); }
assign-keyword(word) => { Library.library-assign-keyword(word); }
unassign-keyword(word) => { Library.library-unassign-keyword(word); }
cursor: Library.library-cursor;
move-cursor(delta, extend) => {
root.library-move-cursor(delta, extend);
Library.library-move-cursor(delta, extend);
}
open-cursor() => { root.library-open-cursor(); }
drag-image: root.library-drag-image;
drag-payload() => { return root.library-drag-payload(); }
drag-started(i) => { root.library-drag-started(i); }
drag-finished() => { root.library-drag-finished(); }
open-cursor() => { Library.library-open-cursor(); }
drag-image: Library.library-drag-image;
drag-payload() => { return Library.library-drag-payload(); }
drag-started(i) => { Library.library-drag-started(i); }
drag-finished() => { Library.library-drag-finished(); }
remove-from-collection() => {
root.library-remove-from-collection();
Library.library-remove-from-collection();
}
open-membership() => { root.library-open-membership(); }
open-membership() => { Library.library-open-membership(); }
filter-people: root.library-filter-people;
filter-people-all: root.library-filter-people-all;
filter-eyes-open: root.library-filter-eyes-open;
people: root.library-people;
gestures: root.library-gestures;
filter-min-rating: root.library-filter-min-rating;
filter-unjudged: root.library-filter-unjudged;
filter-flag: root.library-filter-flag;
rating-counts: root.library-rating-counts;
filter-people: Library.library-filter-people;
filter-people-all: Library.library-filter-people-all;
filter-eyes-open: Library.library-filter-eyes-open;
people: Library.library-people;
gestures: Library.library-gestures;
filter-min-rating: Library.library-filter-min-rating;
filter-unjudged: Library.library-filter-unjudged;
filter-flag: Library.library-filter-flag;
rating-counts: Library.library-rating-counts;
cell-rated(i, n) => { root.library-cell-rated(i, n); }
cell-trashed(i) => { root.library-cell-trashed(i); }
burst-toggled(i) => { root.library-burst-toggled(i); }
cell-rated(i, n) => { Library.library-cell-rated(i, n); }
cell-trashed(i) => { Library.library-cell-trashed(i); }
burst-toggled(i) => { Library.library-burst-toggled(i); }
burst-representative-chosen(i) => {
root.library-burst-representative-chosen(i);
Library.library-burst-representative-chosen(i);
}
trash-selection() => { root.library-trash-selection(); }
trash-selection() => { Library.library-trash-selection(); }
// Derived from the sidebar's own selection rather than
// mirrored in a second property: `-1` is already the sentinel
// for the trash row (see collections.slint), and two sources
// for one fact is how they come to disagree.
viewing-trash: root.collection-selected == -1;
judged(rating, flag) => { root.library-judged(rating, flag); }
judged(rating, flag) => { Library.library-judged(rating, flag); }
// `F2` renames whatever the grid is scoped to. Answered from
// the sidebar's selection rather than a second property, for
// the same reason `viewing-trash` above is: one fact, one
@@ -1776,17 +1512,17 @@ in property <bool> panel-visible: true;
Collections.collection-rename-start(root.collection-selected);
}
filter-min-rating-changed(n) => {
root.library-filter-min-rating-changed(n);
Library.library-filter-min-rating-changed(n);
}
filter-unjudged-toggled(on) => {
root.library-filter-unjudged-changed(on);
Library.library-filter-unjudged-changed(on);
}
filter-flag-changed(f) => { root.library-filter-flag-changed(f); }
filter-person-cleared(id) => { root.library-filter-person-cleared(id); }
filter-people-mode-toggled() => { root.library-filter-people-mode-toggled(); }
filter-eyes-open-toggled() => { root.library-filter-eyes-open-toggled(); }
people-listed() => { root.library-people-listed(); }
filter-person-toggled(id) => { root.library-filter-person-toggled(id); }
filter-flag-changed(f) => { Library.library-filter-flag-changed(f); }
filter-person-cleared(id) => { Library.library-filter-person-cleared(id); }
filter-people-mode-toggled() => { Library.library-filter-people-mode-toggled(); }
filter-eyes-open-toggled() => { Library.library-filter-eyes-open-toggled(); }
people-listed() => { Library.library-people-listed(); }
filter-person-toggled(id) => { Library.library-filter-person-toggled(id); }
}
}
}
@@ -1805,7 +1541,7 @@ in property <bool> panel-visible: true;
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
// Only where a library was opened; command-line files have no
// grid to return to.
can-return-to-library: root.library-total > 0;
can-return-to-library: Library.library-total > 0;
panel-visible: root.panel-visible;
export-label: root.export-label;
export-busy: root.export-busy;
@@ -2315,7 +2051,6 @@ in property <bool> panel-visible: true;
}
}
// --- crop overlay ------------------------------------------
//
// Placed over the fitted image, so the fractions it draws are
@@ -2657,7 +2392,7 @@ in property <bool> panel-visible: true;
// instead — the controls ride on top of the roll's
// band, at whatever height it currently stands.
property <length> roll-clear:
root.library-total > 0
Library.library-total > 0
? (root.roll-open
? Theme.roll-strip + Theme.roll-reach
: Theme.roll-reach)
@@ -2770,14 +2505,14 @@ in property <bool> panel-visible: true;
//
// Last in the canvas, so it draws over the photograph
// rather than under it.
if root.library-total > 0: PhotoRoll {
if Library.library-total > 0: PhotoRoll {
width: 100%;
height: 100%;
cells: root.library-cells;
current: root.library-roll-current;
cells: Library.library-cells;
current: Library.library-roll-current;
open <=> root.roll-open;
centre-request <=> root.library-roll-centre;
pick(i) => { root.library-roll-pick(i); }
centre-request <=> Library.library-roll-centre;
pick(i) => { Library.library-roll-pick(i); }
}
// Report size changes so the render target can be resized to
@@ -3179,7 +2914,7 @@ in property <bool> panel-visible: true;
width: 100%;
height: 100%;
open: Collections.membership-open;
selected-count: root.library-selected-count;
selected-count: Library.library-selected-count;
rows: Collections.membership-rows;
remove(id) => { Collections.membership-remove(id); }
dismiss() => { Collections.membership-dismiss(); }
+270
View File
@@ -479,6 +479,276 @@ export struct LibraryCell {
burst-representative: bool,
}
// TRACES: FR-CAT-3 | FR-CAT-4 | FR-CAT-5 | FR-CAT-6 | FR-CAT-7 | FR-CULL-4 | FR-CULL-5
// The library grid's own state: the loaded window of cells, the selection,
// the timeline, the filter bar, ratings and flags, keywording, drag and drop,
// and the routes into and out of the screen.
//
// A global rather than properties and callbacks on the window root — see
// `session.slint` for why the develop panels stopped taking their wiring
// through the window root, and `collections.slint` for the sidebar's own
// global; the same reasoning applies here. What is shared with the sidebar
// (the collection the grid is scoped to, whether the sidebar is shown) stays
// on the window, because `run()`'s panel-layout wiring and the sidebar's own
// Rust still reach it there.
export global Library {
in-out property <[LibraryCell]> library-cells;
in property <int> library-total: 0;
// Narrows the grid to images whose RAW is stored locally — the ones that
// can actually be opened while offline. Off by default: the catalog is the
// library, and hiding most of it the moment a connection drops would read
// as data loss rather than as a filter.
in-out property <bool> library-local-only: false;
in property <int> library-local-count: 0;
callback library-toggle-local-only();
/// TRACES: FR-CAT-5
callback library-clear-selection();
callback library-select-all();
/// TRACES: FR-CAT-7
/// Whether the grid is showing something with a manual order to change —
/// a single manual collection, not a set and not a saved filter.
in property <bool> library-reorderable: false;
/// Move the selection beside the photograph at this row of the loaded
/// window: after it where the drop landed on the cell's trailing half,
/// before it otherwise.
callback library-reorder-to(int, bool);
callback library-collection-from-selection(string);
in-out property <int> library-columns: 1;
in-out property <length> library-cell-size: 180px;
/// Whether this session is a touch one, which decides if the rating strip
/// waits to be hovered or stands open. Seeded from the platform by Rust
/// and latched by the grid on the first finger it sees.
in-out property <bool> library-touched: false;
callback library-zoom-cells(int);
/// A pinch step on the grid: the ratio since the last update.
callback library-pinch-cells(float);
/// A pinch on the grid has begun, so the press that opened it was not one.
callback library-pinch-started();
in property <int> library-scroll-to: 0;
in property <int> library-scroll-token: 0;
/// Which row of the loaded window is the photograph currently open in
/// develop, so the roll can mark it. `-1` when it is not in the window.
in property <int> library-roll-current: -1;
/// Centre the roll on the open photograph the next time the strip settles.
///
/// Raised by Rust when a develop session begins — a photograph opened from
/// the grid — and cleared by the strip itself once honoured. Owned here for
/// the same reason `roll-open` is: the develop view is rebuilt whenever an
/// overlay screen covers it, and a request raised before that must survive
/// it. See `PhotoRoll::centre-request`.
in-out property <bool> library-roll-centre: false;
/// A photograph was chosen from the roll: the row within the loaded
/// window, which is what a cell click reports too.
callback library-roll-pick(int);
callback library-columns-changed();
callback library-scrolled(int);
/// How many cells the grid's viewport shows at once. Rust sizes the
/// loaded window from it — see `library_ui::window_move`.
callback library-viewport-cells(int);
callback library-cell-clicked(int);
/// TRACES: FR-CAT-7 | FR-UI-4
/// File the grid's selection in a collection without dragging it there:
/// the target's id, and whether to take the images out of the collection
/// currently being shown.
callback library-file-in-collection(int, bool);
/// TRACES: FR-UI-2
/// Whether a tap in the grid selects rather than opens, and the button
/// that turns it on. The long press does the same thing without it.
in property <bool> library-select-mode: false;
callback library-toggle-select-mode();
/// TRACES: FR-CAT-7 | FR-UI-4
/// The row a press has held long enough to pick up, or `-1`.
///
/// Set by the same hold that turns on selection mode and cleared by the
/// grid when the press ends — `in-out` because both ends write it. See
/// `held-row` in `library.slint` for what it draws and what it stops.
in-out property <int> library-held-row: -1;
/// A press on a cell ended, so the long-press timer can be cancelled.
callback library-cell-press-ended();
// --- drag and drop ---
//
// Slint's own `DragArea`/`DropArea` carry this: pointer capture, the
// click-versus-drag threshold, arbitration against the grid's Flickable,
// and the image drawn under the cursor. So there is no badge to position
// and no hover state to mirror here — only the payload, which Rust builds
// from the selection, and the count the header shows.
/// The drag payload, built by Rust from the current selection.
pure callback library-drag-payload() -> data-transfer;
/// The bitmap that travels under the cursor: one thumbnail, or a fanned
/// stack where several images are being dragged. Composited in Rust.
in property <image> library-drag-image;
/// A drag began on a cell, so an unselected one can be promoted before the
/// payload is read.
callback library-drag-started(int);
callback library-drag-finished();
in property <int> library-selected-count: 0;
callback library-cell-pressed(int, bool, bool);
callback library-remove-from-collection();
// --- where the selection is filed (FR-CAT-7) ---------------------------
//
// `library-remove-from-collection` can only ever mean the collection the
// grid is scoped to — it is the only one that button can name. Opens the
// membership sheet, whose state (`Collections.membership-open` and
// `.membership-rows`) is on the sidebar's own global — only the library
// side knows the selection has changed enough to be worth recomputing
// the sheet against.
callback library-open-membership();
// The keyboard cursor: where the arrows are in the library, as an image
// ordinal. Rust owns it — clamping it needs the library's length, and
// moving it may have to swap the loaded window underneath.
in property <int> library-cursor: -1;
callback library-move-cursor(int, bool);
callback library-open-cursor();
/// TRACES: FR-CAT-5 | FR-CAT-6
/// Keywording the grid's selection. The catalog has been searchable by
/// keyword since it existed and there was nowhere to type one; this is it.
///
/// The vocabulary arrives already answered against the selection — each row
/// says how many of the selected photographs carry that word — because only
/// Rust knows what is selected, and a `.slint` file counting it would need
/// the selection as a second model that could disagree with the first.
in property <[KeywordRow]> library-keywords;
/// The sheet is opening: recompute the rows against the selection as it
/// stands now. Pulled rather than pushed, because the selection changes on
/// every arrow key and the sheet is shut for almost all of them.
callback library-keywords-opened();
/// Put a keyword on the selection, creating it if it is new. By name, so a
/// word typed into the field and a word tapped in the list are one path.
callback library-assign-keyword(string);
/// Take a keyword off the selection. Never deletes the keyword itself —
/// it stays in the vocabulary and on every other photograph that carries it.
callback library-unassign-keyword(string);
// --- ratings and flags (FR-CAT-5, FR-CULL-4) ---
//
// Stars and pick/reject, set from the grid and persisted to the catalog
// and the sidecar. Every image enters unrated, which is a state of its
// own rather than a zero score.
/// A star was clicked on a cell: row, then the rating 0..5.
callback library-cell-rated(int, int);
/// The trash target was clicked on one cell, by row.
callback library-cell-trashed(int);
/// The burst mark was clicked on one cell, by row (FR-CULL-5).
callback library-burst-toggled(int);
/// One frame of an open burst was named as the one the group folds to, by
/// row (FR-CULL-5).
callback library-burst-representative-chosen(int);
/// Move the grid selection to the trash — the `Delete` key.
callback library-trash-selection();
/// A judgement key was pressed, applying to the whole selection. One of
/// the two arguments is -1, saying which axis was *not* meant.
callback library-judged(int, int);
in property <int> library-filter-min-rating: 0;
in property <bool> library-filter-unjudged: false;
in property <int> library-filter-flag: 0;
/// Images per star count, index 0 unrated, for the filter chips.
in-out property <[int]> library-rating-counts;
callback library-filter-min-rating-changed(int);
callback library-filter-unjudged-changed(bool);
callback library-filter-flag-changed(int);
/// Whether the grid is narrowed to the timeline's visible span.
in-out property <bool> library-range-active: false;
callback library-toggle-date-range();
/// TRACES: FR-CAT-6
in property <string> library-range-from;
in property <string> library-range-to;
in property <bool> library-range-invalid: false;
callback library-range-edited(string, string);
/// The same range as a band on the capture-time axis, and the ends of it
/// dragged. Fractions of the span the axis is drawn over — Rust owns the
/// conversion both ways, so the band lands where the finger left it.
in property <float> library-range-from-fraction: -1;
in property <float> library-range-to-fraction: -1;
callback library-timeline-range-changed(float, float);
in property <string> library-root-label: "";
in-out property <[TimelineBar]> library-timeline;
in property <string> library-timeline-label: "";
in property <string> library-window-label: "";
in property <int> library-offset: 0;
in property <int> library-sweep-done: 0;
in property <int> library-sweep-total: 0;
in property <int> library-current-bucket: 0;
in property <float> library-current-fraction: -1;
in property <bool> library-timeline-anchored: false;
callback library-scrub-fraction(float);
callback library-timeline-pinch(float);
callback library-timeline-pan(float);
callback library-timeline-zoom(int);
/// Whose photographs the grid is narrowed to, one entry each.
in property <[PersonChip]> library-filter-people;
/// Whether those people are an intersection rather than a union.
in property <bool> library-filter-people-all: false;
callback library-filter-person-cleared(int);
callback library-filter-people-mode-toggled();
/// TRACES: FR-CULL-8a | FR-CULL-13
/// Whether the grid is narrowed to photographs with nobody blinking.
in property <bool> library-filter-eyes-open: false;
callback library-filter-eyes-open-toggled();
/// Everyone the library knows, for the filter bar's people tray.
/// TRACES: FR-UI-4
/// The gesture reference's rows, read from the generated table.
in property <[GestureRow]> library-gestures;
in property <[PersonChip]> library-people;
callback library-people-listed();
callback library-filter-person-toggled(int);
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: "";
// --- offline mode (FR-CAT-9) ---
//
// The library keeps working from local data when the server cannot be
// reached: thumbnails come from the shards, and rating and sorting are
// catalog operations that never needed the network. What stops is opening
// an original that was never stored locally.
//
// `offline-reason` carries the transport's own message ("connection
// refused", "dns error") because it is usually specific enough to act on,
// where a bare "offline" leaves the user guessing whether it is their wifi
// or the server.
in property <bool> library-offline: false;
in property <string> library-offline-reason: "";
in property <string> library-offline-since: "";
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 <bool> library-syncing: false;
/// TRACES: FR-CAT-3 | FR-NC-3
/// The whole-library thumbnail pass, launched from the settings page.
/// `library-open` gates the button on there being a library to sweep —
/// the page is reachable from the launch screen too.
in property <bool> library-thumbnailing: false;
in property <bool> library-open: false;
callback library-thumbnail-all();
callback library-sync-now();
callback library-rescan();
/// Grid → launch screen, to pick a different folder or account.
callback library-change();
/// Merge the grid's selection: opens the page and starts the alignment.
callback library-merge-selection();
/// Whether a batch is running. The grid's export button becomes its cancel
/// button; there is nowhere else a long batch can be stopped from.
in property <bool> library-exporting: false;
/// Export the grid's selection in the background (FR-EXP-7).
callback library-export-selection();
/// TRACES: NFR-ARCH-3
callback library-cancel-export();
}
// The photo roll: the grid's loaded window along the foot of the develop view.
//
// # Why this exists