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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user