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:
@@ -142,11 +142,20 @@ pub struct CollectionsController {
|
||||
/// the tree, the grid cells — and doing that from inside the `dropped`
|
||||
/// handler destroys the elements Slint is still using to deliver the event.
|
||||
dropped_on: RefCell<Option<CollectionId>>,
|
||||
/// Where the trash workers report what they are doing.
|
||||
///
|
||||
/// Shared with [`crate::library_ui`]: a delete and a scan are two jobs in
|
||||
/// one list, and the user asking what the application is busy with does not
|
||||
/// care which module started them.
|
||||
activity: Rc<crate::activity::ActivityLog>,
|
||||
}
|
||||
|
||||
impl CollectionsController {
|
||||
pub fn new() -> Rc<Self> {
|
||||
Rc::new(Self::default())
|
||||
pub fn new(activity: Rc<crate::activity::ActivityLog>) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
activity,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Which collection the grid is scoped to, for [`crate::library_ui`] to
|
||||
@@ -786,6 +795,7 @@ fn start_trash(
|
||||
// survive as a set of ids the user can no longer see.
|
||||
ctl.clear_selection();
|
||||
|
||||
let count = moves.len();
|
||||
let rx = crate::trash::spawn_move(
|
||||
creds,
|
||||
sess.user_id.clone(),
|
||||
@@ -800,6 +810,7 @@ fn start_trash(
|
||||
catalog.clone(),
|
||||
rx,
|
||||
reload.clone(),
|
||||
format!("Moving {count} photograph(s) to the trash"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -861,6 +872,7 @@ fn start_restore(
|
||||
// would survive as ids the user can no longer see.
|
||||
ctl.clear_selection();
|
||||
|
||||
let count = moves.len();
|
||||
let rx = crate::trash::spawn_move(
|
||||
creds,
|
||||
sess.user_id.clone(),
|
||||
@@ -875,6 +887,7 @@ fn start_restore(
|
||||
catalog.clone(),
|
||||
rx,
|
||||
reload.clone(),
|
||||
format!("Restoring {count} photograph(s)"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -888,12 +901,22 @@ fn drain_trash(
|
||||
catalog: Rc<RefCell<Option<Catalog>>>,
|
||||
rx: std::sync::mpsc::Receiver<crate::trash::TrashMessage>,
|
||||
reload: Rc<dyn Fn()>,
|
||||
// What the register calls this operation. Passed in rather than derived
|
||||
// here: the three callers move files to the trash, back out of it, and
|
||||
// delete them outright, and "Deleting 40 photographs" is the one word of
|
||||
// the three that must not appear over a restore.
|
||||
title: String,
|
||||
) {
|
||||
use crate::trash::TrashMessage;
|
||||
|
||||
let timer = slint::Timer::default();
|
||||
let ctl_cb = ctl.clone();
|
||||
|
||||
// A server-side MOVE per file, so it is a transfer in the sense that
|
||||
// matters: it takes as long as the connection is slow, and it can fail
|
||||
// halfway with the library in two states at once.
|
||||
let job = ctl.activity.begin(crate::activity::Kind::Trash, title);
|
||||
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
std::time::Duration::from_millis(120),
|
||||
@@ -907,6 +930,7 @@ fn drain_trash(
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
// A worker that died without reporting must not leave the
|
||||
// status line mid-sentence.
|
||||
job.fail("stopped without finishing");
|
||||
stop_trash(&ctl_cb);
|
||||
return;
|
||||
}
|
||||
@@ -923,6 +947,10 @@ fn drain_trash(
|
||||
} else {
|
||||
format!("{done} / {total}")
|
||||
};
|
||||
job.progress(done, total);
|
||||
if failed > 0 {
|
||||
job.detail(format!("{failed} failed"));
|
||||
}
|
||||
w.set_library_status(status.into());
|
||||
}
|
||||
TrashMessage::Done { moved, failed } => {
|
||||
@@ -934,6 +962,14 @@ fn drain_trash(
|
||||
} else {
|
||||
format!("{moved} done · {} failed", failed.len())
|
||||
};
|
||||
// A partial failure is a failure in the register: the
|
||||
// library is now in two states at once, which is
|
||||
// exactly the thing worth keeping on the list.
|
||||
if failed.is_empty() {
|
||||
job.finish(status.clone());
|
||||
} else {
|
||||
job.fail(status.clone());
|
||||
}
|
||||
w.set_library_status(status.into());
|
||||
if let Some(first) = failed.first() {
|
||||
w.set_collection_error(first.as_str().into());
|
||||
@@ -1396,6 +1432,7 @@ pub fn wire<S, R, C>(
|
||||
|
||||
log::info!("emptying trash: {} image(s)", ids.len());
|
||||
w.set_library_status(format!("Deleting {} image(s)…", ids.len()).into());
|
||||
let count = ids.len();
|
||||
|
||||
let rx = crate::trash::spawn_purge(
|
||||
creds,
|
||||
@@ -1412,6 +1449,7 @@ pub fn wire<S, R, C>(
|
||||
catalog.clone(),
|
||||
rx,
|
||||
reload.clone(),
|
||||
format!("Deleting {count} photograph(s) permanently"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user