LRC-LIB-FOLDERS: Move Folder To… waits while Open Library… or New Library… asks for a folder (#3140)

This commit is contained in:
Bruno-Alumn
2026-10-30 02:22:08 +01:00
parent 5d92a471de
commit 8b3d56142a
3 changed files with 52 additions and 2 deletions
+20 -1
View File
@@ -552,18 +552,37 @@ fn library_node_for(tree: &[FolderNode], path: &str) -> Option<(FolderNode, Stri
Some((node, name))
}
/// Move Folder To… can't start while Open Library… or New Library… waits for its folder: the
/// dialogs answer in the order they were opened ([`crate::pick::poll`]), so the library could
/// switch first and the move then run in the other library, moving the folder on disk with
/// nothing to relink (issue #3140).
fn can_move_folder(app: &LightcraftApp) -> Result<(), String> {
for (command, label) in [("app.openLibrary", "Open Library…"), ("app.newLibrary", "New Library…")] {
if app.pending_picks.iter().any(|p| p.command == command) {
return Err(format!("wait for {label} to finish before moving a folder"));
}
}
Ok(())
}
/// Move Folder To… of a folder row (widget `folderMoveTo`): asks for the destination through the
/// shared folder dialog, off the UI thread where the host has one (issue #3140: a dialog on the UI
/// thread froze the window); `folder.move` then runs when it closes ([`crate::pick::poll`]).
/// Disabled while a library dialog is up ([`can_move_folder`]).
fn move_folder_item(app: &mut LightcraftApp, ui: &mut egui::Ui, path: &str) {
if app.services.pick_folder.is_none() && app.services.picker.is_none() {
return;
}
let r = ui.button(crate::i18n::tr("Move Folder To…"));
let r = ui.add_enabled(can_move_folder(app).is_ok(), egui::Button::new(crate::i18n::tr("Move Folder To…")));
register(ui, "folderMoveTo", r.rect);
if !r.clicked() {
return;
}
if let Err(e) = can_move_folder(app) {
app.toast(ui.ctx(), e);
ui.close();
return;
}
let req = crate::pick::PickRequest::folder(crate::i18n::tr("Move Folder To…"));
let params = json!({"path": path});
let picked = crate::pick::ask(app, "folder.move", &params, "into", req, |s| s.pick_folder.as_mut().and_then(|f| f()).map(|x| vec![x]));
+30
View File
@@ -179,3 +179,33 @@ fn no_library_switch_while_move_folder_to_waits_for_its_destination() {
h.app.run("app.openLibrary", json!({"path": other})).expect("the switch works once the move is done");
assert_eq!(library(&h), Some(other));
}
/// Issue #3140: while Open Library… waits for its folder, Move Folder To… can't start (both
/// dialogs answer in the order opened, so the library could switch before the move ran, which
/// would then move the folder in the other library with nothing to relink): the item is disabled,
/// no destination is asked for, and the folder stays. Once the library dialog is cancelled it works.
#[test]
fn move_folder_to_waits_for_open_library_to_finish() {
let dir = Scratch::new("after-open");
std::fs::create_dir_all(dir.path("dest")).unwrap();
let (mut h, asked, blocking) = app_with_trip(&dir);
assert_eq!(h.app.run("app.openLibrary", json!({})).ok(), Some(serde_json::Value::Null));
assert_eq!(asked.lock().unwrap().len(), 1, "the Open Library dialog is up");
open_row_menu(&mut h, &format!("source:libfolder:{}", dir.path("trip")));
let _ = h.request("ui.clickWidget", json!({"id": "folderMoveTo"}), T);
h.settle(SETTLE);
assert_eq!(asked.lock().unwrap().len(), 1, "no destination is asked for");
assert_eq!(blocking.load(Ordering::Relaxed), 0, "not on the UI thread either");
assert!(!h.app.pending_picks.iter().any(|p| p.command == "folder.move"), "nothing waits to move");
assert!(std::path::Path::new(&dir.path("trip/a.png")).exists(), "the folder stays");
// the Open Library dialog is cancelled: Move Folder To… asks for its destination again
asked.lock().unwrap()[0].1.send(vec![]).unwrap();
h.settle(SETTLE);
assert!(h.app.pending_picks.is_empty());
open_row_menu(&mut h, &format!("source:libfolder:{}", dir.path("trip")));
let r = h.request("ui.clickWidget", json!({"id": "folderMoveTo"}), T);
assert_eq!(r["ok"], true, "{r}");
h.step();
assert_eq!(asked.lock().unwrap().len(), 2, "the destination is asked for");
assert!(h.app.pending_picks.iter().any(|p| p.command == "folder.move"));
}
+2 -1
View File
@@ -7,7 +7,8 @@
Folder… do: the window keeps drawing and answering while you choose, and the folder moves when the dialog closes,
with the same "Moved; N photo(s) relinked" message. Before, the dialog held up the whole window until it closed;
on Linux with X11 the app could stop responding altogether. While the dialog is open, another library can't be
opened (the move belongs to the library it was started in).
opened (the move belongs to the library it was started in), and while Open Library… or New Library… waits for
its folder, Move Folder To… is unavailable.
## 2026-10-24