Merge PR #3403: LRC-LIB-CATALOG-SWITCH: files waiting for a library wait for a running import instead of being dropped

crates/ui-egui/src/dialogs/settings/libraries.rs: can_switch_library keeps both refusals (folder.move pick pending, files waiting to import).
docs/whats-new.md: the PR's entry added under the 2026-10-30 heading.
This commit is contained in:
aiongg
2026-10-31 02:12:00 +00:00
27 changed files with 188 additions and 20 deletions
+1
View File
@@ -2410,6 +2410,7 @@
"Dropped Files": "Přetažené soubory",
"Import dropped files without review": "Importovat přetažené soubory bez kontroly",
"An import is running": "Import už běží",
"The photos are imported once it ends": "Fotky se naimportují potom",
"An import is running. Drop photos when it has finished": "Import už běží. Fotky přetáhněte, až skončí",
"Finish Lightroom catalog import before adding photos": "Dokončete import katalogu Lightroom, než přidáte fotky",
"Import Thumbnail Size": "Velikost miniatur při importu"
+1
View File
@@ -3442,6 +3442,7 @@
"Dropped Files": "Abgelegte Dateien",
"Import dropped files without review": "Abgelegte Dateien ohne Prüfung importieren",
"An import is running": "Ein Import läuft",
"The photos are imported once it ends": "Die Fotos werden danach importiert",
"An import is running. Drop photos when it has finished": "Ein Import läuft. Lege die Fotos ab, wenn er fertig ist",
"Finish Lightroom catalog import before adding photos": "Schließe den Import des Lightroom-Katalogs ab, bevor du Fotos hinzufügst",
"Import Thumbnail Size": "Vorschaubildgröße beim Import"
+1
View File
@@ -2360,6 +2360,7 @@
"Dropped Files": "Archivos soltados",
"Import dropped files without review": "Importar los archivos soltados sin revisión",
"An import is running": "Hay una importación en curso",
"The photos are imported once it ends": "Las fotos se importarán después",
"An import is running. Drop photos when it has finished": "Hay una importación en curso. Suelta las fotos cuando termine",
"Finish Lightroom catalog import before adding photos": "Termina la importación del catálogo de Lightroom antes de añadir fotos",
"Thumbnail size": "Tamaño de las miniaturas",
+1
View File
@@ -2323,6 +2323,7 @@
"Mask Pins": "Maskin nastat",
"Photo Counts": "Kuvamäärät",
"An import is running": "Tuonti on käynnissä",
"The photos are imported once it ends": "Kuvat tuodaan sen jälkeen",
"An import is running. Drop photos when it has finished": "Tuonti on käynnissä. Pudota kuvat, kun se on valmis",
"Close the open dialog to drop files here": "Sulje avoin valintaikkuna pudottaaksesi tiedostot tähän",
"Close the open dialog, then drop the files again": "Sulje avoin valintaikkuna ja pudota tiedostot sitten uudelleen",
+1
View File
@@ -2355,6 +2355,7 @@
"Dropped Files": "Fichiers déposés",
"Import dropped files without review": "Importer les fichiers déposés sans vérification",
"An import is running": "Une importation est en cours",
"The photos are imported once it ends": "Les photos seront importées ensuite",
"An import is running. Drop photos when it has finished": "Une importation est en cours. Déposez les photos quand elle sera terminée",
"Finish Lightroom catalog import before adding photos": "Terminez l'importation du catalogue Lightroom avant d'ajouter des photos",
"The library is empty: the catalog's photos, collections and edits become its first.": "La bibliothèque est vide : les photos, collections et retouches du catalogue seront les premières.",
+1
View File
@@ -2335,6 +2335,7 @@
"Mask Pins": "Pin masker",
"Photo Counts": "Jumlah foto",
"An import is running": "Impor sedang berjalan",
"The photos are imported once it ends": "Foto diimpor setelah selesai",
"An import is running. Drop photos when it has finished": "Impor sedang berjalan. Jatuhkan foto setelah selesai",
"Close the open dialog to drop files here": "Tutup dialog yang terbuka untuk menjatuhkan file di sini",
"Close the open dialog, then drop the files again": "Tutup dialog yang terbuka, lalu jatuhkan file lagi",
+1
View File
@@ -2363,6 +2363,7 @@
"Dropped Files": "File rilasciati",
"Import dropped files without review": "Importa i file rilasciati senza revisione",
"An import is running": "Un'importazione è in corso",
"The photos are imported once it ends": "Le foto verranno importate dopo",
"An import is running. Drop photos when it has finished": "Un'importazione è in corso. Trascina le foto quando è finita",
"Finish Lightroom catalog import before adding photos": "Termina l'importazione del catalogo Lightroom prima di aggiungere foto",
"Thumbnail size": "Dimensione delle miniature",
+1
View File
@@ -2358,6 +2358,7 @@
"Dropped Files": "ドロップしたファイル",
"Import dropped files without review": "ドロップしたファイルを確認せずに読み込む",
"An import is running": "読み込み中です",
"The photos are imported once it ends": "終わりしだい写真を読み込みます",
"An import is running. Drop photos when it has finished": "読み込み中です。完了してから写真をドロップしてください",
"Finish Lightroom catalog import before adding photos": "写真を追加する前に Lightroom カタログの読み込みを完了してください",
"Thumbnail size": "サムネイルのサイズ",
+1
View File
@@ -2736,6 +2736,7 @@
"Dropped Files": "ჩაგდებული ფაილები",
"Import dropped files without review": "ჩაგდებული ფაილების იმპორტი გადახედვის გარეშე",
"An import is running": "იმპორტი მიმდინარეობს",
"The photos are imported once it ends": "ფოტოები ამის შემდეგ შემოიტანება",
"An import is running. Drop photos when it has finished": "იმპორტი მიმდინარეობს. ფოტოები ჩააგდეთ, როცა დასრულდება",
"Finish Lightroom catalog import before adding photos": "ფოტოების დამატებამდე დაასრულეთ Lightroom-ის კატალოგის იმპორტი",
"Thumbnail size": "მინიატურების ზომა",
+1
View File
@@ -2285,6 +2285,7 @@
"Dropped Files": "Upuszczone pliki",
"Import dropped files without review": "Importuj upuszczone pliki bez przeglądu",
"An import is running": "Import jest w toku",
"The photos are imported once it ends": "Zdjęcia zostaną zaimportowane potem",
"An import is running. Drop photos when it has finished": "Import jest w toku. Upuść zdjęcia, gdy się zakończy",
"Finish Lightroom catalog import before adding photos": "Zakończ import katalogu Lightrooma, zanim dodasz zdjęcia",
"Thumbnail size": "Rozmiar miniatur",
+1
View File
@@ -2204,6 +2204,7 @@
"Dropped Files": "Arquivos soltos",
"Import dropped files without review": "Importar arquivos soltos sem revisão",
"An import is running": "Uma importação está em andamento",
"The photos are imported once it ends": "As fotos serão importadas depois",
"An import is running. Drop photos when it has finished": "Uma importação está em andamento. Solte as fotos quando ela terminar",
"Finish Lightroom catalog import before adding photos": "Conclua a importação do catálogo do Lightroom antes de adicionar fotos",
"Thumbnail size": "Tamanho das miniaturas",
+1
View File
@@ -2567,6 +2567,7 @@
"Dropped Files": "Перетащенные файлы",
"Import dropped files without review": "Импортировать перетащенные файлы без проверки",
"An import is running": "Импорт уже выполняется",
"The photos are imported once it ends": "Фото будут импортированы после этого",
"An import is running. Drop photos when it has finished": "Импорт уже выполняется. Перетащите фото, когда он закончится",
"Finish Lightroom catalog import before adding photos": "Завершите импорт каталога Lightroom, прежде чем добавлять фото",
"★ Favorites": "★ Избранное",
+1
View File
@@ -2425,6 +2425,7 @@
"Mask Pins": "Masknålar",
"Photo Counts": "Antal foton",
"An import is running": "En import pågår",
"The photos are imported once it ends": "Bilderna importeras efteråt",
"An import is running. Drop photos when it has finished": "En import pågår. Släpp fotona när den är klar",
"Close the open dialog to drop files here": "Stäng den öppna dialogrutan för att släppa filer här",
"Close the open dialog, then drop the files again": "Stäng den öppna dialogrutan och släpp sedan filerna igen",
+1
View File
@@ -2356,6 +2356,7 @@
"Dropped Files": "Перетягнуті файли",
"Import dropped files without review": "Імпортувати перетягнуті файли без перегляду",
"An import is running": "Імпорт уже виконується",
"The photos are imported once it ends": "Фото буде імпортовано після цього",
"An import is running. Drop photos when it has finished": "Імпорт уже виконується. Перетягніть фото, коли він завершиться",
"Finish Lightroom catalog import before adding photos": "Завершіть імпорт каталогу Lightroom, перш ніж додавати фото",
"Thumbnail size": "Розмір мініатюр",
+1
View File
@@ -2231,6 +2231,7 @@
"Mask Pins": "Ghim mặt nạ",
"Photo Counts": "Số lượng ảnh",
"An import is running": "Đang có một lần nhập chạy",
"The photos are imported once it ends": "Ảnh sẽ được nhập khi việc đó kết thúc",
"An import is running. Drop photos when it has finished": "Đang có một lần nhập chạy. Hãy thả ảnh khi nó hoàn tất",
"Close the open dialog to drop files here": "Hãy đóng hộp thoại đang mở để thả tệp vào đây",
"Close the open dialog, then drop the files again": "Hãy đóng hộp thoại đang mở, rồi thả lại các tệp",
+1
View File
@@ -2202,6 +2202,7 @@
"Dropped Files": "拖入的文件",
"Import dropped files without review": "不经检查直接导入拖入的文件",
"An import is running": "正在导入",
"The photos are imported once it ends": "结束后即导入这些照片",
"An import is running. Drop photos when it has finished": "正在导入。导入完成后再拖放照片",
"Finish Lightroom catalog import before adding photos": "添加照片前,请先完成 Lightroom 目录的导入",
"Thumbnail size": "缩略图大小",
+1
View File
@@ -2159,6 +2159,7 @@
"Dropped Files": "拖放的檔案",
"Import dropped files without review": "不經檢查直接匯入拖放的檔案",
"An import is running": "正在匯入",
"The photos are imported once it ends": "結束後即匯入這些照片",
"An import is running. Drop photos when it has finished": "正在匯入。匯入完成後再拖放照片",
"Finish Lightroom catalog import before adding photos": "新增照片前,請先完成 Lightroom 目錄的匯入",
"Thumbnail size": "縮圖大小",
+1
View File
@@ -102,6 +102,7 @@ impl LightcraftApp {
merge: merge::MergeState::default(),
import: None,
scan: None,
waiting_import: Default::default(),
sync: None,
sync_owns_changes: false,
sync_run: None,
+1
View File
@@ -66,6 +66,7 @@ impl LightcraftApp {
if import_tick_allowed(ctx) {
import::tick(self, ctx);
}
panels::library_problem::import_waiting(self, ctx);
lightroom_import::tick(self, ctx);
recheck::tick(self);
tasks::poll(self, ctx);
@@ -51,6 +51,11 @@ pub struct ImportTask {
}
impl ImportTask {
/// Whether it has its row in the activity list yet (it gets one on its first tick).
pub fn listed(&self) -> bool {
self.guard.is_some()
}
/// An import of `paths` (files or folders) with `library.import` params (without `paths`).
pub fn new(paths: Vec<String>, params: Value, undo0: usize, browse: bool) -> Self {
ImportTask { total: paths.len(), queue: paths, params, undo0, browse, ..Default::default() }
@@ -17,6 +17,10 @@ pub(crate) fn can_switch_library(app: &LightcraftApp) -> Result<(), String> {
if app.pending_picks.iter().any(|p| p.command == "folder.move") {
return Err("wait for Move Folder To… to finish before switching libraries".into());
}
// they were meant for the library open now (issue #2779)
if !app.waiting_import.files.is_empty() {
return Err("wait for the photos waiting to be imported before switching libraries".into());
}
Ok(())
}
+2
View File
@@ -298,6 +298,8 @@ pub struct LightcraftApp {
pub import: Option<import::ImportTask>,
/// A folder scan in progress (feeds the import review).
pub scan: Option<import::ScanTask>,
/// Files that waited for a library and wait for the running import or Synchronize Folder too.
pub waiting_import: panels::library_problem::WaitingImport,
/// A Synchronize Folder scan in progress (feeds its dialog).
pub sync: Option<sync::SyncTask>,
/// `session.folder_changes` came from the Synchronize Folder dialog (and goes with it).
+34 -17
View File
@@ -331,6 +331,16 @@ pub struct Chooser {
pub over: Option<u64>,
}
/// Files that waited for a library (the command line, drops on the launch list) and found another
/// import or a Synchronize Folder running when one opened: kept, and imported once it ends instead
/// of being lost (issue #2779).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WaitingImport {
pub files: Vec<String>,
/// The toast already said why they wait.
told: bool,
}
/// A library that failed to open (set by the host at launch).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LibraryProblem {
@@ -823,12 +833,7 @@ fn open(app: &mut LightcraftApp, path: Option<String>) {
Ok(serde_json::Value::Null) => {} // folder dialog cancelled
Ok(_) => {
let files = app.library_problem.take().map(|p| p.pending_import).unwrap_or_default();
// in the background, like files dropped on the window (issue #374)
if !files.is_empty()
&& let Err(e) = crate::import::start_paths(app, files)
{
log::warn!("import: {e}");
}
app.waiting_import.files.extend(files);
}
Err(e) => {
// a failed switch reopens the library that was open behind the launch list
@@ -860,6 +865,27 @@ fn open(app: &mut LightcraftApp, path: Option<String>) {
}
}
/// Import the files that waited for a library, in the background like files dropped on the window
/// (issue #374). While another import or a Synchronize Folder runs they keep waiting, and a toast
/// says why once (issue #2779). Called from the background logic, so they go in as soon as that
/// ends even while the window is hidden.
pub(crate) fn import_waiting(app: &mut LightcraftApp, ctx: &egui::Context) {
if app.waiting_import.files.is_empty() {
return;
}
let files = std::mem::take(&mut app.waiting_import.files);
match crate::import::start_paths(app, files.clone()) {
Ok(_) => app.waiting_import = WaitingImport::default(),
Err(e) => {
if !app.waiting_import.told {
app.toast(ctx, format!("{e}. {}", crate::i18n::tr("The photos are imported once it ends")));
}
app.waiting_import = WaitingImport { files, told: true };
ctx.request_repaint_after(std::time::Duration::from_millis(250));
}
}
}
/// Clear the problem once a library is open (e.g. through Settings → Open Library…) — another
/// one than the launch list was opened over; offer the list when Alt/Option is held as the window
/// appears; and the list's keys.
@@ -871,11 +897,7 @@ pub fn logic(app: &mut LightcraftApp, ctx: &egui::Context) {
// files from the command line go into the library opened another way (New Library…,
// File ▸ Open Library…), as through the window's own buttons
let files = app.library_problem.take().map(|p| p.pending_import).unwrap_or_default();
if !files.is_empty()
&& let Err(e) = crate::import::start_paths(app, files)
{
log::warn!("import: {e}");
}
app.waiting_import.files.extend(files);
}
let batch_in_flight = app.synthetic_in_flight;
alt_at_launch(app, ctx);
@@ -1155,12 +1177,7 @@ fn act(app: &mut LightcraftApp, choice: Choice) {
p.dismissed = true;
}
let files = app.library_problem.as_mut().map(|p| std::mem::take(&mut p.pending_import)).unwrap_or_default();
// in the background, like files dropped on the window (issue #374)
if !files.is_empty()
&& let Err(e) = crate::import::start_paths(app, files)
{
log::warn!("import: {e}");
}
app.waiting_import.files.extend(files);
}
Choice::Quit => app.ui.quit = true,
}
+15 -3
View File
@@ -65,9 +65,15 @@ pub fn may_close(app: &mut LightcraftApp) -> bool {
}
// before the library is touched: a Cancel here keeps working with it
let running = app.session.activity.running_cancellable();
if !running.is_empty() {
let names = running.iter().map(|t| crate::i18n::tr(&t.label)).collect::<Vec<_>>().join(", ");
app.quit_prompt = Some(QuitPrompt::Tasks(tasks_message(running.len(), &names)));
let mut names = running.iter().map(|t| crate::i18n::tr(&t.label)).collect::<Vec<_>>();
// an import handed its files this tick (files that waited for a library, #2779) is listed only
// from its first tick on, and files still waiting will be imported too
let unlisted = app.import.as_ref().is_some_and(|t| !t.listed()) || !app.waiting_import.files.is_empty();
if unlisted && !running.iter().any(|t| t.kind == "import") {
names.push(crate::i18n::tr("Importing"));
}
if !names.is_empty() {
app.quit_prompt = Some(QuitPrompt::Tasks(tasks_message(names.len(), &names.join(", "))));
return false;
}
if app.session.unsaved().is_none() {
@@ -536,6 +542,12 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
app.quit_prompt = None;
if buttons.get(i).is_some_and(|(_, _, choice)| *choice == Choice::QuitAnyway) {
app.session.activity.cancel_all();
// files waiting for an import, and one handed its files but not started, are given up
// too, or `may_close` would ask about them again
app.waiting_import = Default::default();
if app.import.as_ref().is_some_and(|t| !t.listed()) {
app.import = None;
}
// unsaved changes still get their own question
if may_close(app) {
app.ui.quit = true;
+29
View File
@@ -172,6 +172,35 @@ fn quit_with_a_running_task_asks_and_quit_anyway_cancels() {
assert!(g.is_cancelled() && h.quit_requested());
}
/// Files waiting for a running import or Synchronize Folder (issue #2779) make quitting ask, and
/// Quit Anyway gives them up instead of asking about them again and again.
#[test]
fn quit_anyway_gives_up_files_waiting_to_be_imported() {
let dir = std::env::temp_dir().join(format!("lightcraft-quit-waiting-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for i in 0..1000u32 {
let img = lightcraft_raster::Rgba8 { width: 8, height: 8, data: vec![[(i % 251) as u8, (i / 251) as u8, 9, 255]; 64] };
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
std::fs::write(dir.join(format!("{i}.png")), png).unwrap();
}
let mut h = demo();
// the file waits behind an import that is still running
crate::import::start_paths(&mut h.app, vec![dir.to_string_lossy().to_string()]).unwrap();
h.app.waiting_import.files = vec![dir.join("0.png").to_string_lossy().to_string()];
wait_visible(&mut h);
assert!(h.app.import.is_some() && !h.app.waiting_import.files.is_empty(), "still waiting");
assert!(!crate::panels::notices::may_close(&mut h.app));
assert!(matches!(h.app.quit_prompt, Some(crate::QuitPrompt::Tasks(_))), "{:?}", h.app.quit_prompt);
h.step();
assert!(h.app.import.is_some() && !h.app.waiting_import.files.is_empty(), "still waiting at the click");
click(&mut h, "button:quitAnyway");
assert!(h.app.quit_prompt.is_none(), "not asked again: {:?}", h.app.quit_prompt);
assert!(h.quit_requested());
assert!(h.app.waiting_import.files.is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn quit_cancel_keeps_the_task_running() {
let mut h = demo();
@@ -804,3 +804,78 @@ fn typing_reaches_the_launch_lists_own_new_library_prompt() {
assert_eq!(command, "app.newLibrary");
assert_eq!(value, "Fresh", "typing reaches the launch list's own text prompt");
}
/// Issue #2779: files waiting for a library (the command line, drops on the launch list) that
/// find another import running when a library opens are kept, with a toast saying why, and
/// imported once that import ends instead of being dropped.
#[test]
fn waiting_files_wait_for_a_running_import_instead_of_being_dropped() {
let dir = std::env::temp_dir().join(format!("lightcraft-waiting-import-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let png = |v: u8| {
let img = lightcraft_raster::Rgba8 { width: 8, height: 8, data: vec![[v, 3, 9, 255]; 64] };
lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap()
};
// enough photos that the first import is still running two frames later
let busy = dir.join("busy");
std::fs::create_dir_all(&busy).unwrap();
for i in 0..100u8 {
std::fs::write(busy.join(format!("{i}.png")), png(i)).unwrap();
}
let waiting = dir.join("waiting.png");
std::fs::write(&waiting, png(200)).unwrap();
let app = LightcraftApp::new(lightcraft_engine::Session::new().with_fs(), Services { png: None, ..Default::default() });
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
let lib = dir.join("lib");
h.app.library_problem = Some(LibraryProblem {
pending_import: vec![waiting.to_string_lossy().to_string()],
..LibraryProblem::new(lib.to_string_lossy(), "a test error")
});
h.step();
// a library opens another way (File ▸ Open Library…) while an import is running
h.app.session.open_library(&lib, false).unwrap();
crate::import::start_paths(&mut h.app, vec![busy.to_string_lossy().to_string()]).unwrap();
// the UI pass takes the files; the next background tick tries to import them
h.step();
h.step();
assert!(h.app.library_problem.is_none(), "the problem is over");
assert_eq!(h.app.waiting_import.files, vec![waiting.to_string_lossy().to_string()], "the waiting file is kept");
let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
assert!(toast.contains("An import is running") && toast.contains("once it ends"), "{toast}");
// the files belong to this library: no switch while they wait, also when what they wait for
// is a Synchronize Folder, which doesn't hold switching back by itself (the import set aside)
let running = h.app.import.take();
let r = h.app.run("app.openLibrary", json!({"path": dir.join("other").to_string_lossy()}));
h.app.import = running;
assert!(r.as_ref().is_err_and(|e| e.contains("waiting")), "{r:?}");
assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.clone()), Some(lib.clone()));
// quitting while the file waits asks first
assert!(!crate::panels::notices::may_close(&mut h.app));
h.app.quit_prompt = None;
// the window is hidden from here on: only the background logic runs. The tick that hands the
// file to a new import (once the first one ends) leaves that import unlisted until its own
// first tick, and quitting right then still asks
let t0 = std::time::Instant::now();
while !h.app.waiting_import.files.is_empty() && t0.elapsed() < T {
let ctx = h.view.ctx.clone();
h.app.logic(&ctx);
std::thread::sleep(Duration::from_millis(2));
}
assert!(h.app.waiting_import.files.is_empty(), "the file was handed to a new import");
assert!(h.app.import.as_ref().is_some_and(|t| !t.listed()), "not listed before its first tick");
assert!(!crate::panels::notices::may_close(&mut h.app), "quitting asks about the import just handed over");
h.app.quit_prompt = None;
while !(h.app.import.is_none() && h.app.session.catalog.len() == 101) && t0.elapsed() < T {
let ctx = h.view.ctx.clone();
h.app.logic(&ctx);
std::thread::sleep(Duration::from_millis(2));
}
assert_eq!(h.app.session.catalog.len(), 101, "both imports went in while hidden");
assert!(h.app.waiting_import.files.is_empty());
let _ = h.app.session.close_library();
let _ = std::fs::remove_dir_all(&dir);
}
+5
View File
@@ -9,6 +9,11 @@
ends where that block begins, so these photos import, show and build previews. A JPEG that is really cut short is
still refused.
### Photos waiting for a library wait for a running import too
- Photos given on the command line or dropped on the launch list are imported once a library opens. If another import
or a Synchronize Folder is still running then, they now wait for it to end instead of being dropped, and a message
says so (issue #2779).
## 2026-10-29
### Move Folder To… no longer freezes the window