From 441f46683a242e90fdc0656fb36787bce3c2f0ce Mon Sep 17 00:00:00 2001 From: Michael Anderson Date: Tue, 13 Oct 2026 11:49:31 -0500 Subject: [PATCH] LR-IMP-DEVICE: Show the import review when a card is connected Adds an option under Settings > Import > Cameras and Cards that opens Import from Device when a camera or memory card shows up. It's off by default, and a card that was already in at startup doesn't ask. --- crates/engine/src/cmd/prefs.rs | 5 +++- crates/engine/src/devices.rs | 38 ++++++++++++++++++++++++ crates/engine/src/import.rs | 2 ++ crates/engine/src/tests_prefs.rs | 3 +- crates/ui-egui/locales/de.json | 1 + crates/ui-egui/locales/es.json | 1 + crates/ui-egui/locales/fr.json | 1 + crates/ui-egui/locales/ja.json | 1 + crates/ui-egui/locales/pt-br.json | 1 + crates/ui-egui/locales/ru.json | 1 + crates/ui-egui/locales/uk.json | 1 + crates/ui-egui/locales/zh-hans.json | 1 + crates/ui-egui/locales/zh-hant.json | 1 + crates/ui-egui/src/headless.rs | 42 +++++++++++++++++++++++++++ crates/ui-egui/src/import.rs | 22 ++++++++++++++ crates/ui-egui/src/lib.rs | 10 +++++++ crates/ui-egui/src/panels/settings.rs | 5 ++++ crates/ui-egui/src/state.rs | 5 ++++ docs/parity-checklist.md | 5 ++-- docs/whats-new.md | 7 +++++ 20 files changed, 149 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/prefs.rs b/crates/engine/src/cmd/prefs.rs index 78de9d850..1f1301a51 100644 --- a/crates/engine/src/cmd/prefs.rs +++ b/crates/engine/src/cmd/prefs.rs @@ -43,6 +43,9 @@ fn prefs(s: &mut Session, p: &Value) -> Result { if let Some(v) = i.get("perCamera").and_then(Value::as_bool) { d.per_camera = v; } + if let Some(v) = i.get("showOnCard").and_then(Value::as_bool) { + d.show_on_card = v; + } if let Some(v) = i.get("copyright").and_then(Value::as_str) { d.copyright = v.trim().to_string(); } @@ -107,7 +110,7 @@ pub fn specs() -> Vec { "Library Preferences", [], None, - "{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}", + "{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one), showOnCard?: bool (open the import review when a camera or card is connected)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}", always, prefs )] diff --git a/crates/engine/src/devices.rs b/crates/engine/src/devices.rs index 399d03fde..366cb5273 100644 --- a/crates/engine/src/devices.rs +++ b/crates/engine/src/devices.rs @@ -104,6 +104,18 @@ impl DeviceWatch { s.list.clone() } + /// Like [`DeviceWatch::get`], but `None` until the first scan has landed, so an empty list + /// means no cards rather than not looked yet. + pub fn get_scanned(self: &Arc) -> Option> { + let _ = self.get(); + let s = self.state(); + #[cfg(not(target_arch = "wasm32"))] + let scanned = s.scanned.is_some(); + #[cfg(target_arch = "wasm32")] + let scanned = false; + scanned.then(|| s.list.clone()) + } + /// Scan now on this thread, and serve the result to later [`DeviceWatch::get`] calls. pub fn scan_now(&self) -> Vec { let list = (self.scanner)(); @@ -148,6 +160,12 @@ pub fn devices() -> Vec { global().get() } +/// [`devices`], but `None` until the first scan lands. The import prompt uses it so a card that was +/// already in when LightCraft started isn't taken for a new one. +pub fn devices_scanned() -> Option> { + global().get_scanned() +} + /// Scan the mounted volumes now (blocking), for explicit requests. pub fn devices_now() -> Vec { global().scan_now() @@ -285,4 +303,24 @@ mod tests { assert!(watch.scan_now().is_empty()); assert_eq!(changes.load(Ordering::SeqCst), 2); } + + #[test] + fn get_scanned_waits_for_the_first_scan() { + let gate = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let g = gate.clone(); + let watch = DeviceWatch::new( + move || { + while !g.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(5)); + } + vec![card("NIKON Z 8")] + }, + Duration::from_secs(60), + ); + assert_eq!(watch.get_scanned(), None, "nothing scanned yet"); + assert_eq!(watch.get_scanned(), None); + gate.store(true, Ordering::SeqCst); + assert!(until(|| watch.get_scanned().is_some())); + assert_eq!(watch.get_scanned(), Some(vec![card("NIKON Z 8")])); + } } diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index ecb80741e..ffe979e01 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -310,6 +310,8 @@ pub struct ImportDefaults { pub auto_folder: Option, pub auto_copy: bool, pub auto_album: Option, + /// Open the import review when a camera or memory card is connected. + pub show_on_card: bool, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] diff --git a/crates/engine/src/tests_prefs.rs b/crates/engine/src/tests_prefs.rs index 3534064f2..dec2ddcc0 100644 --- a/crates/engine/src/tests_prefs.rs +++ b/crates/engine/src/tests_prefs.rs @@ -108,7 +108,7 @@ fn preferences_persist_with_the_library() { s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap(); s.execute( "library.preferences", - &json!({"import": {"rawPreset": "user.rawlook", "perCamera": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}), + &json!({"import": {"rawPreset": "user.rawlook", "perCamera": true, "showOnCard": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}), ) .unwrap(); s.close_library().unwrap(); @@ -118,6 +118,7 @@ fn preferences_persist_with_the_library() { assert!(s.xmp.auto_write); assert_eq!(s.import_defaults.raw_preset.as_deref(), Some("user.rawlook")); assert!(s.import_defaults.per_camera); + assert!(s.import_defaults.show_on_card); assert_eq!(s.import_defaults.cameras.len(), 1); assert_eq!(s.import_defaults.preset_for(true, "Canon EOS R5"), Some("user.canonlook")); assert_eq!(s.cache_mb, 512); diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 25c411ce0..c6cb6fdc5 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1402,6 +1402,7 @@ "Show overlay (O)": "Überlagerung anzeigen (O)", "Show ratings, flags and edit badges": "Bewertungen, Markierungen und Bearbeitungssymbole anzeigen", "Show the Navigator while zoomed in": "Navigator beim Hineinzoomen anzeigen", + "Show the import review when a camera or card is connected": "Importfenster öffnen, wenn eine Kamera oder Karte angeschlossen wird", "Show the template tags; click one to insert it": "Vorlagenplatzhalter anzeigen; zum Einfügen anklicken", "Show/Hide Mask": "Maske ein-/ausblenden", "Show/hide My Photos panel": "Bereich „Meine Fotos“ ein-/ausblenden", diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index ba3e1e331..1895de569 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1372,6 +1372,7 @@ "Show overlay (O)": "Mostrar superposición (O)", "Show ratings, flags and edit badges": "Mostrar valoraciones, marcas e indicadores de edición", "Show the Navigator while zoomed in": "Mostrar el Navegador al ampliar", + "Show the import review when a camera or card is connected": "Abrir la importación al conectar una cámara o tarjeta", "Show the template tags; click one to insert it": "Mostrar las etiquetas de plantilla; hacer clic en una para insertarla", "Show/Hide Mask": "Mostrar/ocultar máscara", "Show/hide My Photos panel": "Mostrar/ocultar panel Mis fotos", diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index b0becb154..cd3c5aec7 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -1338,6 +1338,7 @@ "Show overlay (O)": "Afficher l'incrustation (O)", "Show ratings, flags and edit badges": "Afficher les notes, les drapeaux et les pastilles de retouche", "Show the Navigator while zoomed in": "Afficher le Navigateur lors du zoom", + "Show the import review when a camera or card is connected": "Ouvrir l'importation lorsqu'un appareil photo ou une carte est connecté", "Show the template tags; click one to insert it": "Afficher les balises du modèle ; cliquez sur l'une d'elles pour l'insérer", "Show/Hide Mask": "Afficher/masquer le masque", "Show/hide My Photos panel": "Afficher/masquer le panneau Mes photos", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 7a282e676..dcbf07031 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1284,6 +1284,7 @@ "Show overlay (O)": "重ね表示(O)", "Show ratings, flags and edit badges": "評価・フラグ・編集済みマークを表示", "Show the Navigator while zoomed in": "拡大時にナビゲーターを表示", + "Show the import review when a camera or card is connected": "カメラやメモリーカードを接続したときに読み込み画面を表示", "Show the template tags; click one to insert it": "テンプレートのタグを表示(クリックで挿入)", "Show/Hide Mask": "マスクの表示/非表示", "Show/hide My Photos panel": "マイフォトパネルを表示/非表示", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index f8e4753e5..50197de2b 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1361,6 +1361,7 @@ "Show overlay (O)": "Mostrar sobreposição (O)", "Show ratings, flags and edit badges": "Mostrar avaliações, marcações e indicadores de edição", "Show the Navigator while zoomed in": "Mostrar o Navegador quando ampliado", + "Show the import review when a camera or card is connected": "Abrir a importação ao conectar uma câmera ou cartão", "Show the template tags; click one to insert it": "Mostrar os marcadores do modelo; clique em um para inseri-lo", "Show/Hide Mask": "Mostrar/ocultar máscara", "Show/hide My Photos panel": "Mostrar/ocultar painel Minhas fotos", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index c840218a0..493acd188 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1129,6 +1129,7 @@ "Show overlay (O)": "Показать наложение (O)", "Show ratings, flags and edit badges": "Показывать оценки, флаги и значки правок", "Show the Navigator while zoomed in": "Показывать навигатор при увеличении", + "Show the import review when a camera or card is connected": "Открывать импорт при подключении камеры или карты", "Show the template tags; click one to insert it": "Показать теги шаблона; щёлкните по тегу, чтобы вставить его", "Show/Hide Mask": "Показать/скрыть маску", "Show/hide My Photos panel": "Показать/скрыть панель «Мои фото»", diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 85a838098..2dc7c2209 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -1351,6 +1351,7 @@ "Show overlay (O)": "Показати накладання (O)", "Show ratings, flags and edit badges": "Показати оцінки, прапорці й позначки змін", "Show the Navigator while zoomed in": "Показувати навігатор під час збільшення", + "Show the import review when a camera or card is connected": "Відкривати імпорт під час підключення камери або карти", "Show the template tags; click one to insert it": "Показати теги шаблону; клацніть тег, щоб вставити його", "Show/Hide Mask": "Показати/приховати маску", "Show/hide My Photos panel": "Показати/приховати панель моїх фото", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index ad379f1ef..21359c381 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1346,6 +1346,7 @@ "Show overlay (O)": "显示叠加 (O)", "Show ratings, flags and edit badges": "显示星级、旗标和编辑标记", "Show the Navigator while zoomed in": "放大时显示导航器", + "Show the import review when a camera or card is connected": "连接相机或存储卡时显示导入窗口", "Show the template tags; click one to insert it": "显示模板标签;单击即可插入", "Show/Hide Mask": "显示/隐藏蒙版", "Show/hide My Photos panel": "显示/隐藏我的照片面板", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 647d153d0..2b4b79e18 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1346,6 +1346,7 @@ "Show overlay (O)": "顯示疊加(O)", "Show ratings, flags and edit badges": "顯示評分、旗標與編輯標記", "Show the Navigator while zoomed in": "放大時顯示導覽器", + "Show the import review when a camera or card is connected": "連接相機或記憶卡時顯示匯入視窗", "Show the template tags; click one to insert it": "顯示範本標籤;點擊即可插入", "Show/Hide Mask": "顯示/隱藏遮色片", "Show/hide My Photos panel": "顯示/隱藏我的照片面板", diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 0507ab31f..5a64d2036 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -2948,6 +2948,48 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + /// Settings → Import → "Show the import review when a camera or card is connected": a card that + /// was already in doesn't ask, a new one opens Import from Device (copying), and a busy window + /// asks once it's free. + #[test] + fn a_connected_card_opens_the_import_review() { + let mut h = demo([1200.0, 900.0]); + let t = Duration::from_secs(10); + let root = std::env::temp_dir().join(format!("lc-card-{}", std::process::id())); + let dcim = root.join("DCIM/100NIKON"); + std::fs::create_dir_all(&dcim).unwrap(); + let img = lightcraft_raster::Rgba8::from_fn(8, 8, |x, y| [(x * 9) as u8, (y * 12) as u8, 80, 255]); + let o = lightcraft_engine::export::ExportOptions { format: lightcraft_engine::export::ExportFormat::Png, ..Default::default() }; + std::fs::write(dcim.join("DSC_0001.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap(); + let card = lightcraft_engine::devices::Device { + name: "NIKON Z 8".into(), + path: root.join("DCIM").to_string_lossy().to_string(), + root: root.to_string_lossy().to_string(), + }; + h.app.ui.dialog = None; + // first look: the card was already in + crate::import::cards_seen(&mut h.app, vec![card.clone()]); + assert!(h.app.scan.is_none() && h.app.ui.dialog.is_none(), "a card that was already in doesn't ask"); + crate::import::cards_seen(&mut h.app, vec![card.clone()]); + assert!(h.app.scan.is_none()); + // taken out, then put back while another dialog is open: it waits + crate::import::cards_seen(&mut h.app, Vec::new()); + h.app.ui.dialog = Some(crate::state::Dialog::About); + crate::import::cards_seen(&mut h.app, vec![card.clone()]); + assert!(h.app.scan.is_none(), "a busy window isn't interrupted"); + h.app.ui.dialog = None; + crate::import::cards_seen(&mut h.app, vec![card.clone()]); + assert!(h.app.scan.is_some(), "the card is scanned once the window is free"); + assert!(h.step_until(t, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })))); + let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") }; + assert!(opts.copy, "a card is copied into the library"); + // with the option off nothing is remembered, so turning it on again starts with a first look + assert!(!h.app.session.import_defaults.show_on_card); + h.step(); + assert!(h.app.ui.known_cards.is_none()); + let _ = std::fs::remove_dir_all(&root); + } + /// The folder scan runs in the background: the request returns at once and the review opens /// when the scan finishes (a folder on a network share must not freeze the window). /// Clicking the folder being read again keeps the running read (and its progress). diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index a1a720a8e..569e13fc2 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -444,6 +444,28 @@ pub(crate) fn busy_synchronizing(app: &LightcraftApp) -> Result<(), String> { if app.sync_run.is_some() { Err(crate::i18n::tr("A folder is being synchronized").to_string()) } else { Ok(()) } } +/// Settings → Import → "Show the import review when a camera or card is connected", called with +/// each device scan while it's on. The first look only notes the cards already in. A card that +/// shows up after that opens Import from Device once nothing else is open, and a card that is +/// taken out and put back in asks again. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn cards_seen(app: &mut LightcraftApp, now: Vec) { + let Some(known) = app.ui.known_cards.as_mut() else { + app.ui.known_cards = Some(now.into_iter().map(|d| d.root).collect()); + return; + }; + known.retain(|root| now.iter().any(|d| d.root == *root)); + let Some(card) = now.into_iter().find(|d| !known.contains(&d.root)) else { return }; + // busy: ask when the window is free (the card isn't noted until then) + if app.import.is_some() || app.scan.is_some() || app.sync_run.is_some() || app.lightroom.is_some() || app.ui.dialog.is_some() { + return; + } + known.push(card.root); + if let Err(e) = app.run("file.addFromDevice", json!({"path": card.path})) { + log::warn!("import from {}: {e}", card.name); + } +} + /// Scan `paths` in the background, then open the review dialog (see [`poll_scan`]). pub fn open(app: &mut LightcraftApp, paths: Vec) -> Result { if app.scan.is_some() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 37fb86df3..6145498c5 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1092,6 +1092,16 @@ impl LightcraftApp { } ctx.request_repaint_after(std::time::Duration::from_secs(3)); } + // Settings → Import: open the import review when a camera or card is connected + #[cfg(not(target_arch = "wasm32"))] + if self.session.import_defaults.show_on_card { + if let Some(now) = lightcraft_engine::devices::devices_scanned() { + import::cards_seen(self, now); + } + ctx.request_repaint_after(lightcraft_engine::devices::MAX_AGE); + } else { + self.ui.known_cards = None; + } self.session.persist_if_dirty(); panels::denoise::pump(self, ctx); model_setup::pump(self, ctx); diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index a0717ccfd..29f114ea7 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -358,6 +358,11 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) { } }); if !cfg!(target_arch = "wasm32") { + heading(ui, t, crate::i18n::tr("Cameras and Cards")); + let mut on_card = d.show_on_card; + if check(ui, "settings.importOnCard", &mut on_card, "Show the import review when a camera or card is connected") { + let _ = app.run("library.preferences", json!({"import": {"showOnCard": on_card}})); + } heading(ui, t, crate::i18n::tr("Auto Import")); hint(ui, t, crate::i18n::tr("Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete.")); row(ui, t, crate::i18n::tr("Watched folder"), |ui| { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index cc583e7b3..a0d2b37df 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -613,6 +613,10 @@ pub struct UiState { /// When the watched folder was last scanned (egui time). #[serde(skip)] pub auto_import_at: f64, + /// The cards and cameras already seen (volume roots) while Settings → Import → "Show the + /// import review when a camera or card is connected" is on; `None` = not looked yet. + #[serde(skip)] + pub known_cards: Option>, /// The develop control whose slider is being dragged (geometry sliders show a grid). #[serde(skip)] pub dragging_control: Option, @@ -1101,6 +1105,7 @@ impl Default for UiState { external_edits: Vec::new(), was_focused: true, auto_import_at: 0.0, + known_cards: None, search: String::new(), focus_search: false, renaming_mask: None, diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 7bb187811..97b61004f 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (LR-IMP-DEVICE: open the import review when a camera or card is connected; integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -72,7 +72,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | 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), 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 → `cmd:library.import`) | 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) | +| 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 | | LR-IMP-PRESET | Preset on import | P2 | ✅ | `cmd:library.import` (`preset`) | chosen in the import review; one History entry | | LR-IMP-RAWDEFAULT | Raw defaults | P1 | ✅ | `cmd:library.preferences`, `crates/engine/src/import.rs`, `crates/ui-egui/src/panels/settings.rs` | LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it | @@ -795,6 +795,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-13 | patch | LRC-LIB-CATALOG-IMPORT: a catalog whose tables are all still in its `-wal` file (main file schema format 0) opens instead of failing as an unsupported schema format; status unchanged | | 2026-10-14 | patch | LR-REM-HEAL, LR-REM-CLONE: zero-feather edges preserve exact-radius pixels; zero-opacity regression covers all spot modes and image corners. Statuses and feature counts unchanged. | | 2026-10-13 | patch | LR-EXP-DIM: an enlarged export stops at 512 megapixels and is refused when it needs more memory than the computer has, instead of aborting the app; status unchanged | +| 2026-10-13 | minor | LR-IMP-DEVICE: Settings → Import → Cameras and Cards can open the import review when a camera or card is connected; status unchanged (✅) | | 2026-10-11 | minor | LR-MERGE-HDR: HDR and panorama merges of raws without colour matrices (CR2, NEF, ARW…) write the colour they open with as DNG tags (issue #965); status unchanged | | 2026-10-12 | minor | LR-META-XMP: Read Metadata from File no longer auto-writes the sidecar it has just read, which added `lc:settings` and hid another app's later `crs:` edits (#934); status unchanged (✅) | | 2026-10-12 | patch | LR-IMP-CAMERA-COVERAGE: CR2 white level from the active area, not the saturated masked rows above it (issue #1272); status unchanged (🟡) | diff --git a/docs/whats-new.md b/docs/whats-new.md index 1bf44c3f4..e2fee05a0 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,11 @@ ## 2026-10-14 +### Import when a card is connected +- Settings → Import → Cameras and Cards has an option to open the import review as soon as a camera or memory card + is connected, the same review File → Import from Device opens. It's off by default. A card that's already in when + LightCraft starts doesn't ask, and if a dialog is open or an import is running it waits until that's done. + ### Hard-edged Heal and Clone spots - A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged. @@ -109,6 +114,8 @@ LightCraft: an enlargement stops at 512 megapixels, as in Lightroom, and one that needs more memory than the computer has is refused with the size it would take, before it starts. +## 2026-10-13 + ### Slider drags on large raws - Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's