Merge: origin's sync ordering and adoption work, its scan-complete trigger ported into library_ui/

This commit is contained in:
2026-09-21 11:32:05 +02:00
13 changed files with 432 additions and 43 deletions
+14 -3
View File
@@ -67,6 +67,9 @@ pub struct SyncReport {
/// devices already had gains *no* collection, and reporting only the
/// former left the sidebar showing no count beside a full collection.
pub members_gained: usize,
/// Images dated from the remote's snapshot rather than by this device's
/// own sweep — what makes the timeline whole on a fresh device.
pub dates_gained: usize,
// Face data is counted apart from thumbnails for the same reason keywords
// are counted apart from collections: "adopted 4,812 faces" is a sentence
@@ -195,9 +198,13 @@ async fn run(
// cheaper than probing, and an existing folder is not an error.
let _ = backend.create_dir(&base).await;
let _ = tx.send(SyncMessage::Status("checking thumbnails…".into()));
sync_shards(backend, &base, thumbs_dir, scratch, &mut report).await?;
// Small before large, and what the user is waiting for before what
// fills in behind them. Faces first because the catalog merge assigns
// identities to faces this device holds, so they must be here by then;
// the catalog next for collections, people and dates; thumbnails last,
// because a fresh device's thumbnail stage is hundreds of megabytes and
// everything queued behind it — the sidebar, the timeline, the names —
// was invisible for as long as it ran.
let _ = tx.send(SyncMessage::Status("checking faces…".into()));
sync_face_shards(
backend,
@@ -213,6 +220,9 @@ async fn run(
let _ = tx.send(SyncMessage::Status("checking collections…".into()));
sync_catalog(backend, &base, catalog_path, scratch, &mut report).await?;
let _ = tx.send(SyncMessage::Status("checking thumbnails…".into()));
sync_shards(backend, &base, thumbs_dir, scratch, &mut report).await?;
// TRACES: FR-UI-8
// Last, and it costs one small GET plus at most one small PUT. Last because
// it is the only thing here that is not derived state and so the only thing
@@ -815,6 +825,7 @@ fn merge_downloaded(
report.catalog_merged = true;
report.collections_gained += merge.inserted + merge.updated;
report.members_gained += merge.members_added;
report.dates_gained += merge.metadata_adopted;
Ok(())
}
+10 -1
View File
@@ -23,7 +23,7 @@ use crate::{AppWindow, Library, View};
use super::controller::{stop, LibraryController};
use super::offline::refresh_offline;
use super::ratings_keywords::refresh_xmp_conflicts;
use super::sync::start_sweep;
use super::sync::{start_derived_sync, start_sweep};
use super::timeline::{apply_place, note_place};
use super::window::{load_window, window_start};
@@ -494,6 +494,15 @@ fn drain_scan(
}
}
load_window(&w, ctl);
// Take the server's shards and catalog *now*,
// before the sweep: the rows they key on exist
// from this moment, and on a fresh device
// every thumbnail, face and collection a peer
// has already made is on the server. Waiting
// for the sweep — hours on a large library —
// meant re-deriving all of it here first. In
// steady state this is one listing.
start_derived_sync(&w, ctl);
// Everything the grid did not touch: the rest
// of the library gets a thumbnail and a date,
// so the timeline describes all of it rather
+21 -4
View File
@@ -49,8 +49,11 @@ fn spawn_scheduled_backup(ctl: &Rc<LibraryController>) {
/// Push shards and the catalog to the server, and take what it has.
///
/// Fired after the sweep completes, when there is a finished index worth
/// sharing, and from the Sync button for an explicit exchange.
/// Fired when the scan completes, so a fresh device inherits its peers'
/// work before deriving any of its own; after the sweep completes, when
/// there is a finished index worth sharing; and from the Sync button for an
/// explicit exchange. A pass still running when the next trigger fires is
/// left to finish — the guard below.
pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>) {
// An escape hatch for running the app against a *copied* library without
// touching the account's real server.
@@ -174,7 +177,7 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
crate::derived_sync::SyncMessage::Finished(report) => {
log::info!(
"sync: {} shard(s) up, {} down ({} thumbnails), \
catalog {}{}",
catalog {}{}{}",
report.shards_uploaded,
report.shards_downloaded,
report.thumbnails_adopted,
@@ -191,6 +194,11 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
format!(", {} collection(s) gained", report.collections_gained)
} else {
String::new()
},
if report.dates_gained > 0 {
format!(", {} date(s) gained", report.dates_gained)
} else {
String::new()
}
);
w.global::<Library>().set_library_syncing(false);
@@ -208,13 +216,22 @@ pub(super) fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>
.set_library_status(format!("synced · {summary}").into());
}
// Adopted thumbnails and merged collections both change
// what the grid should show.
// what the grid should show; adopted dates change its
// order, and the timeline beside it.
if report.thumbnails_adopted > 0
|| report.collections_gained > 0
|| report.members_gained > 0
|| report.dates_gained > 0
{
load_window(&w, &ctl_cb);
}
if report.dates_gained > 0 {
let borrow = ctl_cb.catalog();
let borrow = borrow.borrow();
if let Some(cat) = borrow.as_ref() {
refresh_timeline(&w, cat, &ctl_cb);
}
}
// TRACES: FR-CAT-7
// And the sidebar, which the grid reload does not
// touch. Membership counts as a change: a sync that