Merge branch 'android-collections'
Build and test / Desktop (Linux) (push) Failing after 18m53s
Build and test / Layer separation (push) Successful in 41s
Traceability / Requirement traces (push) Failing after 1m4s
🐳 Android image / Build and push (push) Successful in 6s
Build and test / android-image (push) Successful in 5s
Build and test / Android (aarch64) (push) Failing after 9m40s

Touch multi-selection in the grid, filing a selection into a collection
without a drag, and taking a collection offline from a held row.
This commit is contained in:
2026-08-17 12:27:49 +02:00
7 changed files with 1739 additions and 52 deletions
+82 -1
View File
@@ -3,7 +3,7 @@ import { AdjustPanel, GeometryPanel, ParamRow, TransferPanel } from "adjust.slin
import { LaunchScreen } from "launch.slint";
import { LibraryGrid, LibraryCell, TimelineBar } from "library.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
import { CollectionsPanel, CollectionRow } from "collections.slint";
import { CollectionsPanel, CollectionRow, OfflinePrompt } from "collections.slint";
import { HistogramPanel, HistogramView } from "histogram.slint";
import { SettingsPage } from "settings.slint";
@@ -395,6 +395,20 @@ export component AppWindow inherits Window {
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.
in property <string> offline-prompt-title: "";
in property <string> offline-prompt-detail: "";
in property <string> offline-prompt-keep-label: "";
in property <string> offline-prompt-release-label: "";
in property <bool> offline-prompt-can-keep: false;
in property <bool> offline-prompt-can-release: false;
in property <bool> offline-prompt-busy: false;
callback offline-prompt-keep();
callback offline-prompt-release();
callback offline-prompt-dismiss();
in property <string> library-root-label: "";
in-out property <[TimelineBar]> library-timeline;
in property <string> library-timeline-label: "";
@@ -451,6 +465,27 @@ export component AppWindow inherits Window {
callback collection-toggle(int);
callback collection-new();
callback collection-menu(int);
/// TRACES: FR-NC-6a
/// Ask about a collection's local copies: the tray on a row, or a long
/// press anywhere on it.
callback collection-offline-menu(int);
/// A row is held or released — the long press is timed in Rust.
callback collection-row-press(int, bool);
/// 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();
/// A press on a cell ended, so the long-press timer can be cancelled.
callback library-cell-press-ended();
/// TRACES: FR-UI-2 | FR-UI-4
/// Two taps on a cell: in selection mode, the far end of a range.
callback library-cell-double-clicked(int);
/// Renaming started on a row, by id — a double-click, or `F2`.
callback collection-rename-start(int);
/// A rename was committed: the collection's id and the new name.
@@ -754,6 +789,15 @@ in property <bool> panel-visible: true;
// the inner scopes that use it for something more specific — a
// rename in the sidebar — see it first and accept it there.
if (event.text == Key.Back || event.text == Key.Escape) {
// A dialogue takes "go up one" first, and it is the *only*
// thing that goes up. Without this, Android's Back button
// would leave the library from behind an open question — the
// view changing underneath a modal, which reads as the app
// having lost its place.
if (root.offline-prompt-title != "") {
root.offline-prompt-dismiss();
return accept;
}
return root.back-requested() ? accept : reject;
}
return reject;
@@ -912,6 +956,8 @@ in property <bool> panel-visible: true;
toggle(id) => { root.collection-toggle(id); }
new-collection() => { root.collection-new(); }
row-menu(id) => { root.collection-menu(id); }
offline-menu(id) => { root.collection-offline-menu(id); }
row-press(id, down) => { root.collection-row-press(id, down); }
renaming-id: root.collection-renaming;
rename-start(id) => { root.collection-rename-start(id); }
@@ -1011,6 +1057,16 @@ in property <bool> panel-visible: true;
cell-pressed(i, ctrl, shift) => {
root.library-cell-pressed(i, ctrl, shift);
}
cell-press-ended() => { root.library-cell-press-ended(); }
cell-double-clicked(i) => { root.library-cell-double-clicked(i); }
select-mode: root.library-select-mode;
toggle-select-mode() => { root.library-toggle-select-mode(); }
// The sidebar's rows, not a second model: the sheet files
// into the same tree the sidebar draws.
collections: root.collection-rows;
file-in-collection(id, moves) => {
root.library-file-in-collection(id, moves);
}
cursor: root.library-cursor;
move-cursor(delta, extend) => {
root.library-move-cursor(delta, extend);
@@ -1635,5 +1691,30 @@ in property <bool> panel-visible: true;
indeterminate: !root.activity-determinate;
fraction: root.activity-fraction;
}
// TRACES: FR-NC-6a
// The offline question, over the shell rather than inside the sidebar
// that raises it. The sidebar is 232px wide and closes entirely in the
// compact layout, so a dialogue living in there would be unreadable on
// the device the gesture exists for — and the answer commits gigabytes
// either way, which deserves the width to say so.
//
// Below the load bar, above everything else: a download started from
// here shows its progress in that bar, and the bar must not be the
// thing the dialogue covers.
OfflinePrompt {
width: 100%;
height: 100%;
title: root.offline-prompt-title;
detail: root.offline-prompt-detail;
keep-label: root.offline-prompt-keep-label;
release-label: root.offline-prompt-release-label;
can-keep: root.offline-prompt-can-keep;
can-release: root.offline-prompt-can-release;
busy: root.offline-prompt-busy;
keep() => { root.offline-prompt-keep(); }
release() => { root.offline-prompt-release(); }
dismiss() => { root.offline-prompt-dismiss(); }
}
}
}