Say a photograph is downloading, and how far, instead of failing
The develop view reported a remote original on its way through the error message, so it read "Could not load image" over "Downloading…". It did so on every step along the roll, including a cached frame that was ready within a tick, so each step flashed the error. Waiting is now its own state. On the step, the grid's thumbnail of the photograph stands in at once. Only when a transfer is really on the wire does it dim under "Not on this device yet", with a line like "Downloading — 12.4 of 38.0 MB" and a progress bar. The bytes come from a new RemoteBackend::get_reporting. The Nextcloud backend overrides it to read the body chunk by chunk; the default reports once at the end. Progress is kept in the in-flight registry by path, because a step usually lands on a frame the prefetcher is already fetching. The catalog's file length stands in when the server sends no Content-Length.
This commit is contained in:
+56
-15
@@ -9,7 +9,7 @@ import { LaunchScreen } from "launch.slint";
|
||||
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip, Library } from "library.slint";
|
||||
import { GestureRow, GestureSheet } from "gestures.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow, Scrolling, ScrollBar } from "widgets.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, WaitingState, ProgressBar, ActivityRow, Scrolling, ScrollBar } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
|
||||
MembershipSheet, MembershipRow } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView, Levels } from "histogram.slint";
|
||||
@@ -150,6 +150,32 @@ export component AppWindow inherits Window {
|
||||
in property <int> total: 0;
|
||||
in property <string> load-error: "";
|
||||
|
||||
/// TRACES: FR-NC-6a
|
||||
/// A photograph has been asked for and is not on the canvas yet — it is
|
||||
/// being read from the cache, or downloaded, or decoded. The previous
|
||||
/// frame is gone from view by then; the grid's thumbnail stands in.
|
||||
in property <bool> load-pending: false;
|
||||
/// And it is not on this device: what has arrived, as a sentence
|
||||
/// ("12.4 of 38.0 MB"). Empty while it is only being read from disk, which
|
||||
/// is too quick to be worth a headline.
|
||||
///
|
||||
/// Its own property rather than a message in `load-error`, which is where
|
||||
/// it used to live — and so the view headed a download that was going
|
||||
/// fine with "Could not load image", on every step, cached or not.
|
||||
in property <string> load-waiting: "";
|
||||
/// How much of it, 0..1, or below zero while the size is not known yet.
|
||||
in property <float> load-fraction: -1;
|
||||
/// The grid's thumbnail of the photograph being fetched, shown dimmed
|
||||
/// behind the bar, so stepping along the roll moves from picture to
|
||||
/// picture rather than through an empty frame.
|
||||
in property <image> load-preview;
|
||||
in property <bool> has-load-preview: false;
|
||||
|
||||
/// A photograph is on the canvas and is the one named: not an empty
|
||||
/// folder, not a failure, not a download still coming in. Everything that
|
||||
/// acts on the picture is gated on this.
|
||||
property <bool> has-photo: root.total > 0 && root.load-error == "" && !root.load-pending;
|
||||
|
||||
/// TRACES: FR-CULL-3
|
||||
/// The focus marks themselves, and whether they describe *this* frame.
|
||||
/// Rust's, because they come from a compute pass — see `peaking.slint` for
|
||||
@@ -1061,7 +1087,7 @@ in property <bool> panel-visible: true;
|
||||
root.export-from-sheet();
|
||||
return accept;
|
||||
}
|
||||
if (root.copy-sheet-open && root.total > 0 && root.load-error == "") {
|
||||
if (root.copy-sheet-open && root.has-photo) {
|
||||
root.copy-sheet-open = false;
|
||||
Transfer.copy();
|
||||
return accept;
|
||||
@@ -1733,7 +1759,7 @@ in property <bool> panel-visible: true;
|
||||
x: 0;
|
||||
y: 0;
|
||||
height: parent.height;
|
||||
enabled: root.total > 0 && root.load-error == "";
|
||||
enabled: root.has-photo;
|
||||
mode: Develop.view-mode;
|
||||
picked(m) => { root.mode-picked(m); }
|
||||
|
||||
@@ -1792,7 +1818,7 @@ in property <bool> panel-visible: true;
|
||||
image-rendering: root.magnified
|
||||
? ImageRendering.pixelated
|
||||
: ImageRendering.smooth;
|
||||
visible: root.total > 0 && root.load-error == "";
|
||||
visible: root.has-photo;
|
||||
}
|
||||
|
||||
// TRACES: FR-DSP-4
|
||||
@@ -1837,7 +1863,7 @@ in property <bool> panel-visible: true;
|
||||
// it is a diagnostic and not an edit: it must not reach
|
||||
// the histogram, an export, or the texture the develop
|
||||
// pass hands the compositor.
|
||||
if root.overlay-on && !Masking.overlay-hidden && root.total > 0: Image {
|
||||
if root.overlay-on && !Masking.overlay-hidden && root.has-photo: Image {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -1867,7 +1893,7 @@ in property <bool> panel-visible: true;
|
||||
// The focus marks, over the same fitted rect. See
|
||||
// `peaking.slint` for why they are a layer over the canvas
|
||||
// rather than a tint in it.
|
||||
if root.focus-overlay-ready && root.total > 0: FocusMarks {
|
||||
if root.focus-overlay-ready && root.has-photo: FocusMarks {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -1900,6 +1926,21 @@ in property <bool> panel-visible: true;
|
||||
: "Pass a folder or file on the command line.";
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// Waiting for an original. Not an empty state and not an
|
||||
// error: the photograph is known, and the grid's thumbnail
|
||||
// of it stands in so a step along the roll lands on *this*
|
||||
// frame at once — with a bar, once it turns out to be a
|
||||
// download, saying how long until it can be worked on.
|
||||
if root.total > 0 && root.load-error == "" && root.load-pending: WaitingState {
|
||||
headline: "Not on this device yet";
|
||||
detail: root.load-waiting;
|
||||
fraction: root.load-fraction;
|
||||
what: "Downloading " + root.filename;
|
||||
preview: root.load-preview;
|
||||
has-preview: root.has-load-preview;
|
||||
}
|
||||
|
||||
// --- zoom and pan ------------------------------------------
|
||||
//
|
||||
// Below the crop overlay in z-order so that, in crop mode, the
|
||||
@@ -1935,7 +1976,7 @@ in property <bool> panel-visible: true;
|
||||
// Anchored on the midpoint between the fingers, which is
|
||||
// what makes a pinch feel like it is moving the picture
|
||||
// rather than the viewport.
|
||||
if root.total > 0 && root.load-error == "": ScaleRotateGestureHandler {
|
||||
if root.has-photo: ScaleRotateGestureHandler {
|
||||
x: 0; y: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1982,7 +2023,7 @@ in property <bool> panel-visible: true;
|
||||
// it.** `GradientHandles` further down is the other end of
|
||||
// the same rule, and is why dragging a handle has always
|
||||
// worked while everything between it and here did not.
|
||||
if root.total > 0 && root.load-error == "": TouchArea {
|
||||
if root.has-photo: TouchArea {
|
||||
x: 0; y: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -2073,7 +2114,7 @@ in property <bool> panel-visible: true;
|
||||
// picking wants a click, and interleaving the two in one
|
||||
// handler is how a drag ends up selecting a region the
|
||||
// user was only scrolling past.
|
||||
if root.region-picking && root.total > 0 && root.load-error == "": pick := TouchArea {
|
||||
if root.region-picking && root.has-photo: pick := TouchArea {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -2111,7 +2152,7 @@ in property <bool> panel-visible: true;
|
||||
// two want the same press, and `region-picking` is false
|
||||
// whenever this is armed so only one of them exists at a
|
||||
// time.
|
||||
if root.painting && root.total > 0 && root.load-error == "": paint := TouchArea {
|
||||
if root.painting && root.has-photo: paint := TouchArea {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -2154,7 +2195,7 @@ in property <bool> panel-visible: true;
|
||||
// Below the circles declared further down, so a press that
|
||||
// lands on an existing repair takes hold of it instead of
|
||||
// making another one on top.
|
||||
if Develop.repairing && root.total > 0 && root.load-error == "": TouchArea {
|
||||
if Develop.repairing && root.has-photo: TouchArea {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -2185,7 +2226,7 @@ in property <bool> panel-visible: true;
|
||||
// were recorded the stack would fill with a hundred
|
||||
// temperatures nobody chose. The arming state is what says
|
||||
// the next click will do something instead.
|
||||
if root.sampling && root.total > 0 && root.load-error == "": TouchArea {
|
||||
if root.sampling && root.has-photo: TouchArea {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
@@ -2211,7 +2252,7 @@ in property <bool> panel-visible: true;
|
||||
//
|
||||
// Placed over the fitted image, so the fractions it draws are
|
||||
// fractions of the picture. See `CropOverlay` in crop.slint.
|
||||
if Develop.cropping && root.total > 0 && root.load-error == "": CropOverlay {
|
||||
if Develop.cropping && root.has-photo: CropOverlay {
|
||||
x: canvas-area.shown-x;
|
||||
y: canvas-area.shown-y;
|
||||
width: canvas-area.shown-w;
|
||||
@@ -2813,7 +2854,7 @@ in property <bool> panel-visible: true;
|
||||
// window away from the thing being confirmed is a
|
||||
// confirmation you take on trust. The rail is now the
|
||||
// second way out rather than the first.
|
||||
if root.total > 0 && root.load-error == "": HorizontalLayout {
|
||||
if root.has-photo: HorizontalLayout {
|
||||
// **Clear of the photo roll, or these do nothing.**
|
||||
//
|
||||
// The roll's swipe handler consumes every press that
|
||||
@@ -3415,7 +3456,7 @@ in property <bool> panel-visible: true;
|
||||
scope-kinds: root.copy-scope-kinds;
|
||||
scope-empty: root.copy-scope-empty;
|
||||
summary: Transfer.summary;
|
||||
can-copy: root.total > 0 && root.load-error == "";
|
||||
can-copy: root.has-photo;
|
||||
scope-toggled(name) => { root.copy-scope-toggled(name); }
|
||||
copy => {
|
||||
root.copy-sheet-open = false;
|
||||
|
||||
@@ -987,6 +987,63 @@ export component EmptyState inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// What a view shows while the thing it will show is still on its way.
|
||||
//
|
||||
// The third answer beside `EmptyState`'s two. A photograph being downloaded is
|
||||
// neither missing nor broken, and the develop view used to put it under the
|
||||
// error heading — "Could not load image", then "Downloading…" — which reads as
|
||||
// a failure followed by a retry. This one shows what is already in hand (the
|
||||
// grid's thumbnail) so the step lands on *this* photograph at once, and, only
|
||||
// once there is a real wait (`detail` set), dims it under a headline, how far
|
||||
// along, and a bar. A read from disk never gets that far, so it never
|
||||
// flashes text.
|
||||
export component WaitingState inherits Rectangle {
|
||||
in property <string> headline;
|
||||
/// Empty while the wait is too short to talk about.
|
||||
in property <string> detail;
|
||||
/// 0..1, or below zero while the size is not known.
|
||||
in property <float> fraction: -1;
|
||||
/// What the bar is announced as.
|
||||
in property <string> what;
|
||||
in property <image> preview;
|
||||
in property <bool> has-preview: false;
|
||||
|
||||
if root.has-preview: Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: root.preview;
|
||||
image-fit: contain;
|
||||
opacity: root.detail != "" ? 0.4 : 1;
|
||||
}
|
||||
|
||||
if root.detail != "": VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
text: root.headline;
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text-lg;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: root.detail;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
ProgressBar {
|
||||
width: min(240px, root.width * 60%);
|
||||
fraction: root.fraction;
|
||||
indeterminate: root.fraction < 0;
|
||||
label: root.what;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A scrolling list that only builds the rows you can see.
|
||||
//
|
||||
// # Why this exists rather than `Flickable { VerticalLayout { for … } }`
|
||||
|
||||
Reference in New Issue
Block a user