mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 04:57:22 -06:00
Merge branch 'fix-344-import-file-types' into integ-b6
This commit is contained in:
@@ -210,6 +210,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",
|
||||
@@ -516,6 +517,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",
|
||||
@@ -1522,6 +1524,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",
|
||||
|
||||
@@ -190,6 +190,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",
|
||||
@@ -483,6 +484,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",
|
||||
@@ -1461,6 +1463,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",
|
||||
|
||||
@@ -192,6 +192,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…": "コピーする編集設定を選択…",
|
||||
@@ -463,6 +464,7 @@
|
||||
"File Path": "ファイルの場所",
|
||||
"File Size": "ファイルサイズ",
|
||||
"File Type": "ファイルの種類",
|
||||
"File types:": "ファイルの種類:",
|
||||
"Filename": "ファイル名",
|
||||
"Fill": "画面いっぱい",
|
||||
"Film": "フィルム",
|
||||
@@ -1382,6 +1384,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",
|
||||
|
||||
@@ -197,6 +197,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",
|
||||
@@ -490,6 +491,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",
|
||||
@@ -1471,6 +1473,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",
|
||||
|
||||
@@ -157,6 +157,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…": "Выбрать настройки для копирования…",
|
||||
@@ -400,6 +401,7 @@
|
||||
"File Path": "Путь к файлу",
|
||||
"File Size": "Размер файла",
|
||||
"File name": "Имя файла",
|
||||
"File types:": "Типы файлов:",
|
||||
"Fill Edges": "Заполнить края",
|
||||
"Film": "Плёнка",
|
||||
"Filmstrip": "Лента",
|
||||
@@ -1217,6 +1219,7 @@
|
||||
"Turn on Include subfolders, or pick another folder under Local": "Включите «Включая вложенные папки» или выберите другую папку в «Локальном»",
|
||||
"Type": "Тип",
|
||||
"Uncheck All": "Снять все отметки",
|
||||
"Uncheck the files of this type": "Снять отметку с файлов этого типа",
|
||||
"Undo": "Отменить",
|
||||
"Unedited": "Без правок",
|
||||
"Unflag": "Снять флаг",
|
||||
|
||||
@@ -196,6 +196,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": "选择要复制的编辑设置",
|
||||
@@ -486,6 +487,7 @@
|
||||
"File Path": "文件路径",
|
||||
"File Size": "文件大小",
|
||||
"File Type": "文件类型",
|
||||
"File types:": "文件类型:",
|
||||
"Filename": "文件名",
|
||||
"Fill": "填满",
|
||||
"First": "最早",
|
||||
@@ -1455,6 +1457,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",
|
||||
|
||||
@@ -196,6 +196,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": "選擇要複製的編輯設定",
|
||||
@@ -486,6 +487,7 @@
|
||||
"File Path": "檔案路徑",
|
||||
"File Size": "檔案大小",
|
||||
"File Type": "檔案類型",
|
||||
"File types:": "檔案類型:",
|
||||
"Filename": "檔名",
|
||||
"Fill": "填滿",
|
||||
"First": "最早",
|
||||
@@ -1455,6 +1457,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",
|
||||
|
||||
@@ -89,6 +89,22 @@ pub struct ImportDialog {
|
||||
pub last_clicked: Option<usize>,
|
||||
}
|
||||
|
||||
/// 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<ImportCandidate>) -> Self {
|
||||
let checked = candidates.iter().map(|c| c.duplicate.is_none() && c.error.is_none()).collect();
|
||||
@@ -97,6 +113,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<String, usize> = 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)
|
||||
@@ -821,6 +860,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();
|
||||
@@ -1392,6 +1446,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<ImportCandidate> =
|
||||
["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();
|
||||
|
||||
+1
-1
@@ -124,7 +124,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) |
|
||||
|
||||
@@ -58,6 +58,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,
|
||||
|
||||
Reference in New Issue
Block a user