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:
@@ -12,7 +12,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::ImageId;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
use crate::{AppWindow, Collections};
|
||||
use crate::{AppWindow, Collections, Library};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
use super::tree_sync::refresh_tree;
|
||||
@@ -56,7 +56,9 @@ pub(super) fn start_trash(
|
||||
}
|
||||
|
||||
log::info!("moving {} image(s) to the trash", moves.len());
|
||||
window.set_library_status(format!("Moving {} to the trash…", moves.len()).into());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_status(format!("Moving {} to the trash…", moves.len()).into());
|
||||
// The images are leaving the grid; a selection pointing at them would
|
||||
// survive as a set of ids the user can no longer see.
|
||||
ctl.clear_selection();
|
||||
@@ -131,7 +133,9 @@ pub(super) fn start_restore(
|
||||
}
|
||||
|
||||
log::info!("restoring {} image(s) from the trash", moves.len());
|
||||
window.set_library_status(format!("Restoring {}…", moves.len()).into());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_status(format!("Restoring {}…", moves.len()).into());
|
||||
// The images are leaving the trash view, so a selection pointing at them
|
||||
// would survive as ids the user can no longer see.
|
||||
ctl.clear_selection();
|
||||
@@ -214,7 +218,7 @@ pub(super) fn drain_trash(
|
||||
if failed > 0 {
|
||||
job.detail(format!("{failed} failed"));
|
||||
}
|
||||
w.set_library_status(status.into());
|
||||
w.global::<Library>().set_library_status(status.into());
|
||||
}
|
||||
TrashMessage::Done { moved, failed } => {
|
||||
// Reported honestly, including the partial case: "38 of
|
||||
@@ -233,7 +237,7 @@ pub(super) fn drain_trash(
|
||||
} else {
|
||||
job.fail(status.clone());
|
||||
}
|
||||
w.set_library_status(status.into());
|
||||
w.global::<Library>().set_library_status(status.into());
|
||||
if let Some(first) = failed.first() {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(first.as_str().into());
|
||||
|
||||
@@ -233,7 +233,7 @@ fn wire_selection(
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
w.global::<Library>().set_library_status(
|
||||
format!(
|
||||
"{} photograph{} moved",
|
||||
moving.len(),
|
||||
@@ -378,7 +378,7 @@ fn wire_selection_filing(
|
||||
Ok(added) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
w.global::<Library>().set_library_status(
|
||||
format!(
|
||||
"{added} photograph{} in {name}",
|
||||
if added == 1 { "" } else { "s" }
|
||||
@@ -485,7 +485,7 @@ fn wire_selection_filing(
|
||||
chosen.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
w.global::<Library>().set_library_status(msg.into());
|
||||
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
@@ -690,7 +690,8 @@ fn wire_drag(
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved collection".into());
|
||||
w.global::<Library>()
|
||||
.set_library_status("Moved collection".into());
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
@@ -731,7 +732,7 @@ fn wire_drag(
|
||||
carried.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
w.global::<Library>().set_library_status(msg.into());
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
@@ -921,7 +922,8 @@ fn wire_trash_from_grid(
|
||||
if chosen.is_empty() {
|
||||
// A keystroke that does nothing reads as a broken key, so it
|
||||
// says why rather than failing silently.
|
||||
w.set_library_status("Select an image first".into());
|
||||
w.global::<Library>()
|
||||
.set_library_status("Select an image first".into());
|
||||
return;
|
||||
}
|
||||
start_trash(&w, &ctl, &catalog, &session, &chosen, &reload);
|
||||
@@ -967,7 +969,8 @@ fn wire_trash_from_grid(
|
||||
let ids: Vec<ImageId> = listed.iter().map(|t| t.image_id).collect();
|
||||
|
||||
log::info!("emptying trash: {} image(s)", ids.len());
|
||||
w.set_library_status(format!("Deleting {} image(s)…", ids.len()).into());
|
||||
w.global::<Library>()
|
||||
.set_library_status(format!("Deleting {} image(s)…", ids.len()).into());
|
||||
let count = ids.len();
|
||||
|
||||
let rx = crate::trash::spawn_purge(
|
||||
|
||||
@@ -260,7 +260,7 @@ pub(super) fn wire_remove(
|
||||
// Named explicitly as a membership change: the images are
|
||||
// still in the library, and a user who reads this as a
|
||||
// delete will not trust the feature again.
|
||||
w.set_library_status(
|
||||
w.global::<Library>().set_library_status(
|
||||
format!("Removed {n} from this collection; still in the library")
|
||||
.into(),
|
||||
);
|
||||
@@ -424,7 +424,8 @@ pub(super) fn wire_row_drag(
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
w.global::<Library>()
|
||||
.set_library_status("Moved to the top level".into());
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} by drag", source.0);
|
||||
}
|
||||
@@ -540,7 +541,8 @@ pub(super) fn wire_row_menu(
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
w.global::<Library>()
|
||||
.set_library_status("Moved to the top level".into());
|
||||
close_row_menu(&w, &ctl);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} to the top level", id.0);
|
||||
@@ -629,7 +631,7 @@ pub(super) fn wire_row_menu(
|
||||
// Named as a membership change, as the removal button is:
|
||||
// a user who reads this as a delete of their photographs
|
||||
// will not trust the feature again.
|
||||
w.set_library_status(if holds > 0 {
|
||||
w.global::<Library>().set_library_status(if holds > 0 {
|
||||
format!(
|
||||
"Deleted the collection; its {holds} photograph(s) stay in the library"
|
||||
)
|
||||
@@ -729,7 +731,7 @@ pub(super) fn wire_membership(
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// The same wording the scoped button uses, and for the same
|
||||
// reason: this is a membership change, not a delete.
|
||||
w.set_library_status(
|
||||
w.global::<Library>().set_library_status(
|
||||
format!("Removed {n} from that collection; still in the library")
|
||||
.into(),
|
||||
);
|
||||
|
||||
+40
-32
@@ -31,8 +31,8 @@ use crate::spots_ui;
|
||||
use crate::{
|
||||
apply_crop_aspect, batch_request, drain_outbox, export, groups_in_rail, refresh_export_label,
|
||||
render_open_frame, sync_crop_aspect, sync_film, sync_framing, sync_rows, Adjustments,
|
||||
AppWindow, Develop, DevelopSession, Framing, Inspection, ParamRow, Peaking, Render, Steps,
|
||||
ViewMode,
|
||||
AppWindow, Develop, DevelopSession, Framing, Inspection, Library, ParamRow, Peaking, Render,
|
||||
Steps, ViewMode,
|
||||
};
|
||||
|
||||
/// Shared state the develop screen's callbacks capture.
|
||||
@@ -285,7 +285,7 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
// buttons share a worker slot, so starting a second run would
|
||||
// drop the first's drain — leaving a batch still writing files
|
||||
// with no progress row and a button that never comes back.
|
||||
if w.get_export_busy() || w.get_library_exporting() {
|
||||
if w.get_export_busy() || w.global::<Library>().get_library_exporting() {
|
||||
w.set_export_status("An export is already running".into());
|
||||
return;
|
||||
}
|
||||
@@ -316,42 +316,50 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
let library = library.clone();
|
||||
let collections = collections.clone();
|
||||
let start = start.clone();
|
||||
window.on_library_export_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// See the develop button above for why one run excludes the
|
||||
// other. The button is a cancel by then, so this only catches
|
||||
// a batch started from develop and left running.
|
||||
if w.get_library_exporting() || w.get_export_busy() {
|
||||
w.set_library_status("An export is already running".into());
|
||||
return;
|
||||
}
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_export_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// See the develop button above for why one run excludes the
|
||||
// other. The button is a cancel by then, so this only catches
|
||||
// a batch started from develop and left running.
|
||||
if w.global::<Library>().get_library_exporting() || w.get_export_busy() {
|
||||
w.global::<Library>()
|
||||
.set_library_status("An export is already running".into());
|
||||
return;
|
||||
}
|
||||
|
||||
let sources = library.export_sources(&collections.selected());
|
||||
if sources.is_empty() {
|
||||
// Said out loud rather than ignored, matching what a paste
|
||||
// or a judgement keystroke does with an empty selection.
|
||||
w.set_library_status("Select an image first".into());
|
||||
return;
|
||||
}
|
||||
let sources = library.export_sources(&collections.selected());
|
||||
if sources.is_empty() {
|
||||
// Said out loud rather than ignored, matching what a paste
|
||||
// or a judgement keystroke does with an empty selection.
|
||||
w.global::<Library>()
|
||||
.set_library_status("Select an image first".into());
|
||||
return;
|
||||
}
|
||||
|
||||
w.set_library_exporting(true);
|
||||
w.set_library_status(format!("Exporting {} images…", sources.len()).into());
|
||||
start(&w, sources, export::Reporting::Library);
|
||||
});
|
||||
w.global::<Library>().set_library_exporting(true);
|
||||
w.global::<Library>()
|
||||
.set_library_status(format!("Exporting {} images…", sources.len()).into());
|
||||
start(&w, sources, export::Reporting::Library);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: NFR-ARCH-3
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
window.on_library_cancel_export(move || {
|
||||
cancel.borrow().cancel();
|
||||
if let Some(w) = weak.upgrade() {
|
||||
// The worker stops at the next point it is safe to — which
|
||||
// may be a frame away — so the button says "asked for" and
|
||||
// not "done". The drain writes the real answer.
|
||||
w.set_library_status("Cancelling the export…".into());
|
||||
}
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_cancel_export(move || {
|
||||
cancel.borrow().cancel();
|
||||
if let Some(w) = weak.upgrade() {
|
||||
// The worker stops at the next point it is safe to — which
|
||||
// may be a frame away — so the button says "asked for" and
|
||||
// not "done". The drain writes the real answer.
|
||||
w.global::<Library>()
|
||||
.set_library_status("Cancelling the export…".into());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ use dr_export::{Encoded, NameContext};
|
||||
use dr_sync::RemotePath;
|
||||
use dr_sync::{Account, Connection};
|
||||
use dr_types::{ExportSettings, ExportTarget};
|
||||
use slint::ComponentHandle as _;
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
/// Where exports wait for a server that is not there yet.
|
||||
///
|
||||
@@ -1131,7 +1132,7 @@ fn summarise(
|
||||
fn report(window: &AppWindow, reporting: Reporting, text: &str) {
|
||||
match reporting {
|
||||
Reporting::Develop => window.set_export_status(text.into()),
|
||||
Reporting::Library => window.set_library_status(text.into()),
|
||||
Reporting::Library => window.global::<Library>().set_library_status(text.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,7 +1141,7 @@ fn settle(window: &AppWindow, reporting: Reporting, text: &str) {
|
||||
report(window, reporting, text);
|
||||
match reporting {
|
||||
Reporting::Develop => window.set_export_busy(false),
|
||||
Reporting::Library => window.set_library_exporting(false),
|
||||
Reporting::Library => window.global::<Library>().set_library_exporting(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1842,7 +1842,7 @@ fn wire_import_and_merge(
|
||||
// on the next sync pass, then look for it, exactly as an
|
||||
// import does.
|
||||
drain_outbox(&library_for_done);
|
||||
w.invoke_library_rescan();
|
||||
w.global::<Library>().invoke_library_rescan();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1879,7 +1879,7 @@ fn wire_import_and_merge(
|
||||
// the second is what the grid shows, so this asks for the scan
|
||||
// that finds them rather than inserting rows itself.
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.invoke_library_rescan();
|
||||
w.global::<Library>().invoke_library_rescan();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
+108
-101
@@ -22,7 +22,7 @@ use crate::merge::{
|
||||
self, AlignmentReport, Cancel, Decision, FillSettings, MergeDestination, MergeEvent,
|
||||
MergeInput, MergeRequest,
|
||||
};
|
||||
use crate::{AppWindow, MergeFrameRow, Page};
|
||||
use crate::{AppWindow, Library, MergeFrameRow, Page};
|
||||
|
||||
/// How often the page reads the job's channel.
|
||||
const DRAIN_INTERVAL: std::time::Duration = std::time::Duration::from_millis(120);
|
||||
@@ -118,107 +118,114 @@ fn wire_start<S, C, F>(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let on_done = on_done.clone();
|
||||
window.on_library_merge_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if ctl.job.borrow().is_some() {
|
||||
w.set_library_status("A panorama is already being merged".into());
|
||||
return;
|
||||
}
|
||||
let Some(gpu) = gpu.clone() else {
|
||||
w.set_library_status("No GPU: a panorama cannot be merged on this device".into());
|
||||
return;
|
||||
};
|
||||
let Some(context) = context() else {
|
||||
w.set_library_status("Open a library first".into());
|
||||
return;
|
||||
};
|
||||
// The selection, in library order. The catalog's order is the
|
||||
// grid's, which is capture time — the order a sweep was shot.
|
||||
let sources: Vec<(String, Option<crate::library::CacheContext>)> = sources()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
crate::export::Source::Library { path, cache } => Some((path, cache)),
|
||||
crate::export::Source::Rendered { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
if sources.len() < 2 {
|
||||
w.set_library_status("Select at least two frames to merge".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Where the composite goes: beside its sources, through the
|
||||
// outbox (FR-MRG-3). The sources' folder, relative to the root.
|
||||
let root = context.conn.account.root.trim_matches('/').to_string();
|
||||
let first_dir = std::path::Path::new(&sources[0].0)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().trim_matches('/').to_string())
|
||||
.unwrap_or_default();
|
||||
let remote_dir = first_dir
|
||||
.strip_prefix(&root)
|
||||
.map(|s| s.trim_start_matches('/').to_string())
|
||||
.unwrap_or(first_dir);
|
||||
let destination = MergeDestination::Outbox {
|
||||
outbox: context.outbox,
|
||||
remote_dir,
|
||||
};
|
||||
|
||||
let names: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|(p, _)| {
|
||||
std::path::Path::new(p)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| p.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The fetch, on the job's own thread before the job: every
|
||||
// original comes down in turn — sequential so twelve downloads
|
||||
// do not halve each other's bandwidth — and the job then takes
|
||||
// bytes, which keeps it free of the library.
|
||||
let conn = context.conn.clone();
|
||||
let fetch_names = names.clone();
|
||||
let fetch = move |tx: &Sender<MergeEvent>,
|
||||
cancel: &Cancel|
|
||||
-> Option<Vec<MergeInput>> {
|
||||
let mut frames = Vec::with_capacity(sources.len());
|
||||
for (i, (path, cache)) in sources.into_iter().enumerate() {
|
||||
if cancel.is_cancelled() {
|
||||
let _ = tx.send(MergeEvent::Cancelled);
|
||||
return None;
|
||||
}
|
||||
let _ = tx.send(MergeEvent::Progress {
|
||||
stage: "Fetching",
|
||||
done: i,
|
||||
total: fetch_names.len(),
|
||||
});
|
||||
let rx = crate::library::spawn_full_fetch(conn.clone(), path.clone(), cache);
|
||||
let bytes = match rx.recv() {
|
||||
Ok(Ok(bytes)) => bytes,
|
||||
Ok(Err(e)) => {
|
||||
let _ = tx.send(MergeEvent::Failed(format!(
|
||||
"{}: {}",
|
||||
fetch_names[i], e.message
|
||||
)));
|
||||
return None;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = tx.send(MergeEvent::Failed(format!(
|
||||
"{}: the download ended without answering",
|
||||
fetch_names[i]
|
||||
)));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
frames.push(MergeInput {
|
||||
name: fetch_names[i].clone(),
|
||||
bytes: std::sync::Arc::new(bytes),
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_merge_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if ctl.job.borrow().is_some() {
|
||||
w.global::<Library>()
|
||||
.set_library_status("A panorama is already being merged".into());
|
||||
return;
|
||||
}
|
||||
Some(frames)
|
||||
};
|
||||
start(&w, &ctl, gpu, names, destination, fetch, &on_done);
|
||||
});
|
||||
let Some(gpu) = gpu.clone() else {
|
||||
w.global::<Library>().set_library_status(
|
||||
"No GPU: a panorama cannot be merged on this device".into(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(context) = context() else {
|
||||
w.global::<Library>()
|
||||
.set_library_status("Open a library first".into());
|
||||
return;
|
||||
};
|
||||
// The selection, in library order. The catalog's order is the
|
||||
// grid's, which is capture time — the order a sweep was shot.
|
||||
let sources: Vec<(String, Option<crate::library::CacheContext>)> = sources()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
crate::export::Source::Library { path, cache } => Some((path, cache)),
|
||||
crate::export::Source::Rendered { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
if sources.len() < 2 {
|
||||
w.global::<Library>()
|
||||
.set_library_status("Select at least two frames to merge".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Where the composite goes: beside its sources, through the
|
||||
// outbox (FR-MRG-3). The sources' folder, relative to the root.
|
||||
let root = context.conn.account.root.trim_matches('/').to_string();
|
||||
let first_dir = std::path::Path::new(&sources[0].0)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().trim_matches('/').to_string())
|
||||
.unwrap_or_default();
|
||||
let remote_dir = first_dir
|
||||
.strip_prefix(&root)
|
||||
.map(|s| s.trim_start_matches('/').to_string())
|
||||
.unwrap_or(first_dir);
|
||||
let destination = MergeDestination::Outbox {
|
||||
outbox: context.outbox,
|
||||
remote_dir,
|
||||
};
|
||||
|
||||
let names: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|(p, _)| {
|
||||
std::path::Path::new(p)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| p.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The fetch, on the job's own thread before the job: every
|
||||
// original comes down in turn — sequential so twelve downloads
|
||||
// do not halve each other's bandwidth — and the job then takes
|
||||
// bytes, which keeps it free of the library.
|
||||
let conn = context.conn.clone();
|
||||
let fetch_names = names.clone();
|
||||
let fetch =
|
||||
move |tx: &Sender<MergeEvent>, cancel: &Cancel| -> Option<Vec<MergeInput>> {
|
||||
let mut frames = Vec::with_capacity(sources.len());
|
||||
for (i, (path, cache)) in sources.into_iter().enumerate() {
|
||||
if cancel.is_cancelled() {
|
||||
let _ = tx.send(MergeEvent::Cancelled);
|
||||
return None;
|
||||
}
|
||||
let _ = tx.send(MergeEvent::Progress {
|
||||
stage: "Fetching",
|
||||
done: i,
|
||||
total: fetch_names.len(),
|
||||
});
|
||||
let rx =
|
||||
crate::library::spawn_full_fetch(conn.clone(), path.clone(), cache);
|
||||
let bytes = match rx.recv() {
|
||||
Ok(Ok(bytes)) => bytes,
|
||||
Ok(Err(e)) => {
|
||||
let _ = tx.send(MergeEvent::Failed(format!(
|
||||
"{}: {}",
|
||||
fetch_names[i], e.message
|
||||
)));
|
||||
return None;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = tx.send(MergeEvent::Failed(format!(
|
||||
"{}: the download ended without answering",
|
||||
fetch_names[i]
|
||||
)));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
frames.push(MergeInput {
|
||||
name: fetch_names[i].clone(),
|
||||
bytes: std::sync::Arc::new(bytes),
|
||||
});
|
||||
}
|
||||
Some(frames)
|
||||
};
|
||||
start(&w, &ctl, gpu, names, destination, fetch, &on_done);
|
||||
});
|
||||
}
|
||||
|
||||
// A way to land on the page at startup with local files, for looking
|
||||
|
||||
@@ -44,7 +44,7 @@ use dr_catalog::recovery;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
use crate::library_ui::LibraryController;
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
// What the open question is about.
|
||||
//
|
||||
@@ -123,7 +123,7 @@ pub(crate) fn offer(window: &AppWindow, catalog: &Path, detail: &str) {
|
||||
window.set_recovery_busy(false);
|
||||
// The scan was held back, so the "Scanning…" the grid is showing behind
|
||||
// this would be a lie the moment the question is dismissed.
|
||||
window.set_library_scanning(false);
|
||||
window.global::<Library>().set_library_scanning(false);
|
||||
|
||||
PENDING.with(|p| {
|
||||
*p.borrow_mut() = Some(Pending {
|
||||
@@ -141,8 +141,8 @@ pub(crate) fn offer(window: &AppWindow, catalog: &Path, detail: &str) {
|
||||
fn dismiss(window: &AppWindow) {
|
||||
PENDING.with(|p| *p.borrow_mut() = None);
|
||||
window.set_recovery_title(slint::SharedString::new());
|
||||
window.set_library_scanning(false);
|
||||
window.set_library_error(
|
||||
window.global::<Library>().set_library_scanning(false);
|
||||
window.global::<Library>().set_library_error(
|
||||
"The library index is damaged. Rescan to rebuild it, or restore a backup.".into(),
|
||||
);
|
||||
}
|
||||
@@ -243,7 +243,9 @@ fn answer(
|
||||
|
||||
window.set_recovery_busy(false);
|
||||
window.set_recovery_title(slint::SharedString::new());
|
||||
window.set_library_error(slint::SharedString::new());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_error(slint::SharedString::new());
|
||||
|
||||
if crate::library_ui::show_catalog_now(window, ctl, &pending.catalog, coll_ctl) {
|
||||
crate::library_ui::start_rescan(window, ctl, coll_ctl);
|
||||
|
||||
+24
-77
@@ -310,40 +310,8 @@ export component AppWindow inherits Window {
|
||||
in property <int> activity-kept: 0;
|
||||
callback activity-clear-finished();
|
||||
|
||||
in property <bool> library-scanning: false;
|
||||
/// The catalog on this device is being opened and checked, on a worker,
|
||||
/// before the scan starts (`library_ui::open_catalog_soon`).
|
||||
///
|
||||
/// Separate from `library-scanning` because they are different answers to
|
||||
/// the same question: this one is reading a file that is already here, and
|
||||
/// that one is walking a tree over the network. Both are true at once for
|
||||
/// the first moment of a launch, and only the more specific of them is
|
||||
/// worth putting on an empty grid.
|
||||
in property <bool> library-opening: false;
|
||||
in property <string> library-status: "";
|
||||
in property <string> library-error: "";
|
||||
|
||||
// --- offline mode (FR-CAT-9) ---
|
||||
//
|
||||
// The library keeps working from local data when the server cannot be
|
||||
// reached: thumbnails come from the shards, and rating and sorting are
|
||||
// catalog operations that never needed the network. What stops is opening
|
||||
// an original that was never stored locally.
|
||||
//
|
||||
// `offline-reason` carries the transport's own message ("connection
|
||||
// refused", "dns error") because it is usually specific enough to act on,
|
||||
// where a bare "offline" leaves the user guessing whether it is their wifi
|
||||
// or the server.
|
||||
in property <bool> library-offline: false;
|
||||
in property <string> library-offline-reason: "";
|
||||
in property <string> library-offline-since: "";
|
||||
callback library-retry-connection();
|
||||
|
||||
// --- pinning a collection offline (FR-NC-6a) ---
|
||||
in property <bool> library-scope-pinned: false;
|
||||
in property <int> library-pin-done: 0;
|
||||
in property <int> library-pin-total: 0;
|
||||
callback library-toggle-pin-scope();
|
||||
|
||||
// The question a held collection asks: keep these on the device, or give
|
||||
// the disk back. Driven entirely from Rust — the counts and the byte sizes
|
||||
@@ -374,13 +342,6 @@ export component AppWindow inherits Window {
|
||||
callback recovery-rebuild();
|
||||
callback recovery-dismiss();
|
||||
|
||||
in property <bool> library-syncing: false;
|
||||
/// TRACES: FR-CAT-3 | FR-NC-3
|
||||
/// The whole-library thumbnail pass, launched from the settings page.
|
||||
/// `library-open` gates the button on there being a library to sweep —
|
||||
/// the page is reachable from the launch screen too.
|
||||
in property <bool> library-thumbnailing: false;
|
||||
in property <bool> library-open: false;
|
||||
|
||||
// --- people and faces (FR-CULL-8 … FR-CULL-12) ---
|
||||
//
|
||||
@@ -443,14 +404,9 @@ export component AppWindow inherits Window {
|
||||
callback identity-stop-indexing();
|
||||
callback identity-check-coverage();
|
||||
callback identity-delete-all();
|
||||
callback library-thumbnail-all();
|
||||
/// Whether the photo roll is out. Owned here rather than by the roll, so
|
||||
/// it survives the develop view being torn down and rebuilt.
|
||||
in-out property <bool> roll-open: false;
|
||||
callback library-sync-now();
|
||||
callback library-rescan();
|
||||
/// Grid → launch screen, to pick a different folder or account.
|
||||
callback library-change();
|
||||
/// Develop → grid.
|
||||
callback back-to-library();
|
||||
|
||||
@@ -608,8 +564,6 @@ export component AppWindow inherits Window {
|
||||
in property <float> merge-fill-mirror: 48;
|
||||
in property <float> merge-fill-feather: 48;
|
||||
in property <bool> merge-done: false;
|
||||
/// Merge the grid's selection: opens the page and starts the alignment.
|
||||
callback library-merge-selection();
|
||||
/// The confirmation (FR-MRG-1). Nothing is written before this.
|
||||
callback merge-confirm();
|
||||
callback merge-abandon();
|
||||
@@ -897,13 +851,6 @@ in property <bool> panel-visible: true;
|
||||
/// composes its own label around a selection count — so it is given the
|
||||
/// fact and not the sentence.
|
||||
in property <bool> export-to-server: false;
|
||||
/// Whether a batch is running. The grid's export button becomes its cancel
|
||||
/// button; there is nowhere else a long batch can be stopped from.
|
||||
in property <bool> library-exporting: false;
|
||||
/// Export the grid's selection in the background (FR-EXP-7).
|
||||
callback library-export-selection();
|
||||
/// TRACES: NFR-ARCH-3
|
||||
callback library-cancel-export();
|
||||
/// Show or hide the develop column.
|
||||
callback toggle-panel();
|
||||
/// Export the image on screen, using the settings as they stand.
|
||||
@@ -1209,7 +1156,7 @@ in property <bool> panel-visible: true;
|
||||
activity-running: root.activity-running;
|
||||
activity-kept: root.activity-kept;
|
||||
clear-finished() => { root.activity-clear-finished(); }
|
||||
thumbnailing: root.library-thumbnailing;
|
||||
thumbnailing: Library.library-thumbnailing;
|
||||
// The face pass reuses the Identity Manager's own state rather
|
||||
// than duplicating it: one job, startable from either place, and
|
||||
// a button that cannot disagree with the screen about whether it
|
||||
@@ -1220,8 +1167,8 @@ in property <bool> panel-visible: true;
|
||||
face-detector-labels: root.settings-face-detector-labels;
|
||||
face-detector-selected: root.settings-face-detector-selected;
|
||||
face-detector-picked(i) => { root.settings-face-detector-picked(i); }
|
||||
library-open: root.library-open;
|
||||
thumbnail-library() => { root.library-thumbnail-all(); }
|
||||
library-open: Library.library-open;
|
||||
thumbnail-library() => { Library.library-thumbnail-all(); }
|
||||
index-faces() => { root.identity-index(); }
|
||||
reindex-faces() => { root.identity-reindex(); }
|
||||
|
||||
@@ -1367,10 +1314,10 @@ in property <bool> panel-visible: true;
|
||||
// referred to the tree rather than to the library.
|
||||
// They read the same window state the header did — one
|
||||
// source per fact — and call the same handlers.
|
||||
scanning: root.library-scanning;
|
||||
scope-pinned: root.library-scope-pinned;
|
||||
toggle-pin-scope() => { root.library-toggle-pin-scope(); }
|
||||
change-library() => { root.library-change(); }
|
||||
scanning: Library.library-scanning;
|
||||
scope-pinned: Library.library-scope-pinned;
|
||||
toggle-pin-scope() => { Library.library-toggle-pin-scope(); }
|
||||
change-library() => { Library.library-change(); }
|
||||
|
||||
select(id) => { root.collection-select(id); }
|
||||
toggle(id) => { Collections.collection-toggle(id); }
|
||||
@@ -1411,15 +1358,15 @@ in property <bool> panel-visible: true;
|
||||
expanded: root.expanded;
|
||||
cells: Library.library-cells;
|
||||
total: Library.library-total;
|
||||
scanning: root.library-scanning;
|
||||
opening: root.library-opening;
|
||||
scan-status: root.library-status;
|
||||
scan-error: root.library-error;
|
||||
scanning: Library.library-scanning;
|
||||
opening: Library.library-opening;
|
||||
scan-status: Library.library-status;
|
||||
scan-error: Library.library-error;
|
||||
|
||||
offline: root.library-offline;
|
||||
offline-reason: root.library-offline-reason;
|
||||
offline-since: root.library-offline-since;
|
||||
retry-connection() => { root.library-retry-connection(); }
|
||||
offline: Library.library-offline;
|
||||
offline-reason: Library.library-offline-reason;
|
||||
offline-since: Library.library-offline-since;
|
||||
retry-connection() => { Library.library-retry-connection(); }
|
||||
local-only: Library.library-local-only;
|
||||
local-count: Library.library-local-count;
|
||||
toggle-local-only() => { Library.library-toggle-local-only(); }
|
||||
@@ -1445,8 +1392,8 @@ in property <bool> panel-visible: true;
|
||||
collections-visible: root.collections-visible;
|
||||
toggle-collections() => { root.toggle-collections(); }
|
||||
|
||||
pin-done: root.library-pin-done;
|
||||
pin-total: root.library-pin-total;
|
||||
pin-done: Library.library-pin-done;
|
||||
pin-total: Library.library-pin-total;
|
||||
root-label: Library.library-root-label;
|
||||
timeline: Library.library-timeline;
|
||||
timeline-label: Library.library-timeline-label;
|
||||
@@ -1469,12 +1416,12 @@ in property <bool> panel-visible: true;
|
||||
}
|
||||
|
||||
// TRACES: FR-EXP-7
|
||||
exporting: root.library-exporting;
|
||||
exporting: Library.library-exporting;
|
||||
export-to-server: root.export-to-server;
|
||||
export-selection => { root.library-export-selection(); }
|
||||
cancel-export => { root.library-cancel-export(); }
|
||||
export-selection => { Library.library-export-selection(); }
|
||||
cancel-export => { Library.library-cancel-export(); }
|
||||
// TRACES: FR-MRG-1
|
||||
merge-selection => { root.library-merge-selection(); }
|
||||
merge-selection => { Library.library-merge-selection(); }
|
||||
|
||||
sweep-done: Library.library-sweep-done;
|
||||
sweep-total: Library.library-sweep-total;
|
||||
@@ -1487,7 +1434,7 @@ in property <bool> panel-visible: true;
|
||||
timeline-pinch(r) => { Library.library-timeline-pinch(r); }
|
||||
timeline-pan(d) => { Library.library-timeline-pan(d); }
|
||||
timeline-zoom(d) => { Library.library-timeline-zoom(d); }
|
||||
syncing: root.library-syncing;
|
||||
syncing: Library.library-syncing;
|
||||
requested-cell-size: Library.library-cell-size;
|
||||
touched <=> Library.library-touched;
|
||||
zoom-cells(d) => { Library.library-zoom-cells(d); }
|
||||
@@ -1495,7 +1442,7 @@ in property <bool> panel-visible: true;
|
||||
pinch-started() => { Library.library-pinch-started(); }
|
||||
scroll-to: Library.library-scroll-to;
|
||||
scroll-token: Library.library-scroll-token;
|
||||
sync-now() => { root.library-sync-now(); }
|
||||
sync-now() => { Library.library-sync-now(); }
|
||||
columns-changed(n) => {
|
||||
Library.library-columns = n;
|
||||
// Which cell begins a row just changed, and month headings sit on
|
||||
@@ -1505,7 +1452,7 @@ in property <bool> panel-visible: true;
|
||||
scrolled(i) => { Library.library-scrolled(i); }
|
||||
viewport-cells-changed(n) => { Library.library-viewport-cells(n); }
|
||||
cell-clicked(i) => { Library.library-cell-clicked(i); }
|
||||
rescan() => { root.library-rescan(); }
|
||||
rescan() => { Library.library-rescan(); }
|
||||
can-import: root.import-supported;
|
||||
open-import() => { root.import-open(); }
|
||||
open-settings() => { root.settings-open(); }
|
||||
|
||||
@@ -694,6 +694,59 @@ export global Library {
|
||||
in property <[PersonChip]> library-people;
|
||||
callback library-people-listed();
|
||||
callback library-filter-person-toggled(int);
|
||||
in property <bool> library-scanning: false;
|
||||
/// The catalog on this device is being opened and checked, on a worker,
|
||||
/// before the scan starts (`library_ui::open_catalog_soon`).
|
||||
///
|
||||
/// Separate from `library-scanning` because they are different answers to
|
||||
/// the same question: this one is reading a file that is already here, and
|
||||
/// that one is walking a tree over the network. Both are true at once for
|
||||
/// the first moment of a launch, and only the more specific of them is
|
||||
/// worth putting on an empty grid.
|
||||
in property <bool> library-opening: false;
|
||||
in property <string> library-status: "";
|
||||
in property <string> library-error: "";
|
||||
// --- offline mode (FR-CAT-9) ---
|
||||
//
|
||||
// The library keeps working from local data when the server cannot be
|
||||
// reached: thumbnails come from the shards, and rating and sorting are
|
||||
// catalog operations that never needed the network. What stops is opening
|
||||
// an original that was never stored locally.
|
||||
//
|
||||
// `offline-reason` carries the transport's own message ("connection
|
||||
// refused", "dns error") because it is usually specific enough to act on,
|
||||
// where a bare "offline" leaves the user guessing whether it is their wifi
|
||||
// or the server.
|
||||
in property <bool> library-offline: false;
|
||||
in property <string> library-offline-reason: "";
|
||||
in property <string> library-offline-since: "";
|
||||
callback library-retry-connection();
|
||||
// --- pinning a collection offline (FR-NC-6a) ---
|
||||
in property <bool> library-scope-pinned: false;
|
||||
in property <int> library-pin-done: 0;
|
||||
in property <int> library-pin-total: 0;
|
||||
callback library-toggle-pin-scope();
|
||||
in property <bool> library-syncing: false;
|
||||
/// TRACES: FR-CAT-3 | FR-NC-3
|
||||
/// The whole-library thumbnail pass, launched from the settings page.
|
||||
/// `library-open` gates the button on there being a library to sweep —
|
||||
/// the page is reachable from the launch screen too.
|
||||
in property <bool> library-thumbnailing: false;
|
||||
in property <bool> library-open: false;
|
||||
callback library-thumbnail-all();
|
||||
callback library-sync-now();
|
||||
callback library-rescan();
|
||||
/// Grid → launch screen, to pick a different folder or account.
|
||||
callback library-change();
|
||||
/// Merge the grid's selection: opens the page and starts the alignment.
|
||||
callback library-merge-selection();
|
||||
/// Whether a batch is running. The grid's export button becomes its cancel
|
||||
/// button; there is nowhere else a long batch can be stopped from.
|
||||
in property <bool> library-exporting: false;
|
||||
/// Export the grid's selection in the background (FR-EXP-7).
|
||||
callback library-export-selection();
|
||||
/// TRACES: NFR-ARCH-3
|
||||
callback library-cancel-export();
|
||||
}
|
||||
|
||||
// The photo roll: the grid's loaded window along the foot of the develop view.
|
||||
|
||||
Reference in New Issue
Block a user