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
+40 -2
View File
@@ -411,6 +411,12 @@ fn is_supported(p: &Path) -> bool {
/// rect chosen for the first — or, since local masking became a mode, would
/// open the next photograph with a mask stack it does not have.
fn reset_view_state(window: &AppWindow) {
// Whatever the last open was waiting for, this one is not — until the
// remote path says otherwise.
window.set_load_pending(false);
window.set_load_waiting("".into());
window.set_load_fraction(-1.0);
window.set_has_load_preview(false);
window.global::<Develop>().set_view_mode(ViewMode::Photo);
window.set_zoom(1.0);
window.set_zoomed(false);
@@ -2769,7 +2775,17 @@ fn wire_remote_open(
log::info!("fetching {path} for develop");
w.set_load_error("Downloading…".into());
// TRACES: FR-NC-6a
// The grid's thumbnail stands in from this moment, so the step
// lands on this photograph rather than on the last one's pixels
// or an empty frame. Whether to say anything about a download is
// decided below, once one is actually running.
let (preview, size) = library.preview_for_path(&w, &path);
if let Some(p) = preview {
w.set_load_preview(p);
w.set_has_load_preview(true);
}
w.set_load_pending(true);
let rx = library::spawn_full_fetch(conn, path.clone(), cache);
@@ -2803,7 +2819,26 @@ fn wire_remote_open(
slint::TimerMode::Repeated,
std::time::Duration::from_millis(50),
move || {
let Ok(got) = rx.try_recv() else { return };
let Ok(got) = rx.try_recv() else {
// TRACES: FR-NC-6a
// Still coming. Said only once the original is on the
// wire — a read from the cache lands before there is
// a transfer to find, and so never flashes a headline.
// Read by path, because the transfer is as often the
// prefetcher's as this open's own.
if current.get() == mine {
if let (Some(w), Some((received, declared))) =
(weak.upgrade(), library::transfer_progress(&path))
{
let (text, fraction) =
activity::describe_download(received, declared.or(size));
w.set_load_waiting(text.as_str().into());
w.set_load_fraction(fraction);
job.detail(text);
}
}
return;
};
// Landed — this timer has done its job.
held.stop();
let Some(w) = weak.upgrade() else { return };
@@ -2819,6 +2854,9 @@ fn wire_remote_open(
log::debug!("{name}: landed after the view moved on");
return;
}
w.set_load_pending(false);
w.set_load_waiting("".into());
w.set_has_load_preview(false);
let bytes = match got {
Ok(b) => b,