From f84347a88e198f25050f80cd09e9b8dc18f48420 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Sat, 17 Oct 2026 19:40:07 -0400 Subject: [PATCH] LR-IMP-DRAGDROP: review follow-ups A finished scan waits for an open dialog before opening its review; drops are refused while an import runs (and, on the web, while a dialog is open); orphaned doc comment restored to builtin_label. Co-Authored-By: Claude --- apps/lightcraft-web/src/web.rs | 6 +++ crates/ui-egui/locales/cs.json | 4 +- crates/ui-egui/locales/de.json | 4 +- crates/ui-egui/locales/es.json | 4 +- crates/ui-egui/locales/fr.json | 4 +- crates/ui-egui/locales/it.json | 4 +- crates/ui-egui/locales/ja.json | 4 +- crates/ui-egui/locales/ka.json | 4 +- crates/ui-egui/locales/pl.json | 4 +- crates/ui-egui/locales/pt-br.json | 4 +- crates/ui-egui/locales/ru.json | 4 +- crates/ui-egui/locales/uk.json | 4 +- crates/ui-egui/locales/zh-hans.json | 4 +- crates/ui-egui/locales/zh-hant.json | 4 +- crates/ui-egui/src/drop_overlay.rs | 4 +- crates/ui-egui/src/i18n.rs | 4 +- crates/ui-egui/src/import.rs | 36 +++++++++++++++-- crates/ui-egui/src/lib.rs | 7 ++++ crates/ui-egui/src/tests_drop_import.rs | 53 +++++++++++++++++++++++++ docs/parity-checklist.md | 4 +- docs/whats-new.md | 5 ++- 21 files changed, 149 insertions(+), 22 deletions(-) diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 9acb98c9b..3a7914be2 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -508,6 +508,12 @@ impl WebApp { fn import_dropped(&mut self, ctx: &egui::Context) { let dropped = ctx.input(|i| i.raw.dropped_files.clone()); + // a dialog is on screen (the card said so while the files hovered): nothing is read or imported + // (files read from an earlier drop still finish below) + if !dropped.is_empty() && self.app.ui.dialog.is_some() { + self.app.toast(ctx, lightcraft_ui_egui::i18n::tr("Close the open dialog, then drop the files again")); + return; + } for f in dropped { let (originals, backend, ctx) = (self.originals.clone(), self.backend.clone(), ctx.clone()); wasm_bindgen_futures::spawn_local(async move { diff --git a/crates/ui-egui/locales/cs.json b/crates/ui-egui/locales/cs.json index d33918dc0..661250c70 100644 --- a/crates/ui-egui/locales/cs.json +++ b/crates/ui-egui/locales/cs.json @@ -2400,5 +2400,7 @@ "Drop to open the face model": "Pusťte pro otevření modelu obličejů", "Drop to import": "Pusťte pro import", "Dropped Files": "Přetažené soubory", - "Import dropped files without review": "Importovat přetažené soubory bez kontroly" + "Import dropped files without review": "Importovat přetažené soubory bez kontroly", + "An import is running": "Import už běží", + "An import is running. Drop photos when it has finished": "Import už běží. Fotky přetáhněte, až skončí" } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index f11a23677..2ef804a2a 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -3428,5 +3428,7 @@ "Drop to open the face model": "Ablegen, um das Gesichtsmodell zu öffnen", "Drop to import": "Ablegen zum Importieren", "Dropped Files": "Abgelegte Dateien", - "Import dropped files without review": "Abgelegte Dateien ohne Prüfung importieren" + "Import dropped files without review": "Abgelegte Dateien ohne Prüfung importieren", + "An import is running": "Ein Import läuft", + "An import is running. Drop photos when it has finished": "Ein Import läuft. Lege die Fotos ab, wenn er fertig ist" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 0e6adad56..abadd6474 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -2348,5 +2348,7 @@ "Drop to open the face model": "Suelta para abrir el modelo de caras", "Drop to import": "Suelta para importar", "Dropped Files": "Archivos soltados", - "Import dropped files without review": "Importar los archivos soltados sin revisión" + "Import dropped files without review": "Importar los archivos soltados sin revisión", + "An import is running": "Hay una importación en curso", + "An import is running. Drop photos when it has finished": "Hay una importación en curso. Suelta las fotos cuando termine" } diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index 197404e7e..928fdf58e 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -2346,5 +2346,7 @@ "Drop to open the face model": "Déposez pour ouvrir le modèle de visages", "Drop to import": "Déposez pour importer", "Dropped Files": "Fichiers déposés", - "Import dropped files without review": "Importer les fichiers déposés sans vérification" + "Import dropped files without review": "Importer les fichiers déposés sans vérification", + "An import is running": "Une importation est en cours", + "An import is running. Drop photos when it has finished": "Une importation est en cours. Déposez les photos quand elle sera terminée" } diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json index cbe7a07ef..6d33c986c 100644 --- a/crates/ui-egui/locales/it.json +++ b/crates/ui-egui/locales/it.json @@ -2352,5 +2352,7 @@ "Drop to open the face model": "Rilascia per aprire il modello dei volti", "Drop to import": "Rilascia per importare", "Dropped Files": "File rilasciati", - "Import dropped files without review": "Importa i file rilasciati senza revisione" + "Import dropped files without review": "Importa i file rilasciati senza revisione", + "An import is running": "Un'importazione è in corso", + "An import is running. Drop photos when it has finished": "Un'importazione è in corso. Trascina le foto quando è finita" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index ada8fe0a1..41a207f75 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -2348,5 +2348,7 @@ "Drop to open the face model": "ドロップして顔モデルを開く", "Drop to import": "ドロップして読み込み", "Dropped Files": "ドロップしたファイル", - "Import dropped files without review": "ドロップしたファイルを確認せずに読み込む" + "Import dropped files without review": "ドロップしたファイルを確認せずに読み込む", + "An import is running": "読み込み中です", + "An import is running. Drop photos when it has finished": "読み込み中です。完了してから写真をドロップしてください" } diff --git a/crates/ui-egui/locales/ka.json b/crates/ui-egui/locales/ka.json index 3fe3d687c..2326c4787 100644 --- a/crates/ui-egui/locales/ka.json +++ b/crates/ui-egui/locales/ka.json @@ -2725,5 +2725,7 @@ "Drop to open the face model": "ჩააგდეთ სახეების მოდელის გასახსნელად", "Drop to import": "ჩააგდეთ იმპორტისთვის", "Dropped Files": "ჩაგდებული ფაილები", - "Import dropped files without review": "ჩაგდებული ფაილების იმპორტი გადახედვის გარეშე" + "Import dropped files without review": "ჩაგდებული ფაილების იმპორტი გადახედვის გარეშე", + "An import is running": "იმპორტი მიმდინარეობს", + "An import is running. Drop photos when it has finished": "იმპორტი მიმდინარეობს. ფოტოები ჩააგდეთ, როცა დასრულდება" } diff --git a/crates/ui-egui/locales/pl.json b/crates/ui-egui/locales/pl.json index 3482c3cbf..e9ca8902d 100644 --- a/crates/ui-egui/locales/pl.json +++ b/crates/ui-egui/locales/pl.json @@ -2275,5 +2275,7 @@ "Drop to open the face model": "Upuść, aby otworzyć model twarzy", "Drop to import": "Upuść, aby zaimportować", "Dropped Files": "Upuszczone pliki", - "Import dropped files without review": "Importuj upuszczone pliki bez przeglądu" + "Import dropped files without review": "Importuj upuszczone pliki bez przeglądu", + "An import is running": "Import jest w toku", + "An import is running. Drop photos when it has finished": "Import jest w toku. Upuść zdjęcia, gdy się zakończy" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 3ede4056c..bfd01c8dc 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -2194,5 +2194,7 @@ "Drop to open the face model": "Solte para abrir o modelo de rostos", "Drop to import": "Solte para importar", "Dropped Files": "Arquivos soltos", - "Import dropped files without review": "Importar arquivos soltos sem revisão" + "Import dropped files without review": "Importar arquivos soltos sem revisão", + "An import is running": "Uma importação está em andamento", + "An import is running. Drop photos when it has finished": "Uma importação está em andamento. Solte as fotos quando ela terminar" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 880577763..4abbe5cfc 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -2341,5 +2341,7 @@ "Drop to open the face model": "Отпустите, чтобы открыть модель лиц", "Drop to import": "Отпустите для импорта", "Dropped Files": "Перетащенные файлы", - "Import dropped files without review": "Импортировать перетащенные файлы без проверки" + "Import dropped files without review": "Импортировать перетащенные файлы без проверки", + "An import is running": "Импорт уже выполняется", + "An import is running. Drop photos when it has finished": "Импорт уже выполняется. Перетащите фото, когда он закончится" } diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index fc6d1f8ca..26908a7c6 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -2346,5 +2346,7 @@ "Drop to open the face model": "Відпустіть, щоб відкрити модель облич", "Drop to import": "Відпустіть для імпорту", "Dropped Files": "Перетягнуті файли", - "Import dropped files without review": "Імпортувати перетягнуті файли без перегляду" + "Import dropped files without review": "Імпортувати перетягнуті файли без перегляду", + "An import is running": "Імпорт уже виконується", + "An import is running. Drop photos when it has finished": "Імпорт уже виконується. Перетягніть фото, коли він завершиться" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 4e433b0fe..c48e3c736 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -2192,5 +2192,7 @@ "Drop to open the face model": "松开鼠标以打开人脸模型", "Drop to import": "松开鼠标以导入", "Dropped Files": "拖入的文件", - "Import dropped files without review": "不经检查直接导入拖入的文件" + "Import dropped files without review": "不经检查直接导入拖入的文件", + "An import is running": "正在导入", + "An import is running. Drop photos when it has finished": "正在导入。导入完成后再拖放照片" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index ff2bd9d82..5af20f3ec 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -2149,5 +2149,7 @@ "Drop to open the face model": "放開滑鼠以開啟人臉模型", "Drop to import": "放開滑鼠以匯入", "Dropped Files": "拖放的檔案", - "Import dropped files without review": "不經檢查直接匯入拖放的檔案" + "Import dropped files without review": "不經檢查直接匯入拖放的檔案", + "An import is running": "正在匯入", + "An import is running. Drop photos when it has finished": "正在匯入。匯入完成後再拖放照片" } diff --git a/crates/ui-egui/src/drop_overlay.rs b/crates/ui-egui/src/drop_overlay.rs index 0e4eaee8d..3754fb76a 100644 --- a/crates/ui-egui/src/drop_overlay.rs +++ b/crates/ui-egui/src/drop_overlay.rs @@ -40,7 +40,9 @@ fn lines(app: &LightcraftApp, paths: Vec, unknown: bool) -> Vec 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() { + if !drop.photos.is_empty() && crate::import::running(app) && !cfg!(target_arch = "wasm32") { + out.push(crate::i18n::tr("An import is running. Drop photos when it has finished").to_string()); + } else if !drop.photos.is_empty() { let n = drop.photos.len(); // the web build has no review to open: its drops import at once if app.ui.settings.import_dropped_directly || cfg!(target_arch = "wasm32") { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 0e97f8a2c..e8bdf03e2 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -263,8 +263,6 @@ fn catalog_verbatim(language: Locale) -> BTreeMap { // `locales/*-formats.json`. include!(concat!(env!("OUT_DIR"), "/tr-formats.rs")); -/// A stock preset's or profile's name in the UI language; an imported or user-edited name stays -/// exactly as its owner wrote it. /// The web build's notice for a dropped file the browser could not read. pub fn dropped_file_unreadable(name: &str, error: &str) -> String { tr_format!("Could not read {name}: {e}", name = name, e = error) @@ -317,6 +315,8 @@ pub fn upper_first(text: &str) -> String { } } +/// A stock preset's or profile's name in the UI language; an imported or user-edited name stays +/// exactly as its owner wrote it. pub fn builtin_label(source: &str, builtin: bool) -> &str { if builtin { tr(source) } else { source } } diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index 323d8d902..cf4d667aa 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -556,6 +556,7 @@ pub fn browse(app: &mut LightcraftApp, path: &str, subfolders: Option) -> /// Collect a finished scan and open the review (called every frame). pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) { + show_pending_review(app, ctx); let Some(task) = app.scan.as_ref() else { return }; ctx.request_repaint_after(std::time::Duration::from_millis(100)); let out = match task.rx.try_recv() { @@ -626,7 +627,36 @@ pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) { // only an album the review offers (it may have been deleted while the scan ran) d.album = task.album.filter(|id| app.session.catalog.album(lightcraft_catalog::AlbumId(*id)).is_some_and(|a| !a.folder && !a.is_smart())); d.sources = task.sources; - app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(d) }); + app.review_pending = Some(crate::state::Dialog::Import { opts: Box::new(d) }); + show_pending_review(app, ctx); +} + +/// Open the review of a finished scan once no other dialog is open (a face model's licence, Settings… +/// opened while the scan ran stays), checked again shortly while it waits. +fn show_pending_review(app: &mut LightcraftApp, ctx: &egui::Context) { + if app.review_pending.is_none() { + return; + } + if app.ui.dialog.is_none() { + app.ui.dialog = app.review_pending.take(); + } else { + ctx.request_repaint_after(std::time::Duration::from_millis(100)); + } +} + +/// Tests: a review scan that never ends while the returned value lives, so a drop meets it running. +#[cfg(test)] +pub(crate) fn stall_scan(app: &mut LightcraftApp) -> Box { + let progress = std::sync::Arc::new(ScanProgress::default()); + let (tx, rx) = std::sync::mpsc::channel::(); + let guard = scan_guard(app, &progress, false); + app.scan = Some(ScanTask { progress, rx, copy: false, album: None, browse: false, sources: Vec::new(), guard }); + Box::new(tx) +} + +/// Whether an import (or the review's scan) is running, which a drop of photos must not replace. +pub fn running(app: &LightcraftApp) -> bool { + app.import.is_some() || app.scan.as_ref().is_some_and(|t| !t.browse) } impl ScanTask { @@ -691,8 +721,8 @@ pub fn start(app: &mut LightcraftApp, d: &ImportDialog) -> Result /// Start importing `paths` (files or folders) in the background, e.g. dropped on the window. pub fn start_paths(app: &mut LightcraftApp, paths: Vec) -> Result { busy_synchronizing(app)?; - if app.import.is_some() || app.scan.as_ref().is_some_and(|t| !t.browse) { - return Err("an import is running".into()); + if running(app) { + return Err(crate::i18n::tr("An import is running").to_string()); } let undo0 = app.session.undo.len(); let total = paths.len(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5c7681b33..994b989c8 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -458,6 +458,8 @@ pub struct LightcraftApp { pub lightroom_last: Option, /// A completed import result waiting for another modal dialog to close. pub(crate) lightroom_result: Option, + /// The import review of a finished scan, waiting for another dialog to close. + pub(crate) review_pending: Option, /// The library generation the window title and the recent list were last updated for. pub shown_library: Option, /// A background export in progress. @@ -557,6 +559,7 @@ impl LightcraftApp { lightroom: None, lightroom_last: None, lightroom_result: None, + review_pending: None, shown_library: None, export: None, tasks: Default::default(), @@ -1276,6 +1279,10 @@ impl LightcraftApp { self.toast(ctx, "Finish Lightroom catalog import before adding photos"); return; } + if import::running(self) { + self.toast(ctx, crate::i18n::tr("An import is running")); + return; + } 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(|_| ()) diff --git a/crates/ui-egui/src/tests_drop_import.rs b/crates/ui-egui/src/tests_drop_import.rs index a6d2b7ba2..fad64397f 100644 --- a/crates/ui-egui/src/tests_drop_import.rs +++ b/crates/ui-egui/src/tests_drop_import.rs @@ -146,3 +146,56 @@ fn a_drop_while_a_dialog_is_open_leaves_the_dialog_and_imports_nothing() { assert_eq!(photos(&h), 0); assert!(h.app.ui.toast.is_some(), "the person is told why"); } + +/// The scan of a dropped folder finishing while another dialog is open (the face model's licence, Settings…) +/// keeps that dialog; the review opens, with the scan's photos, once it is closed. +#[test] +fn a_finished_scan_waits_for_the_open_dialog_before_opening_the_review() { + let dir = Scratch::new("waits"); + let mut h = app(&dir); + crate::import::open(&mut h.app, vec![dir.path("trip")]).unwrap(); + h.app.ui.dialog = Some(Dialog::WhatsNew); + assert!(h.step_until(SETTLE, |h| h.app.scan.is_none()), "the scan ends"); + for _ in 0..10 { + h.step(); + } + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew)), "the dialog the person had open stays"); + h.app.ui.dialog = None; + assert!(h.step_until(T, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the review opens once the dialog is closed"); + let Some(Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") }; + assert_eq!(opts.candidates.len(), 2, "the scan's photos were kept"); +} + +/// A drop while an import runs would open a review whose OK replaces the running import: it is +/// refused instead, and the card says so. +#[test] +fn a_drop_while_an_import_runs_is_refused() { + let dir = Scratch::new("running"); + let mut h = app(&dir); + write_png(&dir.path("more/c.png"), 3); + let _scan = crate::import::stall_scan(&mut h.app); + drop_files(&mut h, &[dir.path("more/c.png")], true); + assert!(said(&h, "An import is running"), "{:?}", h.painted_text()); + assert!(!said(&h, "review"), "{:?}", h.painted_text()); + drop_files(&mut h, &[dir.path("more")], false); + for _ in 0..30 { + h.step(); + } + assert!(h.app.ui.dialog.is_none(), "no review opened"); + assert!(h.app.scan.is_some() && h.app.import.is_none(), "the running scan was left alone and nothing else started"); + assert!(h.app.ui.toast.is_some(), "the person is told why"); + assert_eq!(photos(&h), 0); +} + +/// An import that is running stays the one that runs: a direct drop doesn't start a second. +#[test] +fn a_direct_drop_while_an_import_runs_is_refused_too() { + let dir = Scratch::new("running-direct"); + let mut h = app(&dir); + h.app.ui.settings.import_dropped_directly = true; + write_png(&dir.path("more/c.png"), 3); + let _scan = crate::import::stall_scan(&mut h.app); + drop_files(&mut h, &[dir.path("more")], false); + assert!(h.app.import.is_none(), "no second import"); + assert!(h.app.ui.toast.is_some()); +} diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index e961d8cf5..a86455b67 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -70,7 +70,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | Id | Feature | Tier | Status | Evidence | Missing / notes | |---|---|---|---|---|---| | LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new): the destination picker follows the sidebar folder hierarchy and order, searches full paths, and distinguishes duplicate names by their folder path; folders navigate and smart albums are excluded, with None, New album and album-context preselection retained (#2362; `crates/ui-egui/src/album_picker.rs`, `crates/ui-egui/src/tests_album_picker.rs`); preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (`lightcraft_engine::import::EXTENSIONS`) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (`apps/lightcraft/src/dialog_filter.rs`, issue #342; 0.4.0 hid `.JPG` / `.NEF`, issue #705). The folder scan, Browse Folder, drag-and-drop and the sidecar lookup match extensions in any case (`crates/engine/src/tests_import.rs`) | -| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → the import review), `crates/ui-egui/src/drop_overlay.rs`, `crates/ui-egui/src/tests_drop_import.rs` | while files are dragged over the window a veil and a card say what dropping does (review N items for import, import N presets, open the face model; a note to close the open dialog first when one is open); photos and folders open the import review as File ▸ Import does (Settings ▸ Import ▸ Import dropped files without review imports them at once; the web build always does); a drop while a dialog is open is refused with a toast; the control channel's `ui.dropFiles` hovers or drops files for scripts; dropping onto a specific album not supported | +| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → the import review), `crates/ui-egui/src/drop_overlay.rs`, `crates/ui-egui/src/tests_drop_import.rs` | while files are dragged over the window a veil and a card say what dropping does (review N items for import, import N presets, open the face model; a note to close the open dialog first when one is open); photos and folders open the import review as File ▸ Import does (Settings ▸ Import ▸ Import dropped files without review imports them at once; the web build always does); a drop while a dialog is open (also in the web build) or while an import is running is refused with a toast, and the card says so; a finished scan waits for an open dialog to close before its review opens; the control channel's `ui.dropFiles` hovers or drops files for scripts; dropping onto a specific album not supported | | LR-IMP-DUPES | Skip duplicates by content | P1 | ✅ | `crates/engine/src/import.rs`, `crates/engine/src/tests_import.rs` | | | LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk); Settings → Import → Cameras and Cards can open the import review when a camera or card is connected (off by default; a card that was already in doesn't ask, and an open dialog or running import is finished first; `crates/ui-egui/src/import.rs`) | | LR-IMP-AUTO | Watched-folder auto import | P2 | ✅ | `cmd:library.autoImport`, `cmd:library.autoImportScan`, `crates/ui-egui/src/panels/settings.rs` | Settings ▸ Import ▸ Auto Import: a watched folder whose new photos are added (in place or copied) once complete, into an optional album; scanned every 3 s | @@ -927,4 +927,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-16 | minor | LR-VIEW-PHOTOGRID: when the photos shown change by those an edit touched (an import's batch, a capture time, a crop), the grid reads only those photos' dates and shapes again instead of every photo of the list (a 16-photo batch at 181,749 photos: grid frame 101 ms → 7 ms); status unchanged | | 2026-10-19 | minor | LRC-LIB-LIBFOLDERS: the sidebar trees indent by a narrower step (14 / 12 / 10 / 8 pt) when the deepest open row would not fit the panel, instead of cutting its name or scrolling sideways at once (issue #2298); status unchanged (✅) | | 2026-10-15 | patch | LR-EXP-DIM: Width & Height (`fit: widthHeight`) limits the width and the height as the photo stands, beside Dimensions (W × H, either orientation), as in Lightroom (#1620); status unchanged (✅) | -| 2026-10-17 | minor | LR-IMP-DRAGDROP: files dragged over the window show what dropping will do, and dropped photos and folders open the import review (Settings ▸ Import can import them at once); a drop while a dialog is open is refused; the web build tells the person when a dropped file can't be read or imported. LR-BEHAV-EMPTY-STATES, LR-BEHAV-DRAGDROP: text updated; status unchanged (✅) | +| 2026-10-17 | minor | LR-IMP-DRAGDROP: files dragged over the window show what dropping will do, and dropped photos and folders open the import review (Settings ▸ Import can import them at once); a drop while a dialog is open or an import is running is refused; a finished scan no longer replaces an open dialog; the web build tells the person when a dropped file can't be read or imported. LR-BEHAV-EMPTY-STATES, LR-BEHAV-DRAGDROP: text updated; status unchanged (✅) | diff --git a/docs/whats-new.md b/docs/whats-new.md index b0406a148..6628b5798 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -5,7 +5,10 @@ ### Dropping files shows what will happen - While files are dragged over the window, it dims and a card says what dropping will do: review the photos and folders for import, import the presets, or open the face model. If a dialog is open, the card asks you to close it - first, and a drop is refused with a message instead of replacing the dialog. + first, and a drop is refused with a message instead of replacing the dialog. The same goes while an import is + running, and in the web version. +- The import review of a finished scan (a drop, or File ▸ Import) no longer replaces a dialog you opened in the + meantime; it opens as soon as that dialog is closed. - Photos and folders dropped on the window now open the same import review as File ▸ Import, so you pick add, copy or move, the destination and a preset before anything is imported. Settings ▸ Import ▸ "Import dropped files without review" brings back the old behaviour of importing at once.