mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
commit
d71e7d11c1
2 files changed
+79
No files matched your search
@@ -1068,6 +1068,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(crate::i18n::tr(if transient { "Keep in Local" } else { "Add to Local" }))
|
||||
@@ -1111,6 +1114,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) {
|
||||
@@ -1822,6 +1828,38 @@ fn locate_missing_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNod
|
||||
}
|
||||
}
|
||||
|
||||
/// 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