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
+27
View File
@@ -376,6 +376,33 @@ impl RemoteBackend for NextcloudBackend {
Ok(body)
}
async fn get_reporting(
&self,
id: &RemoteId,
progress: &(dyn Fn(u64, Option<u64>) + Send + Sync),
) -> Result<Vec<u8>, RemoteError> {
let url = self.url_for_id(id)?;
let mut resp = self
.client
.get(&url)
.basic_auth(&self.login, Some(&self.password))
.send()
.await
.map_err(map_send_error)?;
map_status(resp.status(), &url)?;
// Read chunk by chunk rather than with `bytes()`, which is the same
// transfer with nothing to say until it ends.
let declared = resp.content_length();
let mut body = Vec::with_capacity(declared.unwrap_or(0) as usize);
progress(0, declared);
while let Some(chunk) = resp.chunk().await.map_err(map_send_error)? {
body.extend_from_slice(&chunk);
progress(body.len() as u64, declared);
}
Ok(body)
}
async fn put(
&self,
path: &RemotePath,
+20
View File
@@ -98,6 +98,26 @@ pub trait RemoteBackend: Send + Sync {
/// either way; [`Capabilities::range_reads`] says whether it was cheap.
async fn get(&self, id: &RemoteId, range: Option<Range<u64>>) -> Result<Vec<u8>, RemoteError>;
/// Fetch a whole object, saying how much of it has arrived as it arrives.
///
/// `progress` is called with the bytes received so far and the length the
/// server declared, if it declared one. For the one transfer a person
/// watches: an original opened in develop is tens of megabytes, and a
/// view that can only say "downloading" for that long reads as stuck.
///
/// The default fetches with [`get`](Self::get) and reports once, at the
/// end — right for a backend whose `get` is a local read, where there is
/// no wait to report on.
async fn get_reporting(
&self,
id: &RemoteId,
progress: &(dyn Fn(u64, Option<u64>) + Send + Sync),
) -> Result<Vec<u8>, RemoteError> {
let body = self.get(id, None).await?;
progress(body.len() as u64, Some(body.len() as u64));
Ok(body)
}
/// Upload, optionally guarded by a precondition.
///
/// Backends handle chunking internally based on body size — chunked