From 4af709995f05b90c115c89305e71f8287e21dd35 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 20:52:35 +0000 Subject: [PATCH] Fix #344: the import review checks or unchecks a file type at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With several file types among the candidates, the Import Photos review shows a toggle per type (ARW · 120, JPG · 120): unchecking JPG leaves the JPEGs kept beside the raws out of the import, checking a type checks its importable files (duplicates stay unchecked). JPEG/JPG and TIFF/TIF are one type; a file whose probe named no format goes by its extension. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/ui-egui/locales/de.json | 3 ++ crates/ui-egui/locales/es.json | 3 ++ crates/ui-egui/locales/ja.json | 3 ++ crates/ui-egui/locales/pt-br.json | 3 ++ crates/ui-egui/locales/ru.json | 3 ++ crates/ui-egui/locales/zh-hans.json | 3 ++ crates/ui-egui/locales/zh-hant.json | 3 ++ crates/ui-egui/src/import.rs | 80 +++++++++++++++++++++++++++++ docs/parity.md | 2 +- docs/whats-new.md | 2 + 10 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index ed685f812..49712c7bd 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -208,6 +208,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "Unzulässige Zeichen in Dateinamen (/ \\ : * ? \" < > |) werden zu -; ein vorhandener Name erhält -1, -2….", "Check All": "Alle auswählen", "Check Output Path": "Ausgabepfad prüfen", + "Check the files of this type": "Dateien dieses Typs auswählen", "Checking names…": "Namen werden geprüft…", "Choose Another Library…": "Andere Bibliothek auswählen…", "Choose Edit Settings to Copy": "Zu kopierende Bearbeitungseinstellungen auswählen", @@ -511,6 +512,7 @@ "File Size": "Dateigröße", "File Type": "Dateityp", "File name": "Dateiname", + "File types:": "Dateitypen:", "Filename": "Dateiname", "Fill": "Ausfüllen", "Fill Edges": "Ränder füllen", @@ -1485,6 +1487,7 @@ "Turn on Include subfolders, or pick another folder under Local": "Aktiviere „Unterordner einbeziehen“ oder wähle unter „Lokal“ einen anderen Ordner", "Type": "Typ", "Uncheck All": "Alle abwählen", + "Uncheck the files of this type": "Auswahl der Dateien dieses Typs aufheben", "Undo": "Rückgängig", "Unedited": "Unbearbeitet", "Unedited raws": "Unbearbeitete RAW-Fotos", diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index a4eeffe1d..794877ef7 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -188,6 +188,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "Los caracteres no permitidos en nombres de archivo (/ \\ : * ? \" < > |) se convierten en -; un nombre existente recibe -1, -2….", "Check All": "Marcar todo", "Check Output Path": "Comprobar ruta de salida", + "Check the files of this type": "Marcar los archivos de este tipo", "Checking names…": "Comprobando nombres…", "Choose Another Library…": "Elegir otra biblioteca…", "Choose Edit Settings to Copy": "Elegir ajustes de edición para copiar", @@ -478,6 +479,7 @@ "File Size": "Tamaño del archivo", "File Type": "Tipo de archivo", "File name": "Nombre de archivo", + "File types:": "Tipos de archivo:", "Filename": "Nombre de archivo", "Fill": "Rellenar", "Fill Edges": "Rellenar bordes", @@ -1421,6 +1423,7 @@ "Turn on Include subfolders, or pick another folder under Local": "Activar Incluir subcarpetas o elegir otra carpeta en Local", "Type": "Tipo", "Uncheck All": "Desmarcar todo", + "Uncheck the files of this type": "Desmarcar los archivos de este tipo", "Undo": "Deshacer", "Unedited": "Sin editar", "Unedited raws": "RAW sin editar", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index cbde1b82c..0a6312763 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -190,6 +190,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "ファイル名に使えない文字(/ \\ : * ? \" < > |)は - に置き換え、既存の名前と重なる場合は -1、-2… を付けます", "Check All": "すべて選択", "Check Output Path": "書き出し先のパスを確認", + "Check the files of this type": "この種類のファイルをすべてチェック", "Checking names…": "名前を確認中…", "Choose Edit Settings to Copy": "コピーする編集設定を選択", "Choose Edit Settings to Copy…": "コピーする編集設定を選択…", @@ -458,6 +459,7 @@ "File Path": "ファイルの場所", "File Size": "ファイルサイズ", "File Type": "ファイルの種類", + "File types:": "ファイルの種類:", "Filename": "ファイル名", "Fill": "画面いっぱい", "Film": "フィルム", @@ -1345,6 +1347,7 @@ "Turn Off": "無効にする", "Turn on Include subfolders, or pick another folder under Local": "サブフォルダを含める設定を有効にするか、ローカルから別のフォルダを選択してください", "Uncheck All": "すべて選択解除", + "Uncheck the files of this type": "この種類のファイルのチェックをすべて外す", "Undo": "取り消す", "Unedited": "未編集", "Unedited raws": "未編集のRAW", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 9ae6bfe30..b5dfc2542 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -195,6 +195,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "Caracteres não permitidos em nomes de arquivo (/ \\ : * ? \" < > |) viram -; um nome já existente recebe -1, -2….", "Check All": "Marcar todos", "Check Output Path": "Verificar caminho de saída", + "Check the files of this type": "Marcar os arquivos deste tipo", "Checking names…": "Verificando nomes…", "Choose Another Library…": "Escolher outra biblioteca…", "Choose Edit Settings to Copy": "Escolher ajustes de edição a copiar", @@ -485,6 +486,7 @@ "File Size": "Tamanho do arquivo", "File Type": "Tipo de arquivo", "File name": "Nome do arquivo", + "File types:": "Tipos de arquivo:", "Filename": "Nome do arquivo", "Fill": "Preencher", "Fill Edges": "Preencher bordas", @@ -1431,6 +1433,7 @@ "Turn on Include subfolders, or pick another folder under Local": "Ative Incluir subpastas ou escolha outra pasta em Local", "Type": "Tipo", "Uncheck All": "Desmarcar tudo", + "Uncheck the files of this type": "Desmarcar os arquivos deste tipo", "Undo": "Desfazer", "Unedited": "Não editada", "Unedited raws": "RAWs não editados", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 61d35e483..ef0e29952 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -155,6 +155,7 @@ "Catalog Statistics": "Статистика каталога", "Check All": "Отметить все", "Check Output Path": "Проверить путь вывода", + "Check the files of this type": "Отметить файлы этого типа", "Choose Another Library…": "Выбрать другую библиотеку…", "Choose Edit Settings to Copy": "Выбрать настройки для копирования", "Choose Edit Settings to Copy…": "Выбрать настройки для копирования…", @@ -395,6 +396,7 @@ "File Path": "Путь к файлу", "File Size": "Размер файла", "File name": "Имя файла", + "File types:": "Типы файлов:", "Fill Edges": "Заполнить края", "Film": "Плёнка", "Filmstrip": "Лента", @@ -1177,6 +1179,7 @@ "Turn on Include subfolders, or pick another folder under Local": "Включите «Включая вложенные папки» или выберите другую папку в «Локальном»", "Type": "Тип", "Uncheck All": "Снять все отметки", + "Uncheck the files of this type": "Снять отметку с файлов этого типа", "Undo": "Отменить", "Unedited": "Без правок", "Unflag": "Снять флаг", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index fe2e86723..226f38f06 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -194,6 +194,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "文件名中不允许的字符(/ \\ : * ? \" < > |)会替换为 -;与现有名称重复时添加 -1、-2…。", "Check All": "全部选中", "Check Output Path": "检查输出路径", + "Check the files of this type": "勾选此类型的所有文件", "Checking names…": "正在检查名称…", "Choose Another Library…": "选择其他图库…", "Choose Edit Settings to Copy": "选择要复制的编辑设置", @@ -481,6 +482,7 @@ "File Path": "文件路径", "File Size": "文件大小", "File Type": "文件类型", + "File types:": "文件类型:", "Filename": "文件名", "Fill": "填满", "First": "最早", @@ -1418,6 +1420,7 @@ "Turn on Include subfolders, or pick another folder under Local": "开启“包括子文件夹”,或在“本地”下选择其他文件夹", "Type": "类型", "Uncheck All": "全部取消选中", + "Uncheck the files of this type": "取消勾选此类型的所有文件", "Undo": "撤销", "Unedited": "未编辑", "Unedited raws": "未编辑的 RAW", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 4a1050a09..2056534a6 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -194,6 +194,7 @@ "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "檔名中不允許的字元(/ \\ : * ? \" < > |)會改為 -;與現有名稱重複時加上 -1、-2…。", "Check All": "全部勾選", "Check Output Path": "檢驗輸出路徑", + "Check the files of this type": "勾選此類型的所有檔案", "Checking names…": "正在檢查名稱…", "Choose Another Library…": "選擇其他照片庫…", "Choose Edit Settings to Copy": "選擇要複製的編輯設定", @@ -481,6 +482,7 @@ "File Path": "檔案路徑", "File Size": "檔案大小", "File Type": "檔案類型", + "File types:": "檔案類型:", "Filename": "檔名", "Fill": "填滿", "First": "最早", @@ -1418,6 +1420,7 @@ "Turn on Include subfolders, or pick another folder under Local": "啟用「包含子資料夾」,或在本機中選擇其他資料夾", "Type": "類型", "Uncheck All": "全部取消勾選", + "Uncheck the files of this type": "取消勾選此類型的所有檔案", "Undo": "復原", "Unedited": "未編輯", "Unedited raws": "未編輯的 RAW", diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index 0e330722d..1fe852904 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -87,6 +87,22 @@ pub struct ImportDialog { pub last_clicked: Option, } +/// A candidate's file type, as the review groups them: its format, or its extension when the +/// probe named none (`JPEG` and `JPG` are one type). +fn file_type(c: &ImportCandidate) -> String { + let f = if c.format.trim().is_empty() { + std::path::Path::new(&c.path).extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default() + } else { + c.format.clone() + }; + match f.trim().to_uppercase().as_str() { + "JPEG" => "JPG".into(), + "TIFF" => "TIF".into(), + "" => "?".into(), + other => other.to_string(), + } +} + impl ImportDialog { pub fn new(candidates: Vec) -> Self { let checked = candidates.iter().map(|c| c.duplicate.is_none() && c.error.is_none()).collect(); @@ -95,6 +111,29 @@ impl ImportDialog { pub fn importable(&self, i: usize) -> bool { self.candidates.get(i).is_some_and(|c| c.error.is_none() && (c.duplicate.is_none() || self.is_trashed(i) && !self.on_deleted.is_empty())) } + /// The file types among the candidates (`ARW`, `JPG`…, upper case, sorted) and how many of each. + pub fn file_types(&self) -> Vec<(String, usize)> { + let mut types: std::collections::BTreeMap = Default::default(); + for c in &self.candidates { + *types.entry(file_type(c)).or_default() += 1; + } + types.into_iter().collect() + } + /// Any file of type `ty` is checked. + pub fn type_checked(&self, ty: &str) -> bool { + self.candidates.iter().zip(&self.checked).any(|(c, on)| *on && file_type(c) == ty) + } + /// Check (the importable ones) or uncheck every file of type `ty`: the raws only, say, and not + /// the JPEGs kept beside them (issue #344). + pub fn check_type(&mut self, ty: &str, on: bool) { + for i in 0..self.candidates.len() { + let same = self.candidates.get(i).is_some_and(|c| file_type(c) == ty); + let importable = self.importable(i); + if same && let Some(c) = self.checked.get_mut(i) { + *c = on && importable; + } + } + } /// Candidate `i` is a file whose photo is in Recently Deleted. pub fn is_trashed(&self, i: usize) -> bool { self.trashed.get(i).copied().unwrap_or(false) @@ -819,6 +858,21 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) { } }); }); + // with more than one file type: a toggle per type checks or unchecks all of its files + let types = d.file_types(); + if types.len() > 1 { + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new(crate::i18n::tr("File types:")).color(t.text_dim)); + for (ty, count) in &types { + let on = d.type_checked(ty); + let r = crate::widgets::text_button(ui, &format!("importType:{ty}"), &format!("{ty} · {count}"), on) + .on_hover_text(crate::i18n::tr(if on { "Uncheck the files of this type" } else { "Check the files of this type" })); + if r.clicked() { + d.check_type(ty, !on); + } + } + }); + } // candidate grid let cell = 116.0; let avail = ui.available_width(); @@ -1390,6 +1444,32 @@ mod tests { ImportDialog::new(candidates) } + /// A toggle per file type: the raws only, without the JPEGs beside them (issue #344). + #[test] + fn file_types_check_and_uncheck_together() { + let mut candidates: Vec = + ["a.ARW", "a.JPG", "b.arw", "b.jpeg", "c.png"].iter().map(|p| ImportCandidate { path: p.to_string(), ..Default::default() }).collect(); + candidates[2].format = "ARW".into(); + candidates[3].format = "JPEG".into(); + candidates[2].duplicate = Some("content".into()); + let mut d = ImportDialog::new(candidates); + assert_eq!(d.file_types(), [("ARW".to_string(), 2), ("JPG".to_string(), 2), ("PNG".to_string(), 1)]); + assert!(d.type_checked("JPG")); + d.check_type("JPG", false); + assert_eq!(d.checked, [true, false, false, false, true]); + assert!(!d.type_checked("JPG")); + d.check_type("PNG", false); + d.check_type("ARW", true); + // the duplicate stays unchecked: it isn't importable + assert_eq!(d.checked, [true, false, false, false, false]); + d.check_type("JPG", true); + assert_eq!(d.checked, [true, true, false, true, false]); + assert_eq!(d.selected_paths(), ["a.ARW", "a.JPG", "b.jpeg"]); + // an unknown type changes nothing + d.check_type("CR3", false); + assert_eq!(d.checked, [true, true, false, true, false]); + } + #[test] fn click_toggles_one_candidate() { let mut d = dialog(); diff --git a/docs/parity.md b/docs/parity.md index b807e48d8..ca7b8ebb8 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -127,7 +127,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | 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) 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 with a progress window and 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, with a progress window and 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) | +| 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 with a progress window and 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, with a progress window and 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) | | 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) | diff --git a/docs/whats-new.md b/docs/whats-new.md index fc5f2f047..dd12ca7c5 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -47,6 +47,8 @@ (with their edits) or import them as new; the trash view's Photo menu has Empty Recently Deleted. - The Import Photos review opens bigger and can be resized; its photo grid fills it (issue #337). Shift-click checks or unchecks a range of photos (issue #338). +- When a folder holds several file types, the Import Photos review has a toggle per type (`ARW · 120`, `JPG · 120`): + import only the raws and leave the JPEGs beside them (issue #344). ### Languages - The interface is available in Spanish (issue #371), German and Russian (Edit ▸ Language), alongside English,