mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 04:27:30 -06:00
LR-SIDEBAR-LOCAL-SYNC: Synchronize Folder in the Local folder menu
A Local folder that holds library photos (or has a folder with some inside) now offers Synchronize Folder... like the Folders section. The check reads the cached Folders tree only. Headless test covers shown and not shown.
This commit is contained in:
@@ -1024,6 +1024,9 @@ fn folder_tree(
|
||||
app.toast(ui.ctx(), e);
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
// the library actions of the Folders tree, when this folder (or one inside it) holds library photos
|
||||
let library = app.caches.folder_tree(&app.session.catalog);
|
||||
let held = library_node_for(&library, path);
|
||||
if (transient || indent > 0.0)
|
||||
&& ui
|
||||
.button(if transient { "Keep in Local" } else { "Add to Local" })
|
||||
@@ -1067,6 +1070,9 @@ fn folder_tree(
|
||||
{
|
||||
let _ = f(path);
|
||||
}
|
||||
if let Some(held) = &held {
|
||||
synchronize_item(app, ui, &held.0, &held.1);
|
||||
}
|
||||
});
|
||||
if open && indent < 12.0 * 8.0 {
|
||||
for (n, p) in subfolders(ui, path) {
|
||||
@@ -1721,6 +1727,38 @@ fn row_menu(app: &mut LightcraftApp, resp: &egui::Response, n: &FolderNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The node Synchronize Folder… of a Local folder `path` works on, with the name its dialog shows:
|
||||
/// the Folders row for that very folder, else (a folder above rows) the folder itself, as a folder
|
||||
/// holding other folders. `None` when no library photo lies in `path` or inside it. Reads the
|
||||
/// cached Folders tree only; the disk is not touched.
|
||||
fn library_node_for(tree: &[FolderNode], path: &str) -> Option<(FolderNode, String)> {
|
||||
use lightcraft_catalog::query::folder_within;
|
||||
fn exact<'a>(tree: &'a [FolderNode], path: &str) -> Option<&'a FolderNode> {
|
||||
tree.iter().find_map(|n| if same_folder(&n.path, path) { Some(n) } else { exact(&n.children, path) })
|
||||
}
|
||||
fn any_inside(tree: &[FolderNode], path: &str) -> bool {
|
||||
tree.iter().any(|n| (n.count > 0 && folder_within(&n.path, path)) || any_inside(&n.children, path))
|
||||
}
|
||||
if let Some(n) = exact(tree, path).filter(|n| n.count > 0) {
|
||||
return Some((n.clone(), lightcraft_catalog::folders::folder_label(&n.path)));
|
||||
}
|
||||
if !any_inside(tree, path) {
|
||||
return None;
|
||||
}
|
||||
let name = std::path::Path::new(path).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| path.to_string());
|
||||
let node = FolderNode {
|
||||
name: name.clone(),
|
||||
path: path.to_string(),
|
||||
count: 0,
|
||||
own: 0,
|
||||
volume: false,
|
||||
selectable: false,
|
||||
label: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
Some((node, name))
|
||||
}
|
||||
|
||||
/// Synchronize Folder… of a Folders row (widget `folderSynchronize`): opens its dialog, which
|
||||
/// scans the folder in the background.
|
||||
fn synchronize_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode, name: &str) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! * Right-click a folder in the sidebar's Folders section ▸ Synchronize Folder…: a dialog opens
|
||||
//! at once and scans the folder without holding up the app, then says how many photos are new,
|
||||
//! missing, or have metadata updates.
|
||||
//! * Right-click a folder in the sidebar's Local section: Synchronize Folder… is there when the
|
||||
//! folder holds library photos (or has a folder with some inside), and not when it holds none.
|
||||
//! * Synchronize with the defaults imports the new photos and leaves missing ones alone.
|
||||
//! * Ticking "Remove missing photos" also moves those to Recently Deleted.
|
||||
//! * Cancel leaves the library as it was.
|
||||
@@ -302,3 +304,42 @@ fn the_folders_scan_shows_a_row_and_its_cross_closes_the_dialog() {
|
||||
h.settle(SETTLE);
|
||||
assert!(h.app.ui.dialog.is_none(), "a stopped scan opens nothing later");
|
||||
}
|
||||
|
||||
/// Right-click the Local row of `folder`; whether Synchronize Folder… is in its menu.
|
||||
fn local_menu_offers_synchronize(h: &mut Headless, folder: &str) -> bool {
|
||||
let row = format!("source:local:{folder}");
|
||||
assert!(h.step_until(T, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "no row {row}");
|
||||
let rect = h.app.widgets.iter().find(|(w, _)| *w == row).map(|(_, r)| *r).unwrap();
|
||||
let c = rect.center();
|
||||
let r = h.request("ui.click", json!({"x": c.x, "y": c.y, "button": "right"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
h.app.widgets.iter().any(|(w, _)| *w == "folderSynchronize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_local_folder_with_library_photos_offers_synchronize() {
|
||||
let dir = Scratch::new("local-menu");
|
||||
let mut h = changed_folder(&dir);
|
||||
std::fs::create_dir_all(dir.path("empty")).unwrap();
|
||||
// Local writes paths the way the OS does
|
||||
let native = |f: &str| dir.0.join(f).to_string_lossy().to_string();
|
||||
for f in ["trip", "empty"] {
|
||||
let r = h.request("engine.execute", json!({"command": "local.addRoot", "params": {"path": native(f)}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
h.step();
|
||||
assert!(!local_menu_offers_synchronize(&mut h, &native("empty")), "no library photos in it: nothing to synchronize");
|
||||
// close that menu, then open the other folder's
|
||||
let r = h.request("ui.key", json!({"key": "Escape"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(local_menu_offers_synchronize(&mut h, &native("trip")), "the folder holds library photos");
|
||||
let r = h.request("ui.clickWidget", json!({"id": "folderSynchronize"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::SynchronizeFolder { .. })), "the dialog opens at once");
|
||||
assert!(h.step_until(T, |h| matches!(&h.app.ui.dialog, Some(Dialog::SynchronizeFolder { counts: Some(_), .. }))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user