diff --git a/crates/ui-egui/locales/cs.json b/crates/ui-egui/locales/cs.json index 199ea3351..60ac99f07 100644 --- a/crates/ui-egui/locales/cs.json +++ b/crates/ui-egui/locales/cs.json @@ -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" diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index eb7a80b83..2a7ffa28d 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -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" diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index cd7608c7b..4df92685d 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -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", diff --git a/crates/ui-egui/locales/fi.json b/crates/ui-egui/locales/fi.json index a94ba37b7..900ffbaa0 100644 --- a/crates/ui-egui/locales/fi.json +++ b/crates/ui-egui/locales/fi.json @@ -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", diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index 89eb8917f..66330423c 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -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.", diff --git a/crates/ui-egui/locales/id.json b/crates/ui-egui/locales/id.json index b5f2ea70a..d0fe8e8af 100644 --- a/crates/ui-egui/locales/id.json +++ b/crates/ui-egui/locales/id.json @@ -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", diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json index c03a07b2a..e505ed2a1 100644 --- a/crates/ui-egui/locales/it.json +++ b/crates/ui-egui/locales/it.json @@ -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", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 3df86e1b5..545164e57 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -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": "サムネイルのサイズ", diff --git a/crates/ui-egui/locales/ka.json b/crates/ui-egui/locales/ka.json index fc343624e..6ec542023 100644 --- a/crates/ui-egui/locales/ka.json +++ b/crates/ui-egui/locales/ka.json @@ -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": "მინიატურების ზომა", diff --git a/crates/ui-egui/locales/pl.json b/crates/ui-egui/locales/pl.json index 1138009e2..2aa10e8ba 100644 --- a/crates/ui-egui/locales/pl.json +++ b/crates/ui-egui/locales/pl.json @@ -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", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index e07cfaf73..da04c9741 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -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", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 5deeee5dc..02b714add 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -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": "★ Избранное", diff --git a/crates/ui-egui/locales/sv.json b/crates/ui-egui/locales/sv.json index a6bea8eed..f4f57f34e 100644 --- a/crates/ui-egui/locales/sv.json +++ b/crates/ui-egui/locales/sv.json @@ -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", diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index ed69eea57..2a7d20b46 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -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": "Розмір мініатюр", diff --git a/crates/ui-egui/locales/vi.json b/crates/ui-egui/locales/vi.json index 54d01dd15..c82d81758 100644 --- a/crates/ui-egui/locales/vi.json +++ b/crates/ui-egui/locales/vi.json @@ -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", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 4c35b9c02..a00f935a7 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -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": "缩略图大小", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 89078b3a2..76f79eabf 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -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": "縮圖大小", diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index fa10210f0..d158c0429 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -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, diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 6e92091c7..d4a153e2a 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -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); diff --git a/crates/ui-egui/src/dialogs/import/task.rs b/crates/ui-egui/src/dialogs/import/task.rs index e1f506d5b..5c2798340 100644 --- a/crates/ui-egui/src/dialogs/import/task.rs +++ b/crates/ui-egui/src/dialogs/import/task.rs @@ -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, params: Value, undo0: usize, browse: bool) -> Self { ImportTask { total: paths.len(), queue: paths, params, undo0, browse, ..Default::default() } diff --git a/crates/ui-egui/src/dialogs/settings/libraries.rs b/crates/ui-egui/src/dialogs/settings/libraries.rs index 6f63530c1..30724efa4 100644 --- a/crates/ui-egui/src/dialogs/settings/libraries.rs +++ b/crates/ui-egui/src/dialogs/settings/libraries.rs @@ -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(()) } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8bf57b341..a4787594d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -298,6 +298,8 @@ pub struct LightcraftApp { pub import: Option, /// A folder scan in progress (feeds the import review). pub scan: Option, + /// 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, /// `session.folder_changes` came from the Synchronize Folder dialog (and goes with it). diff --git a/crates/ui-egui/src/panels/library_problem.rs b/crates/ui-egui/src/panels/library_problem.rs index 6ba99b0c6..c8b8aed60 100644 --- a/crates/ui-egui/src/panels/library_problem.rs +++ b/crates/ui-egui/src/panels/library_problem.rs @@ -331,6 +331,16 @@ pub struct Chooser { pub over: Option, } +/// 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, + /// 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) { 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) { } } +/// 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, } diff --git a/crates/ui-egui/src/panels/notices.rs b/crates/ui-egui/src/panels/notices.rs index 96e356bb4..85061f252 100644 --- a/crates/ui-egui/src/panels/notices.rs +++ b/crates/ui-egui/src/panels/notices.rs @@ -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::>().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::>(); + // 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; diff --git a/crates/ui-egui/src/tests/activity.rs b/crates/ui-egui/src/tests/activity.rs index 58873a6e2..284083ee5 100644 --- a/crates/ui-egui/src/tests/activity.rs +++ b/crates/ui-egui/src/tests/activity.rs @@ -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(); diff --git a/crates/ui-egui/src/tests/library_problem.rs b/crates/ui-egui/src/tests/library_problem.rs index 1a2e5a2d9..cdc3b7c8f 100644 --- a/crates/ui-egui/src/tests/library_problem.rs +++ b/crates/ui-egui/src/tests/library_problem.rs @@ -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); +} diff --git a/docs/whats-new.md b/docs/whats-new.md index 9abcd644d..fc9b7f834 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -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