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:
+26
-2
@@ -14,6 +14,7 @@
|
||||
//! pipeline what parameters it has and builds a control per answer; no code
|
||||
//! in `ui/` names an operation or knows a shader exists (FR-DEV-3a).
|
||||
|
||||
mod activity;
|
||||
mod collections_ui;
|
||||
mod derived_sync;
|
||||
mod develop;
|
||||
@@ -434,6 +435,18 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
|
||||
let window = AppWindow::new()?;
|
||||
|
||||
// Every background job reports here, and this draws the bar across the top
|
||||
// of the shell and fills the settings page's list. Built before the
|
||||
// controllers because they take a handle to it: a job that starts during
|
||||
// startup — the scan a resumed session begins immediately — has to have
|
||||
// somewhere to report to before it starts, or its first minute is invisible.
|
||||
let activity = activity::ActivityLog::new();
|
||||
activity.attach(&window);
|
||||
{
|
||||
let activity = activity.clone();
|
||||
window.on_activity_clear_finished(move || activity.clear_finished());
|
||||
}
|
||||
|
||||
// Set once `show` exists; see where the library grid is wired below.
|
||||
#[allow(clippy::type_complexity)]
|
||||
let open_from_library: Rc<RefCell<Option<Rc<dyn Fn(String)>>>> = Rc::new(RefCell::new(None));
|
||||
@@ -449,7 +462,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
// Declared out here rather than inside the launch block below because the
|
||||
// develop side reads `paths` to rebuild its browsing list when an image is
|
||||
// opened from the grid.
|
||||
let library = library_ui::LibraryController::new();
|
||||
let library = library_ui::LibraryController::new(activity.clone());
|
||||
|
||||
// Launch screen: shown when there is nothing to display — no local paths
|
||||
// and no configured library. A user who has already signed in and chosen
|
||||
@@ -460,7 +473,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
window.set_show_launch(startup == launch::Startup::ShowLaunchScreen);
|
||||
|
||||
let library = library.clone();
|
||||
let collections = collections_ui::CollectionsController::new();
|
||||
let collections = collections_ui::CollectionsController::new(activity.clone());
|
||||
|
||||
// The click handler needs `show`, which is built further down because
|
||||
// it captures the develop session and the GPU context. This cell is
|
||||
@@ -860,6 +873,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
let gpu = gpu.clone();
|
||||
let activity = activity.clone();
|
||||
*open_from_library.borrow_mut() = Some(Rc::new(move |path: String| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
|
||||
@@ -895,6 +909,14 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
|
||||
let rx = library::spawn_full_fetch(creds, user_id, path.clone(), cache);
|
||||
|
||||
// The one transfer the user is actively waiting on. It gets a row
|
||||
// like any other, so a download that is still running after they
|
||||
// give up and go back to the grid is still accounted for.
|
||||
//
|
||||
// No denominator: `spawn_full_fetch` reports a result, not bytes as
|
||||
// they arrive, so the honest bar here is the indeterminate one.
|
||||
let job = activity.begin(activity::Kind::Download, format!("Downloading {name}"));
|
||||
|
||||
// Polled on the UI thread rather than joined: a join would freeze
|
||||
// the window for the length of the download.
|
||||
let weak = w.as_weak();
|
||||
@@ -916,6 +938,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let bytes = match got {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
job.fail(e.message.clone());
|
||||
log::warn!("{name}: {e}");
|
||||
// Offline needs its own words. "network error:
|
||||
// connection refused" over a photograph the user
|
||||
@@ -931,6 +954,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
job.finish(activity::describe_bytes(bytes.len() as u64));
|
||||
log::info!("{name}: {} bytes fetched", bytes.len());
|
||||
|
||||
match load_bytes(gpu.as_ref(), &bytes) {
|
||||
|
||||
Reference in New Issue
Block a user