Select photographs with a finger, and take a collection with you
Two things a tablet could not do. Both existed for a pointer and had no touch form at all, which on Android meant the collection sidebar was somewhere to look at rather than somewhere to file into. **Selecting more than one.** Ctrl-click and shift-click are the only ways into a multi-selection, and touch has neither. Holding a cell now enters selection mode, where a tap toggles — reported to Rust as a ctrl-press, so it goes through the same `apply_press` as everything else rather than growing a second copy of the selection rules. A double tap takes the run between where selecting began and there: the touch form of shift-click, and the reason the anchor from *before* the double tap has to be remembered, since both of its taps move the anchor onto the cell being tapped. A "Select" button does the same thing where a gesture would go undiscovered (FR-UI-4). **Filing without a drag.** A one-finger drag beginning in the grid belongs to the Flickable that scrolls it — that is the arbitration working, not a bug to route around — so the selection can now be filed from a sheet listing the sidebar's own rows. Copy by default, as the drag has always been; moving out of the collection being shown is a switch, because it is the one that takes something away. **Taking a collection offline.** The machinery was there and reachable only by scoping the grid to a collection and finding a button behind a disclosure. Holding a collection's name now asks the question directly, and the tray on a row and the header button ask the same one — three affordances doing two different things is how a user comes to avoid all three. The question is asked rather than a toggle flipped because both answers are expensive: one downloads gigabytes, the other deletes them, and the counts and sizes go in the buttons where they are read before the tap. `Cache::release` is new and is the destructive half `unpin` deliberately is not. "Remove the local copies" is asked by someone whose device is full, and withdrawing a promise while leaving the bytes for a future eviction to notice is not an answer to it. It unpins before forgetting, or the next pin fetch would dutifully download everything it just deleted. The sidebar's trays read `tier_actual`, never `tier_desired`: the question is whether these will open on the aeroplane, and a pin whose download has not run yet answers no. TRACES: FR-CAT-7 | FR-NC-6a | FR-NC-6b | FR-NC-6c | FR-UI-2 | FR-UI-3 | FR-UI-4 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+82
-1
@@ -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.
|
||||
@@ -753,6 +788,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;
|
||||
@@ -910,6 +954,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); }
|
||||
@@ -1009,6 +1055,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);
|
||||
@@ -1633,5 +1689,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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user