Merge #2701@52ce53622 (photos dropped with no library open wait for one) into round3a-integration

crates/ui-egui/src/lib.rs: the batch's handle_dropped_files holds the photos
(library_problem::hold_dropped) before the Lightroom / running-import
checks. The drop card (drop_overlay.rs, LR-IMP-DRAGDROP) says the photos
wait instead of announcing an import or a review; test
photos_dropped_with_no_library_open_wait_for_one drops through
`ui.dropFiles`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
francoisgn
2026-10-23 21:55:16 +03:00
co-authored by Claude Opus 5.5
7 changed files with 79 additions and 5 deletions
+2 -1
View File
@@ -2384,5 +2384,6 @@
"The library's catalog can't be read:": "Le catalogue de la bibliothèque ne peut pas être lu :",
"Not answering yet: the drive may be asleep or offline": "Pas encore de réponse : le disque est peut-être en veille ou hors ligne",
"This library couldn't be checked": "Impossible de vérifier cette bibliothèque",
"Checking recent libraries": "Vérification des bibliothèques récentes"
"Checking recent libraries": "Vérification des bibliothèques récentes",
"The photos are imported once a library is open": "Les photos seront importées dès qu'une bibliothèque sera ouverte"
}
+4 -1
View File
@@ -40,7 +40,10 @@ fn lines(app: &LightcraftApp, paths: Vec<String>, unknown: bool) -> Vec<String>
let n = drop.presets.len();
out.push(crate::i18n::tr_format!("Drop to import {n} preset{}", plural(n), n = n));
}
if !drop.photos.is_empty() && app.lightroom.is_some() && !cfg!(target_arch = "wasm32") {
if !drop.photos.is_empty() && crate::panels::library_problem::holds_drops(app) {
// no library open yet: they wait for the one chosen (the drop handler holds them)
out.push(crate::i18n::tr("The photos are imported once a library is open").to_string());
} else if !drop.photos.is_empty() && app.lightroom.is_some() && !cfg!(target_arch = "wasm32") {
// the drop is refused while a Lightroom catalog import owns the catalog
out.push(crate::i18n::tr("Finish Lightroom catalog import before adding photos").to_string());
} else if !drop.photos.is_empty() && crate::import::running(app) && !cfg!(target_arch = "wasm32") {
+4 -2
View File
@@ -1305,6 +1305,8 @@ impl LightcraftApp {
if drop.photos.is_empty() {
return;
}
// no library open yet (the launch list, a library that failed to open): they wait for one
let Some(photos) = panels::library_problem::hold_dropped(self, ctx, drop.photos) else { return };
if self.lightroom.is_some() {
self.toast(ctx, crate::i18n::tr("Finish Lightroom catalog import before adding photos"));
return;
@@ -1315,9 +1317,9 @@ impl LightcraftApp {
}
let started = if self.ui.settings.import_dropped_directly {
// read and added on a worker thread (dropped folders can be large, or on a slow drive)
import::start_paths(self, drop.photos).map(|_| ())
import::start_paths(self, photos).map(|_| ())
} else {
import::open(self, drop.photos).map(|_| ())
import::open(self, photos).map(|_| ())
};
if let Err(e) = started {
self.toast(ctx, e);
@@ -426,6 +426,26 @@ enum Choice {
Keep,
}
/// Whether dropped photos wait for a library ([`hold_dropped`]): none is open and the window
/// asking for one is up (not dismissed by Continue Without Saving).
pub fn holds_drops(app: &LightcraftApp) -> bool {
app.session.library.is_none() && app.library_problem.as_ref().is_some_and(|p| !p.dismissed)
}
/// Photos dropped on the window while no library is open (the launch list, or the window of a
/// library that failed to open): kept with the command line's files and imported once a library
/// opens, instead of going into the session the next library replaces. Gives the photos back when
/// a library is open or Continue Without Saving was chosen.
pub fn hold_dropped(app: &mut LightcraftApp, ctx: &egui::Context, photos: Vec<String>) -> Option<Vec<String>> {
if !holds_drops(app) {
return Some(photos);
}
let problem = app.library_problem.as_mut()?;
problem.pending_import.extend(photos);
app.toast(ctx, crate::i18n::tr("The photos are imported once a library is open"));
None
}
/// Open `path` (`None`: ask for a folder) through `app.openLibrary`; on success the problem is
/// over (and command-line files are imported), else it shows the new error.
fn open(app: &mut LightcraftApp, path: Option<String>) {
+17
View File
@@ -251,3 +251,20 @@ fn drop_files_with_a_non_string_path_is_rejected_and_drops_nothing() {
assert!(h.app.ui.dialog.is_none(), "nothing was dropped");
assert_eq!(photos(&h), 0);
}
/// No library open yet (the window of a library that failed to open, or the launch list): the
/// card says the photos wait, and a drop keeps them for the library chosen instead of importing
/// them into the session that library replaces.
#[test]
fn photos_dropped_with_no_library_open_wait_for_one() {
let dir = Scratch::new("no-library");
let mut h = app(&dir);
h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new(dir.path("Gone"), "not found"));
drop_files(&mut h, &[dir.path("trip/a.png")], true);
assert!(said(&h, "once a library is open"), "{:?}", h.painted_text());
drop_files(&mut h, &[], true);
drop_files(&mut h, &[dir.path("trip/a.png"), dir.path("trip/b.png")], false);
assert_eq!(h.app.library_problem.as_ref().map(|p| p.pending_import.len()), Some(2));
assert!(h.app.ui.dialog.is_none() && h.app.import.is_none(), "no review, no import into the temporary session");
assert_eq!(photos(&h), 0);
}
@@ -832,3 +832,32 @@ fn a_failed_open_or_an_unreadable_catalog_doesnt_mark_a_library_missing() {
assert!(matches!(open_recent(&h.app).first(), Some(MenuNode::Item { enabled: true, .. })));
let _ = std::fs::remove_dir_all(&dir);
}
/// Photos dropped on the launch list wait for the library chosen (CodeRabbit on #2689): before,
/// they went into the session the library then replaced, and were gone from view.
#[test]
fn photos_dropped_on_the_launch_list_go_into_the_library_chosen() {
let dir = temp("drop");
let a = make_library(&dir.join("Alpha"));
let mut h = asking(vec![a.clone()], &a, None);
let img = lightcraft_raster::Rgba8 { width: 8, height: 8, data: vec![[90, 3, 9, 255]; 64] };
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
let photo = dir.join("a.png");
std::fs::write(&photo, png).unwrap();
let ctx = h.view.ctx.clone();
let photos = vec![photo.to_string_lossy().into_owned()];
assert!(crate::panels::library_problem::holds_drops(&h.app));
assert_eq!(crate::panels::library_problem::hold_dropped(&mut h.app, &ctx, photos.clone()), None, "held while no library is open");
assert_eq!(h.app.library_problem.as_ref().map(|p| p.pending_import.clone()), Some(photos.clone()));
assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0.contains("once a library is open")), "{:?}", h.app.ui.toast);
assert_eq!(h.app.session.catalog.len(), 0, "nothing goes into the session the library replaces");
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryOpen"}), T)["ok"], true);
settle(&mut h);
h.step_until(T, |h| h.app.import.is_none() && h.app.session.catalog.len() == 1);
assert_eq!(h.app.session.catalog.len(), 1, "the dropped photo is imported into the library opened");
// with a library open, a drop is the usual import
assert!(!crate::panels::library_problem::holds_drops(&h.app));
assert_eq!(crate::panels::library_problem::hold_dropped(&mut h.app, &ctx, photos.clone()), Some(photos));
drop(h);
let _ = std::fs::remove_dir_all(&dir);
}
+3 -1
View File
@@ -166,7 +166,9 @@ catalog, a missing drive), the session starts empty and in memory — never with
to do: `ui.inspect` → `libraryProblem` is `{path, error, temporarySession, pendingImport}` (else `null`); its buttons
are `button:libraryRetry`, `button:libraryChoose`, `button:libraryTemporary` (Continue Without Saving) and
`button:libraryQuit`. A temporary session shows a banner (`indicator:temporarySession`, `button:libraryReopen`) and
writes nothing. `app.openLibrary` opening a library ends it.
writes nothing. `app.openLibrary` opening a library ends it. `pendingImport` counts the files waiting for a
library: those given on the command line, and photos dropped on the window while it (or the launch list below) is
up; they are imported into the library that opens, or into the temporary session.
### Choosing the library at launch