Put the two collection actions in the collections panel

"Keep offline" and "Change library" were buttons in the library
header, in a row that is otherwise entirely library-wide. Both refer
to the tree instead.

"Keep offline" could only ever mean the scoped collection, while
sitting nowhere near the tree that says which that is — and while the
sidebar already offered the same question twice, on each row's tray
and on a held row. It now sits under the tree, in the panel whose
selection decides what it acts on, in the same place and shape the
trash already gives Restore and Empty. It stays a labelled control
rather than being dropped for the tray: a 26px row's tray is a small
thing to hit, and "Kept offline" spelt out for the collection you are
looking at is the discoverable version.

"Change library" was wedged between Sync and Rescan, two buttons that
act on the library you already have. Reading it as one of that group
is a way to lose a scan by aiming badly. It is now the last thing in
the panel, under the tree it replaces wholesale, behind a rule.

On a tablet both are one tap further away, behind the sidebar toggle
that leads the header. That is the panel a user is already in when
they scope the grid to a collection, so it is where they are when
either of these becomes the thing they want.

The grid keeps `pin-done`/`pin-total` and its progress bar: the
transfer is worth reporting wherever it was started from, including a
row the grid is not scoped to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 13:19:53 +02:00
co-authored by Claude Opus 5
parent f4fbabf18b
commit c7e1822f77
5 changed files with 120 additions and 79 deletions
+10 -3
View File
@@ -1437,6 +1437,16 @@ in property <bool> panel-visible: true;
total-images: root.library-total;
error: root.collection-error;
// TRACES: FR-NC-6a
// The two actions that were in the library header and
// 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(); }
select(id) => { root.collection-select(id); }
toggle(id) => { root.collection-toggle(id); }
new-collection() => { root.collection-new(); }
@@ -1505,10 +1515,8 @@ in property <bool> panel-visible: true;
collections-visible: root.collections-visible;
toggle-collections() => { root.toggle-collections(); }
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;
@@ -1566,7 +1574,6 @@ in property <bool> panel-visible: true;
viewport-cells-changed(n) => { root.library-viewport-cells(n); }
cell-clicked(i) => { root.library-cell-clicked(i); }
rescan() => { root.library-rescan(); }
change-library() => { root.library-change(); }
can-import: root.import-supported;
open-import() => { root.import-open(); }
open-settings() => { root.settings-open(); }
+65
View File
@@ -349,6 +349,32 @@ export component CollectionsPanel inherits Rectangle {
in property <int> selected-id: 0;
in property <int> total-images: 0;
in property <string> error: "";
/// Whether a scan is running, which is when neither of the two actions at
/// the foot of this panel can be honoured: pinning walks a collection the
/// scan is still writing, and changing library pulls the catalog out from
/// under it.
in property <bool> scanning: false;
// --- what to do with the collection this panel has scoped --------------
//
// TRACES: FR-NC-6a
// These two used to be buttons in the library header, where they were the
// only things in a row of library-wide actions that referred to *one
// collection* — and where "Keep offline" could only ever mean the scoped
// one while sitting nowhere near the tree that says which that is.
//
// Here they are under the tree, in the panel whose selection decides what
// they act on, and in the same place and shape the trash already puts its
// own two. The header keeps only what belongs to the whole library.
/// TRACES: FR-NC-6a
/// Whether the scoped collection is being kept on this device.
in property <bool> scope-pinned: false;
/// TRACES: FR-NC-6a
/// Ask about the scoped collection's local copies — the same question the
/// tray on a row and a held row open.
callback toggle-pin-scope();
/// Open a different library altogether.
callback change-library();
callback select(int);
callback toggle(int);
@@ -567,6 +593,29 @@ export component CollectionsPanel inherits Rectangle {
}
}
// TRACES: FR-NC-6a
// Keep the scoped collection on this device.
//
// Only while one is scoped: "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. Ids at or below zero
// are "All photographs" and the trash, neither of which is a
// collection to pin.
//
// No tick on the kept label: `active` already inverts the button,
// which says the same thing without a symbol inside a string.
//
// It opens the same question the tray on a row and a held row open,
// rather than pinning outright — see the note on the callback in
// `library_ui::wire`. This is the version with a word on it, for the
// collection the grid is actually showing.
if root.selected-id > 0: Button {
text: root.scope-pinned ? "Kept offline" : "Keep offline";
active: root.scope-pinned;
enabled: !root.scanning;
clicked => { root.toggle-pin-scope(); }
}
// --- trash --------------------------------------------------------
//
// TRACES: FR-CAT-15
@@ -669,6 +718,22 @@ export component CollectionsPanel inherits Rectangle {
font-size: Theme.text-sm;
wrap: word-wrap;
}
// The foot of the panel, under everything the current library holds,
// because it is the one control here that replaces all of it.
//
// It was in the library header, wedged between Sync and Rescan — two
// buttons that act on the library you already have. Reading "Change
// library" as one of that group is a way to lose a scan by aiming
// badly. Down here it is the last thing in the panel and the only
// thing below the tree it would replace.
Rectangle { height: 1px; background: Theme.rule; }
Button {
text: "Change library";
enabled: !root.scanning;
clicked => { root.change-library(); }
}
}
// Right edge, separating the panel from the grid.
+14 -45
View File
@@ -845,8 +845,12 @@ export component FlagMark inherits Rectangle {
// moment a hand was already travelling toward one — and splitting "what can I
// do with these?" across two bars at opposite ends of the window. A row that
// only changes when the *library* changes can be aimed at from memory.
//
// Nor does anything in here refer to *one collection*. "Keep offline" and
// "Change library" are in the collections panel, under the tree whose
// selection decides what the first of them acts on and which the second
// replaces wholesale.
component HeaderActions inherits HorizontalLayout {
in property <string> scope-label;
/// TRACES: FR-UI-2 | FR-UI-4
/// Whether taps are selecting rather than opening. The non-gesture half of
/// touch multi-selection: the long press is the quick way in, and this is
@@ -854,15 +858,12 @@ component HeaderActions inherits HorizontalLayout {
in property <bool> select-mode: false;
in property <bool> scanning: false;
in property <bool> syncing: false;
in property <bool> scope-pinned: false;
/// Centres each button in a 44px header. Off in the disclosure row, which
/// is sized to its content.
in property <bool> centred: true;
in property <length> row-height: 44px;
callback toggle-select-mode();
callback change-library();
callback toggle-pin-scope();
callback sync-now();
/// TRACES: FR-CAT-10
/// Whether this platform can reach a card at all (`dr_plat::imports_supported`).
@@ -903,31 +904,6 @@ component HeaderActions inherits HorizontalLayout {
clicked => { root.toggle-select-mode(); }
}
if !root.scanning: Button {
text: "Change library";
y: root.centred ? (root.row-height - self.height) / 2 : 0;
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 {
// No tick on the kept label: `active` already inverts the button,
// which says the same thing without a symbol inside a string.
//
// It opens the same question the sidebar's tray and a held row open,
// rather than pinning outright — see the note on the callback in
// `library_ui::wire`.
text: root.scope-pinned ? "Kept offline" : "Keep offline";
active: root.scope-pinned;
enabled: !root.scanning;
y: root.centred ? (root.row-height - self.height) / 2 : 0;
clicked => { root.toggle-pin-scope(); }
}
Button {
// Shares the finished index so a second device inherits it rather
// than repeating hours of range fetches.
@@ -1154,8 +1130,6 @@ export component LibraryGrid inherits Rectangle {
callback toggle-collections();
callback rescan();
/// Back to the launch screen, to change library or account.
callback change-library();
/// Open the settings page.
/// TRACES: FR-CAT-10
/// Whether this platform can import at all. Forwarded to the header, which
@@ -1384,14 +1358,17 @@ export component LibraryGrid inherits Rectangle {
// --- 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;
// 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.
//
// Only the progress. Asking for the pin, and saying whether the scoped
// collection already has one, belong to the collections panel — the tree
// is what decides which collection is meant. This bar reports on the
// transfer wherever it was started, including from a row this grid is not
// scoped to, which is why it is not gated on the scope.
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;
@@ -1768,15 +1745,11 @@ export component LibraryGrid inherits Rectangle {
// is room, and a disclosure would be a click in front of a
// button that was already visible.
if root.expanded: HeaderActions {
scope-label: root.scope-label;
scanning: root.scanning;
syncing: root.syncing;
scope-pinned: root.scope-pinned;
can-import: root.can-import;
select-mode: root.select-mode;
toggle-select-mode => { root.toggle-select-mode(); }
change-library => { root.change-library(); }
toggle-pin-scope => { root.toggle-pin-scope(); }
sync-now => { root.sync-now(); }
rescan => { root.rescan(); }
open-import => { root.open-import(); }
@@ -1828,15 +1801,11 @@ export component LibraryGrid inherits Rectangle {
HeaderActions {
centred: true;
scope-label: root.scope-label;
scanning: root.scanning;
syncing: root.syncing;
scope-pinned: root.scope-pinned;
can-import: root.can-import;
select-mode: root.select-mode;
toggle-select-mode => { root.toggle-select-mode(); }
change-library => { root.change-library(); }
toggle-pin-scope => { root.toggle-pin-scope(); }
sync-now => { root.sync-now(); }
rescan => { root.rescan(); }
open-import => { root.open-import(); }