Sync develop presets through the library
Presets were the one piece of the photographer's work that never left the device: faces, sidecars, albums, collections, keywords and camera profiles all travel with the sync pass, the preset library did not. It now goes to <derived>/presets/library.drpl. PresetLibrary::merge decides each name against the base the last exchange left (kept per library beside place.json), so presets added on two devices both survive, a deletion reaches the other device instead of being restored by it, and an edit outlives a deletion made elsewhere. The upload is If-Match / If-None-Match on the server's copy, and a 412 reads and merges again, so two devices exchanging at once cannot save over each other. A server copy that will not parse (a newer build's) is left alone, and a local file that will not read stops the exchange rather than being taken for an empty library. The develop view's save merges with the file when the sync changed it since the view read it, and a sync that brought presets reloads and redraws the list. Also corrects the register, which still said camera profiles do not sync.
This commit is contained in:
+86
-7
@@ -723,6 +723,14 @@ pub fn wire(
|
||||
pub struct NamedPresets {
|
||||
store: PresetStore,
|
||||
library: RefCell<PresetLibrary>,
|
||||
/// The file as this window last read or wrote it.
|
||||
///
|
||||
/// The sync rewrites the file from another thread when presets arrive
|
||||
/// from another device. Saving the in-memory copy over that would delete
|
||||
/// them here, and the next exchange would carry the deletion everywhere.
|
||||
/// So a save first checks whether the file still holds this, and merges
|
||||
/// with it as the base if not (`PresetLibrary::merge`).
|
||||
on_disk: RefCell<PresetLibrary>,
|
||||
/// The category folders the photographer has opened, by key (see
|
||||
/// [`PresetTree`]). Every folder starts closed, so a list of seventy
|
||||
/// presets arrives as six lines; for the session, not saved, as the
|
||||
@@ -752,16 +760,35 @@ impl NamedPresets {
|
||||
// writes the file without them, and until then nothing is lost,
|
||||
// because each one is identical to the preset now shipped in its
|
||||
// place.
|
||||
let mut library = store.load();
|
||||
let on_disk = store.load();
|
||||
let library = Self::shown(&on_disk);
|
||||
Rc::new(Self {
|
||||
store,
|
||||
library: RefCell::new(library),
|
||||
on_disk: RefCell::new(on_disk),
|
||||
open: RefCell::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// What the window lists from what the file holds.
|
||||
fn shown(on_disk: &PresetLibrary) -> PresetLibrary {
|
||||
let mut library = on_disk.clone();
|
||||
let forgotten = dr_pipeline::bundled::forget_unchanged_copies(&mut library);
|
||||
if forgotten > 0 {
|
||||
log::info!("{forgotten} seeded preset copies are now shipped presets");
|
||||
}
|
||||
Rc::new(Self {
|
||||
store,
|
||||
library: RefCell::new(library),
|
||||
open: RefCell::default(),
|
||||
})
|
||||
library
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-6
|
||||
/// Read the file again, after a sync brought presets from another device.
|
||||
///
|
||||
/// Every change is saved as it is made, so there is nothing in memory
|
||||
/// this could discard.
|
||||
pub fn reload(&self) {
|
||||
let on_disk = self.store.load();
|
||||
*self.library.borrow_mut() = Self::shown(&on_disk);
|
||||
*self.on_disk.borrow_mut() = on_disk;
|
||||
}
|
||||
|
||||
/// Read presets from a file or a folder and store them.
|
||||
@@ -930,9 +957,22 @@ impl NamedPresets {
|
||||
/// the write failed and leave what is on screen matching what is in
|
||||
/// memory until the next successful save.
|
||||
fn persist(&self, rollback: impl FnOnce(&mut PresetLibrary)) -> Result<(), SaveError> {
|
||||
// A file that changed since it was read here was the sync; take what
|
||||
// it brought rather than saving over it. One that will not read is
|
||||
// left to the save, which says so.
|
||||
if let Ok(Some(now)) = self.store.try_load() {
|
||||
if now != *self.on_disk.borrow() {
|
||||
let merged =
|
||||
PresetLibrary::merge(&self.on_disk.borrow(), &self.library.borrow(), &now);
|
||||
*self.library.borrow_mut() = merged;
|
||||
}
|
||||
}
|
||||
let result = self.store.save(&self.library.borrow());
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Ok(()) => {
|
||||
*self.on_disk.borrow_mut() = self.library.borrow().clone();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
rollback(&mut self.library.borrow_mut());
|
||||
// Named, the way the settings page names its file: "could not
|
||||
@@ -1188,6 +1228,7 @@ pub fn wire_named(
|
||||
open,
|
||||
} = develop;
|
||||
|
||||
*library.named_presets.borrow_mut() = Some(Rc::downgrade(&named));
|
||||
render_named(window, &named);
|
||||
|
||||
// --- open and close a category folder ---------------------------------
|
||||
@@ -1974,6 +2015,44 @@ mod tests {
|
||||
assert_eq!(reloaded.names(), vec!["Warm".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_save_keeps_what_the_sync_wrote_since_the_window_read_the_file() {
|
||||
// TRACES: FR-DEV-6
|
||||
// The sync rewrites the file from its own thread. Saving the window's
|
||||
// copy over it would delete what arrived, and the next exchange would
|
||||
// carry that deletion to the device that sent it.
|
||||
let (presets, dir) = named("sync-wrote-meanwhile");
|
||||
let path = dir.join("presets.drpl");
|
||||
let mut arrived = PresetLibrary::default();
|
||||
arrived
|
||||
.insert("From the tablet", Preset::default())
|
||||
.unwrap();
|
||||
PresetStore::open_at(path.clone()).save(&arrived).unwrap();
|
||||
|
||||
presets.insert("Warm", Preset::capture(&edited())).unwrap();
|
||||
|
||||
let reloaded = NamedPresets::open_at(path);
|
||||
assert_eq!(
|
||||
reloaded.names(),
|
||||
vec!["From the tablet".to_string(), "Warm".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reload_lists_what_the_sync_brought() {
|
||||
// TRACES: FR-DEV-6
|
||||
let (presets, dir) = named("reload-after-sync");
|
||||
let mut arrived = PresetLibrary::default();
|
||||
arrived
|
||||
.insert("From the tablet", Preset::default())
|
||||
.unwrap();
|
||||
PresetStore::open_at(dir.join("presets.drpl"))
|
||||
.save(&arrived)
|
||||
.unwrap();
|
||||
presets.reload();
|
||||
assert_eq!(presets.names(), vec!["From the tablet".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_preset_carries_the_edit_it_captured() {
|
||||
let (presets, _dir) = named("carries-the-edit");
|
||||
|
||||
Reference in New Issue
Block a user