Catalogue a merged panorama the moment it is written

A finished merge drained the outbox and started a rescan beside it. The
scan raced the upload: on a folder library the 800 MB copy was still
running when the folder was listed, on Nextcloud the upload takes
minutes, and either way the listing lacked the composite, recorded the
folder's validator, and nothing looked again until the next sync pass.

The job now reports what the catalog needs (the name, the size of the
picture it opens on, the capture time it wrote into the DNG) and the
library writes the row at once, in one transaction, keyed where the scan
will list the file; a composite with no time of its own takes its
sources' earliest. The grid reloads and shows it beside its sources.

The upload then gives the row what only the server knows: after sending
a file the catalog already has a row for, it lists the folder once, takes
the file id the server assigned, and records it (and, once the merge
makes them, the thumbnails waiting beside the payload) under that id.
Every drain now rescans when something landed in the library, after the
upload rather than beside it. A second merge of the same frames is no
longer named over the first: the name is checked against the catalog's
names in that folder, which is all that knows it once the outbox is empty.
This commit is contained in:
2026-09-28 19:57:37 -04:00
parent 37136f7377
commit 98a67393d9
11 changed files with 1335 additions and 89 deletions
+22 -24
View File
@@ -731,7 +731,7 @@ fn batch_request(
/// the first is what makes an upload feel immediate, and the second is what
/// eventually delivers the exports made while the train was in a tunnel.
/// Running it twice over an empty outbox costs a directory listing.
fn drain_outbox(library: &Rc<library_ui::LibraryController>) {
fn drain_outbox(library: &Rc<library_ui::LibraryController>, window: slint::Weak<AppWindow>) {
// Offline is not a failure worth reporting here — the entries stay
// staged and the next pass takes them.
if library.is_offline() {
@@ -747,23 +747,7 @@ fn drain_outbox(library: &Rc<library_ui::LibraryController>) {
let root = conn.account.root.clone();
let rx = export::spawn_upload(conn, root, outbox);
executors::spawn(executors::Executor::Io, "upload-log", move || {
while let Ok(msg) = rx.recv() {
match msg {
export::UploadMessage::Status(s) => log::info!("export: {s}"),
export::UploadMessage::Finished {
uploaded,
remaining,
error,
} => {
log::info!("export: {uploaded} uploaded, {remaining} still queued");
if let Some(e) = error {
log::warn!("export upload stopped: {e}");
}
}
}
}
});
export::watch_upload(rx, window);
}
/// What the export button should say, given where an export would go.
@@ -1901,17 +1885,31 @@ fn wire_import_and_merge(
move || library_for_sources.export_sources(&collections_for_sources.selected()),
move || {
let conn = library_for_context.session()?;
let catalog = library_for_context.catalog();
let root = conn.account.root.clone();
Some(merge_ui::Context {
outbox: export::outbox_dir(&conn.account),
conn,
names_in: Box::new(move |folder| {
catalog
.borrow()
.as_ref()
.map(|c| library::names_in_folder(c, &root, folder))
.unwrap_or_default()
}),
})
},
move |w| {
// Staged beside its sources: upload it now rather than
// on the next sync pass, then look for it, exactly as an
// import does.
drain_outbox(&library_for_done);
w.global::<Library>().invoke_library_rescan();
move |w, placed| {
// TRACES: FR-MRG-6
// In the grid now, from what the merge knows: a scan
// started here raced the upload and did not find it.
if let Some(placed) = placed {
library_ui::catalogue_composite(w, &library_for_done, placed);
}
// Upload it now rather than on the next sync pass; the
// scan that follows the upload gives the row the file id
// the server assigned.
drain_outbox(&library_for_done, w.as_weak());
},
);
}