Say what the application is doing, in one bar and one list
Every background job reported into a window property of its own — library-thumbs-done, library-pin-total, library-syncing — which only the grid ever read. A pin download that outlived the view it was started from drew nothing at all once the user opened an image, and there was no answer anywhere to "what is this busy with", because the answer was spread across eight properties nothing collected. They report to one register now (ui/dr-ui/src/activity.rs). It publishes an aggregate, which draws a three-pixel bar across the top of the shell in every view, and a row per job, which the settings page lists: scans, thumbnail batches, pin and open downloads, sidecar uploads, the sync and the trash. Failures stay on the list until they are cleared; routine successes do not, or a scroll would bury them. The handle removes a still-running job when it drops, so a worker that dies mid-transfer takes its row with it rather than leaving the bar sweeping for the rest of the session. Also carries in-flight work from a parallel session — the drawn icon set and the dr-pipeline ops split. dr-pipeline's build script does not compile at this commit; ui/dr-ui does, with clippy clean and its tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+57
-105
@@ -9,7 +9,7 @@
|
||||
// must not look identical (FR-NC-6c).
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip } from "widgets.slint";
|
||||
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip, ProgressBar, Icon } from "widgets.slint";
|
||||
|
||||
// One bar of the capture-time histogram.
|
||||
export struct TimelineBar {
|
||||
@@ -259,52 +259,11 @@ export struct LibraryCell {
|
||||
flag: int,
|
||||
}
|
||||
|
||||
// A horizontal progress bar with two modes.
|
||||
//
|
||||
// Determinate where a real denominator exists (thumbnails: we know how many
|
||||
// cells we asked for). Indeterminate where one does not — a directory walk
|
||||
// discovers its own extent, so any percentage would be invented, and inventing
|
||||
// one is worse than admitting the work is unbounded.
|
||||
component ProgressBar inherits Rectangle {
|
||||
in property <float> fraction: 0;
|
||||
in property <bool> indeterminate: false;
|
||||
|
||||
height: 3px;
|
||||
background: Theme.rule;
|
||||
|
||||
// Determinate: a bar proportional to real progress.
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: parent.width * clamp(root.fraction, 0, 1);
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: !root.indeterminate;
|
||||
}
|
||||
|
||||
// Indeterminate: a sweep that says "working" without claiming a position.
|
||||
Rectangle {
|
||||
width: parent.width * 25%;
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: root.indeterminate;
|
||||
|
||||
x: root.indeterminate ? -self.width : 0;
|
||||
animate x {
|
||||
duration: 1200ms;
|
||||
iteration-count: -1;
|
||||
easing: ease-in-out;
|
||||
}
|
||||
states [
|
||||
running when root.indeterminate: { x: parent.width; }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// A row of five stars, readable at a glance and clickable to set a rating.
|
||||
//
|
||||
// **Filled versus empty carries the meaning, not colour.** NFR-A11Y-3 forbids
|
||||
// status by hue alone, and the palette is achromatic anyway — so a set star is
|
||||
// a solid glyph at `active` and an unset one is an outline at `ink-faint`. The
|
||||
// a solid star at `active` and an unset one is an outline at `ink-faint`. The
|
||||
// two differ in both shape and luminance, which survives greyscale and low
|
||||
// vision alike.
|
||||
//
|
||||
@@ -343,7 +302,7 @@ export component StarStrip inherits Rectangle {
|
||||
// Separation between the trash target and ★1.
|
||||
//
|
||||
// **This gap is load-bearing.** The star targets are 44px over a 22px
|
||||
// glyph, so they deliberately overlap and a near-miss lands one star out —
|
||||
// cell, so they deliberately overlap and a near-miss lands one star out —
|
||||
// harmless, same control, corrected by clicking again. That reasoning does
|
||||
// not survive a neighbour that *moves a file*, so trash is held off the
|
||||
// scale by a gap wider than the overhang it would otherwise share with
|
||||
@@ -373,18 +332,16 @@ export component StarStrip inherits Rectangle {
|
||||
height: root.star;
|
||||
visible: root.can-trash;
|
||||
|
||||
Text {
|
||||
text: "🗑";
|
||||
Icon {
|
||||
name: "trash";
|
||||
// Reject and trash are the two destructive ends of this UI and
|
||||
// share the palette's one hue, so the gesture reads the same
|
||||
// in both places (NFR-A11Y-3: the glyph carries it, not the
|
||||
// in both places (NFR-A11Y-3: the shape carries it, not the
|
||||
// colour).
|
||||
color: Theme.warn-ink;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
ink: Theme.warn-ink;
|
||||
size: 13px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
@@ -411,18 +368,16 @@ export component StarStrip inherits Rectangle {
|
||||
width: root.star;
|
||||
height: root.star;
|
||||
|
||||
Text {
|
||||
Icon {
|
||||
// Solid versus outline: the shape says it, not the colour.
|
||||
text: root.rating >= n ? "★" : "☆";
|
||||
color: root.rating >= n ? Theme.active : Theme.ink-dim;
|
||||
// Large enough to hit the difference between ★ and ☆ at arm's
|
||||
// length on a tablet; the glyphs differ in fill, which needs
|
||||
// more pixels to read than a difference in shape would.
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
name: root.rating >= n ? "star" : "star-outline";
|
||||
ink: root.rating >= n ? Theme.active : Theme.ink-dim;
|
||||
// Large enough to hit the difference between filled and empty
|
||||
// at arm's length on a tablet; the two differ in fill, which
|
||||
// needs more pixels to read than a difference in shape would.
|
||||
size: 14px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
// Grown past the drawn star to meet FR-UI-3's 44pt minimum, and
|
||||
@@ -453,10 +408,11 @@ export component StarStrip inherits Rectangle {
|
||||
|
||||
// The pick/reject mark.
|
||||
//
|
||||
// A glyph rather than a colour, for the same NFR-A11Y-3 reason as the stars:
|
||||
// ✓ and ✗ are distinguishable without hue, and a reject reads as a reject in
|
||||
// greyscale. A reject also dims its whole cell, which is the cue that carries
|
||||
// at grid scale — the glyph is confirmation, not the primary signal.
|
||||
// A shape rather than a colour, for the same NFR-A11Y-3 reason as the stars:
|
||||
// a tick and a cross are distinguishable without hue, and a reject reads as a
|
||||
// reject in greyscale. A reject also dims its whole cell, which is the cue
|
||||
// that carries at grid scale — the mark is confirmation, not the primary
|
||||
// signal.
|
||||
export component FlagMark inherits Rectangle {
|
||||
in property <int> flag: 0;
|
||||
|
||||
@@ -467,17 +423,14 @@ export component FlagMark inherits Rectangle {
|
||||
background: Theme.surface;
|
||||
opacity: 0.92;
|
||||
|
||||
Text {
|
||||
text: root.flag == 2 ? "✗" : "✓";
|
||||
Icon {
|
||||
name: root.flag == 2 ? "cross" : "check";
|
||||
// Reject earns the one hue in the palette: it is the destructive end
|
||||
// of the axis and the thing a user must not mistake for a pick.
|
||||
color: root.flag == 2 ? Theme.warn-ink : Theme.active;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
ink: root.flag == 2 ? Theme.warn-ink : Theme.active;
|
||||
size: 9px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,10 +488,9 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// column-count change. Rust resizes the loaded window to match.
|
||||
callback capacity-changed(int);
|
||||
|
||||
// Thumbnail progress. Unlike the scan, this has a real denominator — the
|
||||
// number of cells we asked for — so the bar can be honest about position.
|
||||
in property <int> thumbs-done: 0;
|
||||
in property <int> thumbs-total: 0;
|
||||
// Thumbnail progress is no longer reported here: the batch belongs to a
|
||||
// window of the grid rather than to anything the user asked for, and the
|
||||
// shell's bar now carries it along with every other running job.
|
||||
|
||||
// Whole-library indexing, which runs for far longer than one window's
|
||||
// thumbnails and is reported separately so the two do not fight over the
|
||||
@@ -549,9 +501,6 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <bool> syncing: false;
|
||||
property <bool> sweeping: root.sweep-total > 0 && root.sweep-done < root.sweep-total;
|
||||
|
||||
property <bool> thumbs-running: root.thumbs-total > 0
|
||||
&& root.thumbs-done < root.thumbs-total;
|
||||
|
||||
callback cell-clicked(int);
|
||||
/// A star was clicked on a cell: row, and the rating 0..5.
|
||||
callback cell-rated(int, int);
|
||||
@@ -743,7 +692,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// interface with a collapsible sidebar puts one, and the only
|
||||
// place that stays put whichever way the panel is.
|
||||
IconButton {
|
||||
glyph: "☰";
|
||||
icon: "menu";
|
||||
active: root.collections-visible;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.toggle-collections(); }
|
||||
@@ -825,7 +774,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
// much more expensive request, and a button that meant either
|
||||
// depending on invisible state would be a trap.
|
||||
if root.scope-label != "": Button {
|
||||
text: root.scope-pinned ? "Pinned ✓" : "Pin offline";
|
||||
// No tick on the pinned label: `active` already inverts
|
||||
// the button, which says the same thing without a symbol
|
||||
// inside a string (see icons.slint).
|
||||
text: root.scope-pinned ? "Pinned" : "Pin offline";
|
||||
active: root.scope-pinned;
|
||||
enabled: !root.scanning;
|
||||
y: (parent.height - self.height) / 2;
|
||||
@@ -908,7 +860,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
|
||||
for n[i] in [1, 2, 3, 4, 5]: FilterChip {
|
||||
label: "★" + n + "+";
|
||||
icon: "star";
|
||||
label: n + "+";
|
||||
count: root.rating-counts.length > n ? root.rating-counts[n] : -1;
|
||||
active: root.filter-min-rating == n;
|
||||
y: (parent.height - self.height) / 2;
|
||||
@@ -923,7 +876,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
FilterChip {
|
||||
label: "✓ Picks";
|
||||
icon: "check";
|
||||
label: "Picks";
|
||||
active: root.filter-flag == 1;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
@@ -932,7 +886,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
|
||||
FilterChip {
|
||||
label: "✗ Rejects";
|
||||
icon: "cross";
|
||||
label: "Rejects";
|
||||
active: root.filter-flag == 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
@@ -978,23 +933,20 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
// --- progress -----------------------------------------------------
|
||||
//
|
||||
// Indeterminate during the scan: a directory walk cannot know its own
|
||||
// extent, so a percentage would be fiction. Determinate for
|
||||
// thumbnails, where the denominator is the cells we requested.
|
||||
if root.scanning || root.thumbs-running || root.sweeping: ProgressBar {
|
||||
// Only the sweep has a denominator worth showing. A scan cannot
|
||||
// know its extent, and the window's own fetches are sized by the
|
||||
// viewport rather than by anything meaningful to the user.
|
||||
indeterminate: root.scanning || (!root.sweeping && root.thumbs-running);
|
||||
fraction: root.sweep-total > 0
|
||||
? root.sweep-done / root.sweep-total
|
||||
: 0;
|
||||
}
|
||||
// The bar that used to sit here is now the shell's, drawn across the
|
||||
// top of every view from the activity register (see `activity.rs`).
|
||||
// Two reasons it moved. It only ever knew about the three things the
|
||||
// grid happens to report — a download running while the user was in
|
||||
// develop drew nothing anywhere — and a second bar here would now say
|
||||
// the same thing twice, one line apart.
|
||||
//
|
||||
// The grid keeps its *words*: "indexing 4000 / 17000" in the header
|
||||
// above says which work is running, which a bar cannot.
|
||||
|
||||
// --- pinning ------------------------------------------------------
|
||||
//
|
||||
// Its own line rather than the shared progress bar above: that one is
|
||||
// driven by the scan and the sweep, and a pin runs alongside both.
|
||||
// Its own line, with the count spelled out: the shell's bar says that
|
||||
// something is transferring, and this says how much of what.
|
||||
if root.pin-total > 0: Rectangle {
|
||||
height: 34px;
|
||||
background: Theme.surface;
|
||||
@@ -1464,8 +1416,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
// hidden: the cull is reversible, and a photo
|
||||
// that vanished on one keypress would make the
|
||||
// gesture frightening to use. Dimming is the
|
||||
// cue that reads at grid scale — the ✗ glyph
|
||||
// confirms it up close.
|
||||
// cue that reads at grid scale — the cross on
|
||||
// the mark confirms it up close.
|
||||
opacity: cell.lifted ? 0.25
|
||||
: (cell.flag == 2 ? 0.4 : 1.0);
|
||||
animate opacity { duration: 120ms; }
|
||||
|
||||
Reference in New Issue
Block a user