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:
2026-09-26 11:02:11 -04:00
parent 3b97195b37
commit 4bec01eaf1
9 changed files with 380 additions and 63 deletions
+44
View File
@@ -415,6 +415,31 @@ pub fn describe_bytes(bytes: u64) -> String {
}
}
/// TRACES: FR-NC-6a
/// A download someone is watching, as the line under its bar and how full the
/// bar is: `received` of `total` bytes, and a fraction in 0..1 — or below
/// zero when there is no total, which the bar draws as indeterminate rather
/// than as a position it would have to invent.
pub fn describe_download(received: u64, total: Option<u64>) -> (String, f32) {
match total.filter(|&t| t > 0) {
// Nothing yet: the size alone says what the wait is for, where
// "0 kB of 38.0 MB" would read as a transfer that has stalled.
Some(t) if received == 0 => (format!("Downloading {}", describe_bytes(t)), 0.0),
// A file that grew since the scan measured it can overrun the
// catalog's length; the bar stops full rather than past its end.
Some(t) => (
format!(
"Downloading — {} of {}",
describe_bytes(received),
describe_bytes(t.max(received))
),
(received as f64 / t as f64).min(1.0) as f32,
),
None if received > 0 => (format!("Downloading — {}", describe_bytes(received)), -1.0),
None => ("Downloading…".to_string(), -1.0),
}
}
/// A running job, held by whatever is reporting on it.
///
/// Every method is idempotent and every one is a no-op once the job has
@@ -498,6 +523,25 @@ impl Drop for Activity {
mod tests {
use super::*;
#[test]
fn a_download_reads_as_what_it_knows() {
let mb = 1024 * 1024;
assert_eq!(describe_download(0, None), ("Downloading…".to_string(), -1.0));
assert_eq!(
describe_download(0, Some(38 * mb)),
("Downloading 38.0 MB".to_string(), 0.0)
);
let (text, fraction) = describe_download(19 * mb, Some(38 * mb));
assert_eq!(text, "Downloading — 19.0 MB of 38.0 MB");
assert_eq!(fraction, 0.5);
let (text, fraction) = describe_download(12 * mb, None);
assert_eq!(text, "Downloading — 12.0 MB");
assert!(fraction < 0.0, "no total, no position");
let (text, fraction) = describe_download(40 * mb, Some(38 * mb));
assert_eq!(text, "Downloading — 40.0 MB of 40.0 MB");
assert_eq!(fraction, 1.0, "an overrun stops full");
}
#[test]
fn a_running_job_makes_the_bar_busy() {
let log = ActivityLog::new();