Move the library's routes and status onto its own Slint global

The scan/opening/status/error lines, offline mode and its retry, pinning a
collection offline, the sync and thumbnail-sweep state, and the callbacks
that route the grid to a rescan, a sync, another library, a panorama merge
or an export — the last of what AppWindow still carried under the
library- prefix — move onto the `Library` global started earlier on this
branch. Rust reaches them through window.global::<Library>() rather than
window.set_/get_/on_/invoke_ on the root.

library-visible is the one name that stays: it is computed from
active-page and active-view, the shell's own routing state, which a
global cannot read. AppWindow now declares no other library- property or
callback.
This commit is contained in:
2026-09-20 22:22:55 +02:00
parent 6bf67cefc4
commit 1e472fd251
20 changed files with 542 additions and 433 deletions
+19 -15
View File
@@ -430,13 +430,15 @@ pub(super) fn wire_filter_scope_and_offline(
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_toggle_pin_scope(move || {
let Some(w) = weak.upgrade() else { return };
let Some(scope) = *ctl.scope.borrow() else {
return;
};
open_offline_prompt(&w, &ctl, scope);
});
window
.global::<Library>()
.on_library_toggle_pin_scope(move || {
let Some(w) = weak.upgrade() else { return };
let Some(scope) = *ctl.scope.borrow() else {
return;
};
open_offline_prompt(&w, &ctl, scope);
});
}
// TRACES: FR-NC-6a | FR-UI-4
@@ -493,14 +495,16 @@ pub(super) fn wire_filter_scope_and_offline(
let weak = window.as_weak();
let ctl = ctl.clone();
let coll_ctl = coll_ctl.clone();
window.on_library_retry_connection(move || {
let Some(w) = weak.upgrade() else { return };
log::info!("retrying the connection at the user's request");
// A rescan is the probe: it is the same request the scan worker
// makes, so a success proves reachability and repopulates the
// catalog in one pass rather than proving it twice.
start_rescan(&w, &ctl, &coll_ctl);
});
window
.global::<Library>()
.on_library_retry_connection(move || {
let Some(w) = weak.upgrade() else { return };
log::info!("retrying the connection at the user's request");
// A rescan is the probe: it is the same request the scan worker
// makes, so a success proves reachability and repopulates the
// catalog in one pass rather than proving it twice.
start_rescan(&w, &ctl, &coll_ctl);
});
}
}
+10 -8
View File
@@ -448,7 +448,7 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_sync_now(move || {
window.global::<Library>().on_library_sync_now(move || {
if let Some(w) = weak.upgrade() {
start_derived_sync(&w, &ctl);
}
@@ -461,11 +461,13 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_thumbnail_all(move || {
if let Some(w) = weak.upgrade() {
start_thumbnail_sweep(&w, &ctl);
}
});
window
.global::<Library>()
.on_library_thumbnail_all(move || {
if let Some(w) = weak.upgrade() {
start_thumbnail_sweep(&w, &ctl);
}
});
}
// A column-count change moves which cells begin a row, and month headings
@@ -666,7 +668,7 @@ fn wire_grid_routes(
// folder could not be changed without clearing stored state by hand.
{
let weak = window.as_weak();
window.on_library_change(move || {
window.global::<Library>().on_library_change(move || {
if let Some(w) = weak.upgrade() {
w.set_active_view(View::Launch);
}
@@ -715,7 +717,7 @@ fn wire_grid_routes(
let weak = window.as_weak();
let ctl = ctl.clone();
let coll_ctl = coll_ctl.clone();
window.on_library_rescan(move || {
window.global::<Library>().on_library_rescan(move || {
let Some(w) = weak.upgrade() else { return };
start_rescan(&w, &ctl, &coll_ctl);
});
+58 -33
View File
@@ -115,7 +115,9 @@ pub(super) fn open_offline_prompt(
let ids = match dr_catalog::collections::descendants(catalog.connection(), id) {
Ok(ids) => ids,
Err(e) => {
window.set_library_error(format!("resolving collection: {e}").into());
window
.global::<Library>()
.set_library_error(format!("resolving collection: {e}").into());
return;
}
};
@@ -180,7 +182,7 @@ pub(super) fn open_offline_prompt(
);
window.set_offline_prompt_can_keep(summary.missing() > 0);
window.set_offline_prompt_can_release(summary.held > 0);
window.set_offline_prompt_busy(window.get_library_pin_total() > 0);
window.set_offline_prompt_busy(window.global::<Library>().get_library_pin_total() > 0);
}
/// Close the offline question without answering it.
@@ -208,7 +210,9 @@ pub(super) fn keep_collection_offline(
return;
};
let Some(cache) = ctl.cache() else {
window.set_library_error("No cache directory for this library.".into());
window
.global::<Library>()
.set_library_error("No cache directory for this library.".into());
return;
};
@@ -220,26 +224,34 @@ pub(super) fn keep_collection_offline(
let ids = match dr_catalog::collections::descendants(catalog.connection(), id) {
Ok(ids) => ids,
Err(e) => {
window.set_library_error(format!("resolving collection: {e}").into());
window
.global::<Library>()
.set_library_error(format!("resolving collection: {e}").into());
return;
}
};
let images = collection_images(catalog, &ids);
if images.is_empty() {
window.set_library_error("Nothing in that collection to keep offline.".into());
window
.global::<Library>()
.set_library_error("Nothing in that collection to keep offline.".into());
return;
}
if let Err(e) = cache.pin(catalog.connection(), &images) {
window.set_library_error(format!("pinning: {e}").into());
window
.global::<Library>()
.set_library_error(format!("pinning: {e}").into());
return;
}
images
};
log::info!("pinned {} image(s) for offline use", images.len());
window.set_library_error(slint::SharedString::new());
window
.global::<Library>()
.set_library_error(slint::SharedString::new());
if ctl.scope.borrow().as_ref() == Some(&id) {
window.set_library_scope_pinned(true);
window.global::<Library>().set_library_scope_pinned(true);
}
close_offline_prompt(window, ctl);
start_pin_fetch(window, ctl);
@@ -264,7 +276,9 @@ pub(super) fn release_collection_offline(
return;
};
let Some(cache) = ctl.cache() else {
window.set_library_error("No cache directory for this library.".into());
window
.global::<Library>()
.set_library_error("No cache directory for this library.".into());
return;
};
@@ -276,7 +290,9 @@ pub(super) fn release_collection_offline(
let ids = match dr_catalog::collections::descendants(catalog.connection(), id) {
Ok(ids) => ids,
Err(e) => {
window.set_library_error(format!("resolving collection: {e}").into());
window
.global::<Library>()
.set_library_error(format!("resolving collection: {e}").into());
return;
}
};
@@ -284,7 +300,9 @@ pub(super) fn release_collection_offline(
let outcome = match cache.release(catalog.connection(), &images) {
Ok(r) => r,
Err(e) => {
window.set_library_error(format!("removing local copies: {e}").into());
window
.global::<Library>()
.set_library_error(format!("removing local copies: {e}").into());
return;
}
};
@@ -312,8 +330,10 @@ pub(super) fn release_collection_offline(
"released {count} image(s), freeing {}",
crate::activity::describe_bytes(freed)
);
window.set_library_error(slint::SharedString::new());
window.set_library_status(
window
.global::<Library>()
.set_library_error(slint::SharedString::new());
window.global::<Library>().set_library_status(
format!(
"Removed local copies · {} freed",
crate::activity::describe_bytes(freed)
@@ -322,13 +342,13 @@ pub(super) fn release_collection_offline(
.into(),
);
if ctl.scope.borrow().as_ref() == Some(&id) {
window.set_library_scope_pinned(false);
window.global::<Library>().set_library_scope_pinned(false);
}
// A download that was still running for this collection has just had its
// reason withdrawn; the worker checks `pending_pins` per file, so it stops
// finding work rather than being killed.
window.set_library_pin_total(0);
window.set_library_pin_done(0);
window.global::<Library>().set_library_pin_total(0);
window.global::<Library>().set_library_pin_done(0);
close_offline_prompt(window, ctl);
refresh_local_count(window, ctl);
refresh_collection_tree(window, ctl, coll_ctl);
@@ -433,7 +453,7 @@ fn start_pin_fetch(window: &AppWindow, ctl: &Rc<LibraryController>) {
Ok(m) => m,
Err(std::sync::mpsc::TryRecvError::Empty) => return,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
w.set_library_pin_total(0);
w.global::<Library>().set_library_pin_total(0);
job.fail("stopped without finishing");
stop(&ctl_cb.pin_timer);
return;
@@ -442,13 +462,13 @@ fn start_pin_fetch(window: &AppWindow, ctl: &Rc<LibraryController>) {
match msg {
library::PinMessage::Planned { total } => {
w.set_library_pin_total(total as i32);
w.set_library_pin_done(0);
w.global::<Library>().set_library_pin_total(total as i32);
w.global::<Library>().set_library_pin_done(0);
job.total(total);
}
library::PinMessage::Stored { done } => {
w.set_library_pin_done(done as i32);
job.progress(done, w.get_library_pin_total() as usize);
w.global::<Library>().set_library_pin_done(done as i32);
job.progress(done, w.global::<Library>().get_library_pin_total() as usize);
// The "On this device" count grows as they land, so
// the chip agrees with the progress line beside it.
refresh_local_count(&w, &ctl_cb);
@@ -462,8 +482,8 @@ fn start_pin_fetch(window: &AppWindow, ctl: &Rc<LibraryController>) {
"{stored} photograph(s) · {}",
crate::activity::describe_bytes(bytes)
));
w.set_library_pin_total(0);
w.set_library_pin_done(0);
w.global::<Library>().set_library_pin_total(0);
w.global::<Library>().set_library_pin_done(0);
refresh_local_count(&w, &ctl_cb);
stop(&ctl_cb.pin_timer);
return;
@@ -471,7 +491,7 @@ fn start_pin_fetch(window: &AppWindow, ctl: &Rc<LibraryController>) {
library::PinMessage::Failed { message, offline } => {
log::warn!("pin fetch stopped: {message}");
job.fail(message.clone());
w.set_library_pin_total(0);
w.global::<Library>().set_library_pin_total(0);
if offline {
ctl_cb
.reachability
@@ -479,7 +499,8 @@ fn start_pin_fetch(window: &AppWindow, ctl: &Rc<LibraryController>) {
.mark_unreachable(message, std::time::Instant::now());
refresh_offline(&w, &ctl_cb);
} else {
w.set_library_error(format!("keeping offline: {message}").into());
w.global::<Library>()
.set_library_error(format!("keeping offline: {message}").into());
}
refresh_local_count(&w, &ctl_cb);
stop(&ctl_cb.pin_timer);
@@ -554,12 +575,14 @@ pub(super) fn refresh_offline(window: &AppWindow, ctl: &Rc<LibraryController>) {
let lost = ctl.root_lost.borrow();
let offline = reach.is_offline() || lost.is_some();
window.set_library_offline(offline);
window.set_library_offline_reason(match lost.as_deref() {
Some(why) => why.into(),
None => reach.reason().unwrap_or_default().into(),
});
window.set_library_offline_since(
window.global::<Library>().set_library_offline(offline);
window
.global::<Library>()
.set_library_offline_reason(match lost.as_deref() {
Some(why) => why.into(),
None => reach.reason().unwrap_or_default().into(),
});
window.global::<Library>().set_library_offline_since(
// A duration is what a network outage has and a revoked permission
// does not: "for 4 minutes" invites waiting, and waiting is precisely
// what will not help here.
@@ -577,7 +600,7 @@ pub(super) fn refresh_offline(window: &AppWindow, ctl: &Rc<LibraryController>) {
// A stale scan error under an offline banner reports one problem twice.
if offline {
window.set_library_error("".into());
window.global::<Library>().set_library_error("".into());
}
drop(reach);
@@ -657,7 +680,9 @@ fn start_outbox_drain(window: &AppWindow, ctl: &Rc<LibraryController>) {
} else if written > 0 {
log::info!("{written} queued sidecar(s) uploaded");
job.finish(format!("{written} queued edit(s) uploaded"));
w.set_library_status(format!("{written} queued edit(s) uploaded").into());
w.global::<Library>().set_library_status(
format!("{written} queued edit(s) uploaded").into(),
);
} else {
job.finish_quietly();
}
+43 -24
View File
@@ -57,7 +57,9 @@ pub fn open(
let conn = match store.connection(&account, crate::remote::needs_secret(&account)) {
Ok(c) => c,
Err(e) => {
window.set_library_error(format!("credentials: {e}").into());
window
.global::<Library>()
.set_library_error(format!("credentials: {e}").into());
window.set_active_view(View::Library);
return;
}
@@ -79,10 +81,14 @@ pub fn open(
ctl.place_untouched.set(true);
window.set_active_view(View::Library);
window.set_library_open(true);
window.set_library_scanning(true);
window.set_library_error(slint::SharedString::new());
window.set_library_status("Starting…".into());
window.global::<Library>().set_library_open(true);
window.global::<Library>().set_library_scanning(true);
window
.global::<Library>()
.set_library_error(slint::SharedString::new());
window
.global::<Library>()
.set_library_status("Starting…".into());
// Always visible: two folders one letter apart are easy to confuse, and a
// scan of the wrong one is indistinguishable from a broken scan.
window
@@ -92,8 +98,10 @@ pub fn open(
// An empty filter would walk the whole tree and match nothing, which looks
// exactly like a broken scan. Say so instead.
if filter.is_empty() {
window.set_library_scanning(false);
window.set_library_error("No formats selected — tick at least one.".into());
window.global::<Library>().set_library_scanning(false);
window
.global::<Library>()
.set_library_error("No formats selected — tick at least one.".into());
return;
}
@@ -166,8 +174,10 @@ fn open_catalog_soon(
return;
}
window.set_library_opening(true);
window.set_library_status("Reading the catalog on this device…".into());
window.global::<Library>().set_library_opening(true);
window
.global::<Library>()
.set_library_status("Reading the catalog on this device…".into());
let rx = spawn_catalog_open(path.clone());
let weak = window.as_weak();
@@ -192,7 +202,7 @@ fn open_catalog_soon(
}
};
stop(&held.catalog_timer);
w.set_library_opening(false);
w.global::<Library>().set_library_opening(false);
match got {
CatalogOpen::Opened(cat) => {
@@ -307,7 +317,9 @@ fn begin_scan(
filter.iter().count(),
path.display()
);
window.set_library_status("Starting…".into());
window
.global::<Library>()
.set_library_status("Starting…".into());
let rx = library::spawn_scan(
conn.clone(),
@@ -358,9 +370,10 @@ fn drain_scan(
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
// A worker that died without sending must not leave
// the screen on "Scanning…" forever.
if w.get_library_scanning() {
w.set_library_scanning(false);
w.set_library_error("scan ended unexpectedly".into());
if w.global::<Library>().get_library_scanning() {
w.global::<Library>().set_library_scanning(false);
w.global::<Library>()
.set_library_error("scan ended unexpectedly".into());
job.fail("ended unexpectedly");
}
stop(&ctl.scan_timer);
@@ -384,7 +397,7 @@ fn drain_scan(
format!("{directories} folders · {images} images")
};
job.detail(status.clone());
w.set_library_status(status.into());
w.global::<Library>().set_library_status(status.into());
}
ScanMessage::Done {
found,
@@ -398,7 +411,7 @@ fn drain_scan(
{pruned} folders unchanged, {judgements} judgements \
taken in) in {elapsed_ms} ms"
);
w.set_library_scanning(false);
w.global::<Library>().set_library_scanning(false);
// A completed scan is the strongest possible evidence
// the server is reachable, so it clears offline mode
@@ -452,7 +465,7 @@ fn drain_scan(
status
};
job.finish(status.clone());
w.set_library_status(status.into());
w.global::<Library>().set_library_status(status.into());
// Usually already open — `show_catalog_now` opened it
// before this scan started, so the grid has been
@@ -487,7 +500,9 @@ fn drain_scan(
// than the part that was scrolled past.
start_sweep(&w, ctl);
}
Err(e) => w.set_library_error(format!("opening catalog: {e}").into()),
Err(e) => w
.global::<Library>()
.set_library_error(format!("opening catalog: {e}").into()),
}
stop(&ctl.scan_timer);
return;
@@ -498,7 +513,7 @@ fn drain_scan(
lost_root,
} => {
log::warn!("scan failed: {message}");
w.set_library_scanning(false);
w.global::<Library>().set_library_scanning(false);
// Recorded as a failure even where it is only the
// connection: the grid's offline banner says the server
// is unreachable, and this says which piece of work
@@ -540,7 +555,7 @@ fn drain_scan(
// for.
open_catalog_for_offline(&w, ctl, &catalog_path, &coll_ctl);
} else {
w.set_library_error(message.into());
w.global::<Library>().set_library_error(message.into());
}
stop(&ctl.scan_timer);
return;
@@ -568,9 +583,13 @@ pub(crate) fn start_rescan(
return;
};
window.set_library_scanning(true);
window.set_library_error(slint::SharedString::new());
window.set_library_status("Rescanning…".into());
window.global::<Library>().set_library_scanning(true);
window
.global::<Library>()
.set_library_error(slint::SharedString::new());
window
.global::<Library>()
.set_library_status("Rescanning…".into());
let path = library::catalog_path(&conn.account);
let rx = library::spawn_scan(
@@ -616,7 +635,7 @@ fn open_catalog_for_offline(
// No catalog and no server. This is the one genuinely empty case:
// a first run that never reached the server has nothing indexed.
log::warn!("offline with no local catalog: {e}");
window.set_library_error(
window.global::<Library>().set_library_error(
"Offline, and this library has not been scanned on this device yet.".into(),
);
}
+34 -14
View File
@@ -168,7 +168,9 @@ fn apply_keyword(window: &AppWindow, ctl: &Rc<LibraryController>, word: &str, as
Err(e) => {
// `BadName` carries text written to be read by the user rather than
// by a developer, so it is shown as it is.
window.set_library_error(format!("{e}").into());
window
.global::<Library>()
.set_library_error(format!("{e}").into());
return;
}
};
@@ -177,7 +179,9 @@ fn apply_keyword(window: &AppWindow, ctl: &Rc<LibraryController>, word: &str, as
// in the vocabulary, ready for the photographs it was typed for — so only
// the removal half needs a selection to act on.
if images.is_empty() && !assigning {
window.set_library_status("Select an image first".into());
window
.global::<Library>()
.set_library_status("Select an image first".into());
return;
}
@@ -197,13 +201,19 @@ fn apply_keyword(window: &AppWindow, ctl: &Rc<LibraryController>, word: &str, as
let n = match outcome {
Ok(n) => n,
Err(e) => {
window.set_library_error(format!("{e}").into());
window
.global::<Library>()
.set_library_error(format!("{e}").into());
return;
}
};
window.set_library_error(slint::SharedString::new());
window.set_library_status(keyword_summary(&word, n, images.len(), assigning).into());
window
.global::<Library>()
.set_library_error(slint::SharedString::new());
window
.global::<Library>()
.set_library_status(keyword_summary(&word, n, images.len(), assigning).into());
refresh_keywords(window, ctl, &images);
start_xmp_writes(window, ctl, &images);
@@ -268,7 +278,9 @@ fn apply_judgement(
if images.is_empty() {
// Nothing selected. Said out loud rather than ignored: a keystroke
// that silently does nothing reads as a broken key.
window.set_library_status("Select an image first".into());
window
.global::<Library>()
.set_library_status("Select an image first".into());
return;
}
@@ -287,14 +299,18 @@ fn apply_judgement(
};
if let Err(e) = wrote {
window.set_library_error(format!("recording rating: {e}").into());
window
.global::<Library>()
.set_library_error(format!("recording rating: {e}").into());
return;
}
// Report what happened, in the user's terms rather than as a count of
// rows. A bulk judgement on a selection is easy to trigger by accident
// and the status line is the only confirmation of its extent.
window.set_library_status(judgement_summary(images.len(), rating, flag).into());
window
.global::<Library>()
.set_library_status(judgement_summary(images.len(), rating, flag).into());
// Refresh the grid and the chips from what actually landed, rather
// than assuming the write took: a clamped or coalesced value must show
@@ -363,7 +379,9 @@ pub fn paste_settings_to_selection(
if images.is_empty() {
// Said out loud rather than ignored, matching what a judgement
// keystroke does with an empty selection.
window.set_library_status("Select an image first".into());
window
.global::<Library>()
.set_library_status("Select an image first".into());
return;
}
@@ -383,12 +401,14 @@ pub fn paste_settings_to_selection(
};
if writes.is_empty() {
window.set_library_error("Could not find those images in the catalog.".into());
window
.global::<Library>()
.set_library_error("Could not find those images in the catalog.".into());
return;
}
let count = writes.len();
window.set_library_status(
window.global::<Library>().set_library_status(
format!(
"Applied settings to {count} image{}.",
if count == 1 { "" } else { "s" }
@@ -643,7 +663,7 @@ fn drain_xmp(
}
}
if let Some(w) = weak.upgrade() {
w.set_library_status(status.into());
w.global::<Library>().set_library_status(status.into());
if reload {
// The grid draws what the reload changed, and the
// settings page stops offering what is settled.
@@ -752,7 +772,7 @@ pub(crate) fn start_sidecar_writes(
log::debug!("{queued} sidecar(s) queued for upload");
ctl_cb.outbox_maybe_dirty.set(true);
job.finish_quietly();
w.set_library_status(
w.global::<Library>().set_library_status(
format!("{queued} edit(s) saved · will upload when back online").into(),
);
stop(&ctl_cb.sidecar_timer);
@@ -772,7 +792,7 @@ pub(crate) fn start_sidecar_writes(
// Said plainly, because the consequence is specific:
// the rating is safe in the catalog but will not
// survive deleting it.
w.set_library_status(
w.global::<Library>().set_library_status(
format!(
"{written} saved · {failed} could not be written \
to the library folder"
+23 -15
View File
@@ -73,7 +73,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
};
// Already running: a second pass would race the first over the same
// scratch files.
if ctl.sync_timer.borrow().is_some() && window.get_library_syncing() {
if ctl.sync_timer.borrow().is_some() && window.global::<Library>().get_library_syncing() {
return;
}
@@ -128,7 +128,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
}
}
window.set_library_syncing(true);
window.global::<Library>().set_library_syncing(true);
let rx = crate::derived_sync::spawn_sync(
conn.clone(),
conn.account.root.clone(),
@@ -159,7 +159,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
Ok(m) => m,
Err(std::sync::mpsc::TryRecvError::Empty) => return,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
w.set_library_syncing(false);
w.global::<Library>().set_library_syncing(false);
job.fail("stopped without finishing");
stop(&ctl_cb.sync_timer);
return;
@@ -169,7 +169,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
match msg {
crate::derived_sync::SyncMessage::Status(s) => {
job.detail(s.clone());
w.set_library_status(s.into());
w.global::<Library>().set_library_status(s.into());
}
crate::derived_sync::SyncMessage::Finished(report) => {
log::info!(
@@ -193,7 +193,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
String::new()
}
);
w.set_library_syncing(false);
w.global::<Library>().set_library_syncing(false);
let summary = format!(
"{} shard(s) up, {} down",
report.shards_uploaded, report.shards_downloaded
@@ -204,7 +204,8 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
"nothing to exchange".to_string()
});
if report.did_anything() {
w.set_library_status(format!("synced · {summary}").into());
w.global::<Library>()
.set_library_status(format!("synced · {summary}").into());
}
// Adopted thumbnails and merged collections both change
// what the grid should show.
@@ -240,10 +241,11 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
crate::derived_sync::SyncMessage::Failed(e) => {
log::warn!("sync failed: {e}");
job.fail(e.to_string());
w.set_library_syncing(false);
w.global::<Library>().set_library_syncing(false);
// Not an error banner: a failed sync costs nothing —
// everything is still local and the next pass retries.
w.set_library_status(format!("sync failed: {e}").into());
w.global::<Library>()
.set_library_status(format!("sync failed: {e}").into());
stop(&ctl_cb.sync_timer);
return;
}
@@ -386,7 +388,9 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
// Already running. A second pass would re-fetch everything the first is
// part way through — the work list is built from what the store lacks, and
// the first pass has not filled it yet.
if ctl.thumb_sweep_timer.borrow().is_some() && window.get_library_thumbnailing() {
if ctl.thumb_sweep_timer.borrow().is_some()
&& window.global::<Library>().get_library_thumbnailing()
{
return;
}
@@ -397,11 +401,13 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
// button that silently does nothing is worse than one that declines.
if ctl.is_offline() {
log::debug!("offline: not starting the thumbnail pass");
window.set_library_status("Offline — thumbnailing needs the server.".into());
window
.global::<Library>()
.set_library_status("Offline — thumbnailing needs the server.".into());
return;
}
window.set_library_thumbnailing(true);
window.global::<Library>().set_library_thumbnailing(true);
let rx = library::spawn_thumbnail_sweep(
conn.clone(),
library::catalog_path(&conn.account),
@@ -436,7 +442,7 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
Ok(m) => m,
Err(std::sync::mpsc::TryRecvError::Empty) => return,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
w.set_library_thumbnailing(false);
w.global::<Library>().set_library_thumbnailing(false);
job.fail("stopped without finishing");
stop(&ctl_cb.thumb_sweep_timer);
return;
@@ -447,7 +453,8 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
library::ThumbSweepMessage::Total(n) => {
total.set(n);
job.total(n);
w.set_library_status(format!("Thumbnailing {n} photograph(s)…").into());
w.global::<Library>()
.set_library_status(format!("Thumbnailing {n} photograph(s)…").into());
}
library::ThumbSweepMessage::Progress { done, stored } => {
job.progress(done, total.get());
@@ -469,7 +476,7 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
log::info!(
"thumbnail pass finished: {stored} stored, {failed} without a preview"
);
w.set_library_thumbnailing(false);
w.global::<Library>().set_library_thumbnailing(false);
stop(&ctl_cb.thumb_sweep_timer);
let summary = if offline {
@@ -479,7 +486,8 @@ pub(super) fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryControll
} else {
format!("{stored} stored")
};
w.set_library_status(summary.clone().into());
w.global::<Library>()
.set_library_status(summary.clone().into());
// Finished rather than failed even when the server
// dropped: the pass is resumable and what it did build
// is stored, and a red row would invite the user to
+1 -1
View File
@@ -657,7 +657,7 @@ pub(super) fn write_place(window: &AppWindow, ctl: &Rc<LibraryController>) {
fn current_place(window: &AppWindow, ctl: &Rc<LibraryController>) -> Option<dr_types::Place> {
use dr_types::{Place, PlaceScope, Screen};
if window.get_active_view() == View::Launch || !window.get_library_open() {
if window.get_active_view() == View::Launch || !window.global::<Library>().get_library_open() {
return None;
}
+13 -9
View File
@@ -234,13 +234,15 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// Three queries deep — descendants, their images, and whether every one of
// them is pinned — and the answer cannot change by scrolling.
if describes_something_new {
window.set_library_scope_pinned(match scope {
Some(id) => dr_catalog::collections::descendants(catalog.connection(), id)
.map(|ids| collection_images(catalog, &ids))
.map(|images| scope_is_pinned(catalog, &images))
.unwrap_or(false),
None => false,
});
window
.global::<Library>()
.set_library_scope_pinned(match scope {
Some(id) => dr_catalog::collections::descendants(catalog.connection(), id)
.map(|ids| collection_images(catalog, &ids))
.map(|images| scope_is_pinned(catalog, &images))
.unwrap_or(false),
None => false,
});
}
// Read before it is overwritten: the property still holds what the library
// was last time this ran, and a library that has become shorter is the
@@ -293,7 +295,9 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
let cells = match cells {
Ok(c) => c,
Err(e) => {
window.set_library_error(format!("reading catalog: {e}").into());
window
.global::<Library>()
.set_library_error(format!("reading catalog: {e}").into());
return;
}
};
@@ -757,7 +761,7 @@ fn drain_thumbnails(
if !parts.is_empty() {
let status = parts.join(" · ");
job.detail(status.clone());
w.set_library_status(status.into());
w.global::<Library>().set_library_status(status.into());
}
}
// A header-only date read. Advances the bar; draws nothing.