Merge PR #1216
# Conflicts: # crates/engine/src/files.rs # crates/ui-egui/locales/de-formats.json # crates/ui-egui/locales/de.json # crates/ui-egui/locales/es-formats.json # crates/ui-egui/locales/es.json # crates/ui-egui/locales/fr-formats.json # crates/ui-egui/locales/fr.json # crates/ui-egui/locales/ja-formats.json # crates/ui-egui/locales/ja.json # crates/ui-egui/locales/pt-br-formats.json # crates/ui-egui/locales/pt-br.json # crates/ui-egui/locales/ru-formats.json # crates/ui-egui/locales/ru.json # crates/ui-egui/locales/uk-formats.json # crates/ui-egui/locales/uk.json # crates/ui-egui/locales/zh-hans-formats.json # crates/ui-egui/locales/zh-hans.json # crates/ui-egui/locales/zh-hant-formats.json # crates/ui-egui/locales/zh-hant.json # docs/parity-checklist.md
This commit is contained in:
commit
84d6849cf3
|
|
@ -176,6 +176,7 @@ pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
|
|||
kind: MediaKind::Raw,
|
||||
file_size: bytes.len() as u64,
|
||||
captured,
|
||||
file_modified: None,
|
||||
meta,
|
||||
as_shot_wb,
|
||||
content_hash,
|
||||
|
|
@ -207,6 +208,7 @@ pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
|
|||
kind: MediaKind::Image,
|
||||
file_size: bytes.len() as u64,
|
||||
captured,
|
||||
file_modified: None,
|
||||
meta,
|
||||
as_shot_wb: None,
|
||||
content_hash,
|
||||
|
|
@ -984,11 +986,21 @@ pub fn fs_hooks() -> (FileLoader, FileProbe) {
|
|||
let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0);
|
||||
let _permit = crate::memory::work_gate().acquire(len);
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
|
||||
probe_bytes(path, &bytes)
|
||||
let mut info = probe_bytes(path, &bytes)?;
|
||||
info.file_modified = file_modified(path);
|
||||
Ok(info)
|
||||
});
|
||||
(loader, probe)
|
||||
}
|
||||
|
||||
/// `path`'s modification time as ISO 8601 (see [`ProbeInfo::file_modified`]); `None` when the
|
||||
/// filesystem doesn't say or the time is before 1970.
|
||||
fn file_modified(path: &str) -> Option<String> {
|
||||
let t = std::fs::metadata(path).ok()?.modified().ok()?;
|
||||
let secs = i64::try_from(t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs()).ok()?;
|
||||
Some(crate::import::civil(secs))
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// Install the filesystem file hooks (desktop, CLI, MCP headless), and on macOS the Apple Photos
|
||||
/// runner ([`crate::apple_photos::osascript`]; installed, not run).
|
||||
|
|
@ -1281,6 +1293,28 @@ mod tests {
|
|||
let (_, window, read) = preview_window(&path).expect("the file reads").expect("sRGB is what the JPEG-only decode assumes");
|
||||
assert!(read.planned, "the plan still answers");
|
||||
assert!(window.starts_with(&[0xff, 0xd8]), "and hands on the JPEG");
|
||||
/// A JPEG with no date in its metadata (an export saved without it) takes the file's date, as
|
||||
/// in Lightroom, instead of grouping under "Unknown Date"; a dated one keeps its capture time.
|
||||
#[test]
|
||||
fn undated_files_take_the_file_date() {
|
||||
let dir = std::env::temp_dir().join(format!("lightcraft-undated-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let rgb = vec![128u8; 16 * 16 * 3];
|
||||
let img = EncodeImage::new(16, 16, 3, Samples::U8(&rgb));
|
||||
let jpeg = encode_jpeg(&img, 90, ChromaSubsampling::S420, &EncodeMeta::default()).unwrap();
|
||||
let path = dir.join("export.jpg");
|
||||
std::fs::write(&path, &jpeg).unwrap();
|
||||
let path = path.to_string_lossy().to_string();
|
||||
let mtime = std::fs::metadata(&path).unwrap().modified().unwrap().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let want = crate::import::civil(mtime as i64);
|
||||
let (_, probe) = fs_hooks();
|
||||
let info = probe(&path).unwrap();
|
||||
assert_eq!(info.captured, None, "the file itself has no date");
|
||||
assert_eq!(info.file_modified.as_deref(), Some(want.as_str()));
|
||||
let mut s = crate::Session::new().with_fs();
|
||||
let r = crate::import::import(&mut s, &[path], crate::import::ImportMode::Add).unwrap();
|
||||
let id = lightcraft_catalog::PhotoId(r.imported[0]);
|
||||
assert_eq!(s.catalog.photo(id).unwrap().captured.as_deref(), Some(want.as_str()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -649,6 +649,8 @@ pub fn scan_with(mut input: ScanInput, paths: &[String], progress: &ScanProgress
|
|||
Some(Ok(info)) => {
|
||||
(c.format, c.kind, c.width, c.height, c.file_size, c.captured) =
|
||||
(info.format.clone(), info.kind, info.width, info.height, info.file_size, info.captured.clone());
|
||||
// as the import will date it (a sidecar's date, when there is one, still wins there)
|
||||
c.captured = c.captured.or_else(|| info.file_modified.clone());
|
||||
c.preview_only = info.preview_only.clone();
|
||||
if let Some(h) = &info.content_hash {
|
||||
match seen_hash.get(h) {
|
||||
|
|
@ -990,6 +992,10 @@ impl ImportJob {
|
|||
if info.captured.is_none() {
|
||||
info.captured = sidecar.as_ref().and_then(|sc| sc.captured.clone());
|
||||
}
|
||||
// no date in the file or its sidecar: the file's own date, as Lightroom does
|
||||
if info.captured.is_none() {
|
||||
info.captured = info.file_modified.clone();
|
||||
}
|
||||
let stored = match (mode, &self.copy_root) {
|
||||
(ImportMode::Copy | ImportMode::Move, Some(root))
|
||||
if !crate::import_move::inside(Path::new(&path), root)
|
||||
|
|
|
|||
|
|
@ -1551,6 +1551,10 @@ pub struct ProbeInfo {
|
|||
pub kind: MediaKind,
|
||||
pub file_size: u64,
|
||||
pub captured: Option<String>,
|
||||
/// The file's modification time (ISO 8601, the clock of [`crate::import::system_clock`]): the
|
||||
/// capture date of a file whose metadata has none, as Lightroom does (exports saved without
|
||||
/// metadata would otherwise all land under "Unknown Date").
|
||||
pub file_modified: Option<String>,
|
||||
pub meta: lightcraft_catalog::Meta,
|
||||
pub as_shot_wb: Option<(f64, f64)>,
|
||||
/// Hash of the file's bytes (hex), for duplicate detection. When it is a 32-digit
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ fn write_png(path: &Path, seed: u8) {
|
|||
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
// undated files take the file's date: the tests' clock time (2026-01-14T05:58:48)
|
||||
let t = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_768_370_328);
|
||||
std::fs::File::options().write(true).open(path).unwrap().set_modified(t).unwrap();
|
||||
}
|
||||
|
||||
fn ids(v: &Value, key: &str) -> usize {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ fn write_png(path: &Path, seed: u8) {
|
|||
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
// undated files take the file's date: the tests' clock time (2026-01-14T05:58:48)
|
||||
let t = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_768_370_328);
|
||||
std::fs::File::options().write(true).open(path).unwrap().set_modified(t).unwrap();
|
||||
}
|
||||
|
||||
fn xmp(rating: u8) -> String {
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "{n} zu Apple Fotos hinzugefügt",
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} Fotos zur Bearbeitung geöffnet; jedes ist mit seinem Original gestapelt",
|
||||
"{name} opened in the other editor": "{name} im anderen Editor geöffnet",
|
||||
"{n} originals opened in the other editor": "{n} Originale im anderen Editor geöffnet"
|
||||
"{n} originals opened in the other editor": "{n} Originale im anderen Editor geöffnet",
|
||||
"· {in_source} identical copies": "· {in_source} identische Kopien"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2115,5 +2115,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "Beim ersten Mal fragt macOS, ob LightCraft Fotos steuern darf.",
|
||||
"Adding to Apple Photos": "Wird zu Apple Fotos hinzugefügt",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Kein Grafikprozessor gefunden: LightCraft rendert auf der CPU, was viel langsamer ist. Details unter Hilfe → Systeminformationen.",
|
||||
"On NixOS, start it from `nix develop`.": "Unter NixOS starten Sie es aus `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "Unter NixOS starten Sie es aus `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Dateien mit demselben Inhalt wie eine andere Datei dieses Imports: Nur die erste wird importiert."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "{n} añadidas a Fotos de Apple",
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abiertas para editar; cada una está apilada con su original",
|
||||
"{name} opened in the other editor": "{name} abierto en el otro editor",
|
||||
"{n} originals opened in the other editor": "{n} originales abiertos en el otro editor"
|
||||
"{n} originals opened in the other editor": "{n} originales abiertos en el otro editor",
|
||||
"· {in_source} identical copies": "· {in_source} copias idénticas"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1992,5 +1992,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "La primera vez, macOS pregunta si LightCraft puede controlar Fotos.",
|
||||
"Adding to Apple Photos": "Añadiendo a Fotos de Apple",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "No se encontró ningún procesador gráfico: LightCraft renderiza en la CPU, lo que es mucho más lento. Detalles en Ayuda → Información del sistema.",
|
||||
"On NixOS, start it from `nix develop`.": "En NixOS, inícielo desde `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "En NixOS, inícielo desde `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Archivos con el mismo contenido que otro archivo de esta importación: solo se importa el primero."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "{n} ajouté(s) à Apple Photos",
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} photos ouvertes pour modification ; chacune est empilée avec son original",
|
||||
"{name} opened in the other editor": "{name} ouvert dans l’autre éditeur",
|
||||
"{n} originals opened in the other editor": "{n} originaux ouverts dans l’autre éditeur"
|
||||
"{n} originals opened in the other editor": "{n} originaux ouverts dans l’autre éditeur",
|
||||
"· {in_source} identical copies": "· {in_source} copies identiques"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1954,5 +1954,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "La première fois, macOS demande si LightCraft peut contrôler Photos.",
|
||||
"Adding to Apple Photos": "Ajout à Apple Photos",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Aucun processeur graphique trouvé : LightCraft effectue le rendu sur le processeur, ce qui est beaucoup plus lent. Détails dans Aide → Informations système.",
|
||||
"On NixOS, start it from `nix develop`.": "Sous NixOS, lancez-le depuis `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "Sous NixOS, lancez-le depuis `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Fichiers au contenu identique à un autre fichier de cette importation : seul le premier est importé."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "{n}件をAppleの写真に追加しました",
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています",
|
||||
"{name} opened in the other editor": "{name}を別のエディタで開きました",
|
||||
"{n} originals opened in the other editor": "{n}個の元ファイルを別のエディタで開きました"
|
||||
"{n} originals opened in the other editor": "{n}個の元ファイルを別のエディタで開きました",
|
||||
"· {in_source} identical copies": "・同一のコピー {in_source} 件"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1958,5 +1958,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "初回は、LightCraftに写真の操作を許可するかmacOSが確認します。",
|
||||
"Adding to Apple Photos": "Appleの写真に追加中",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "グラフィックプロセッサが見つかりません。LightCraftはCPUで描画するため、かなり遅くなります。詳細は「ヘルプ → システム情報」を参照してください。",
|
||||
"On NixOS, start it from `nix develop`.": "NixOSでは `nix develop` から起動してください。"
|
||||
"On NixOS, start it from `nix develop`.": "NixOSでは `nix develop` から起動してください。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "この読み込み内の別のファイルと内容が同じファイルです。最初のファイルだけが読み込まれます。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "{n} adicionadas ao Fotos da Apple",
|
||||
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abertas para edição; cada uma está empilhada com a original",
|
||||
"{name} opened in the other editor": "{name} aberto no outro editor",
|
||||
"{n} originals opened in the other editor": "{n} originais abertos no outro editor"
|
||||
"{n} originals opened in the other editor": "{n} originais abertos no outro editor",
|
||||
"· {in_source} identical copies": "· {in_source} cópias idênticas"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1995,5 +1995,6 @@
|
|||
"Detect Faces (Photo menu) needs it: a small one-time download.": "Detectar rostos (menu Foto) precisa dele: um pequeno download único.",
|
||||
"Recognition models": "Modelos de reconhecimento",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Nenhum processador gráfico encontrado: o LightCraft renderiza na CPU, o que é muito mais lento. Detalhes em Ajuda → Informações do sistema.",
|
||||
"On NixOS, start it from `nix develop`.": "No NixOS, inicie-o a partir de `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "No NixOS, inicie-o a partir de `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Arquivos com o mesmo conteúdo de outro arquivo desta importação: apenas o primeiro é importado."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "Добавлено в Apple Фото: {n}",
|
||||
"{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом",
|
||||
"{name} opened in the other editor": "{name} открыт в другом редакторе",
|
||||
"{n} originals opened in the other editor": "Открыто оригиналов в другом редакторе: {n}"
|
||||
"{n} originals opened in the other editor": "Открыто оригиналов в другом редакторе: {n}",
|
||||
"· {in_source} identical copies": "· идентичных копий: {in_source}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2148,5 +2148,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "В первый раз macOS спросит, может ли LightCraft управлять приложением «Фото».",
|
||||
"Adding to Apple Photos": "Добавление в Apple Фото",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Графический процессор не найден: LightCraft выполняет рендеринг на CPU, а это намного медленнее. Подробности: Справка → Сведения о системе.",
|
||||
"On NixOS, start it from `nix develop`.": "В NixOS запускайте его из `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "В NixOS запускайте его из `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файлы с тем же содержимым, что и другой файл этого импорта: импортируется только первый."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "Додано до Apple Фото: {n}",
|
||||
"{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом",
|
||||
"{name} opened in the other editor": "{name} відкрито в іншому редакторі",
|
||||
"{n} originals opened in the other editor": "Відкрито оригіналів в іншому редакторі: {n}"
|
||||
"{n} originals opened in the other editor": "Відкрито оригіналів в іншому редакторі: {n}",
|
||||
"· {in_source} identical copies": "· ідентичних копій: {in_source}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2148,5 +2148,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "Першого разу macOS запитає, чи може LightCraft керувати програмою «Фото».",
|
||||
"Adding to Apple Photos": "Додавання до Apple Фото",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Графічний процесор не знайдено: LightCraft виконує рендеринг на ЦП, а це набагато повільніше. Докладніше: Довідка → Інформація про систему.",
|
||||
"On NixOS, start it from `nix develop`.": "У NixOS запускайте його з `nix develop`."
|
||||
"On NixOS, start it from `nix develop`.": "У NixOS запускайте його з `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Файли з тим самим вмістом, що й інший файл цього імпорту: імпортується лише перший."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "已将 {n} 个添加到 Apple 照片",
|
||||
"{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起",
|
||||
"{name} opened in the other editor": "已在其他编辑器中打开 {name}",
|
||||
"{n} originals opened in the other editor": "已在其他编辑器中打开 {n} 个原始文件"
|
||||
"{n} originals opened in the other editor": "已在其他编辑器中打开 {n} 个原始文件",
|
||||
"· {in_source} identical copies": "· {in_source} 个相同副本"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1994,5 +1994,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "首次使用时,macOS 会询问是否允许 LightCraft 控制“照片”。",
|
||||
"Adding to Apple Photos": "正在添加到 Apple 照片",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "未找到图形处理器:LightCraft 改用 CPU 渲染,速度会慢很多。详情见“帮助 → 系统信息”。",
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,请从 `nix develop` 启动。"
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,请从 `nix develop` 启动。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "与本次导入中另一个文件内容相同的文件:只导入第一个。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,6 @@
|
|||
"{n} added to Apple Photos": "已將 {n} 個加入 Apple 照片",
|
||||
"{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起",
|
||||
"{name} opened in the other editor": "已在其他編輯器中開啟 {name}",
|
||||
"{n} originals opened in the other editor": "已在其他編輯器中開啟 {n} 個原始檔案"
|
||||
"{n} originals opened in the other editor": "已在其他編輯器中開啟 {n} 個原始檔案",
|
||||
"· {in_source} identical copies": "· {in_source} 個相同副本"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1956,5 +1956,6 @@
|
|||
"The first time, macOS asks whether LightCraft may control Photos.": "第一次使用時,macOS 會詢問是否允許 LightCraft 控制「照片」。",
|
||||
"Adding to Apple Photos": "正在加入 Apple 照片",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "找不到圖形處理器:LightCraft 改用 CPU 算繪,速度會慢很多。詳情請見「說明 → 系統資訊」。",
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,請從 `nix develop` 啟動。"
|
||||
"On NixOS, start it from `nix develop`.": "在 NixOS 上,請從 `nix develop` 啟動。",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "與本次匯入中另一個檔案內容相同的檔案:只匯入第一個。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3474,10 +3474,16 @@ mod tests {
|
|||
let img = lightcraft_raster::Rgba8 { width: 8, height: 8, data: vec![[40, 3, 9, 255]; 64] };
|
||||
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
std::fs::write(src.join("IMG_01.png"), png).unwrap();
|
||||
// undated files are filed by the file's date (2026-01-14T05:58:48)
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(src.join("IMG_01.png"))
|
||||
.unwrap()
|
||||
.set_modified(std::time::UNIX_EPOCH + Duration::from_secs(1_768_370_328))
|
||||
.unwrap();
|
||||
let dest_s = dest.to_string_lossy().to_string();
|
||||
let services = crate::Services { png: None, pick_folder: Some(Box::new(move || Some(dest_s.clone()))), ..Default::default() };
|
||||
let mut session = lightcraft_engine::Session::with_demo().with_fs();
|
||||
// undated files are filed by the import time
|
||||
session.clock = Box::new(|| "2026-01-14T05:58:48".to_string());
|
||||
let mut app = LightcraftApp::new(session, services);
|
||||
app.ui.view = crate::state::ViewMode::PhotoGrid;
|
||||
|
|
@ -3527,6 +3533,12 @@ mod tests {
|
|||
let img = lightcraft_raster::Rgba8 { width: 8, height: 8, data: vec![[40 + i, 3, 9, 255]; 64] };
|
||||
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
std::fs::write(src.join(format!("IMG_0{i}.png")), png).unwrap();
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(src.join(format!("IMG_0{i}.png")))
|
||||
.unwrap()
|
||||
.set_modified(std::time::UNIX_EPOCH + Duration::from_secs(1_768_370_328))
|
||||
.unwrap();
|
||||
}
|
||||
let dest_s = dest.to_string_lossy().to_string();
|
||||
let services = crate::Services { png: None, pick_folder: Some(Box::new(move || Some(dest_s.clone()))), ..Default::default() };
|
||||
|
|
|
|||
|
|
@ -922,19 +922,31 @@ fn show_existing(app: &mut LightcraftApp, ctx: &egui::Context, existing: &[u64])
|
|||
true
|
||||
}
|
||||
|
||||
/// Skipped candidates: (already in the library, identical copies within the source, in Recently
|
||||
/// Deleted). Files of photos in Recently Deleted are not "already in the library", and a file with
|
||||
/// the same bytes as one earlier in the list (no library photo) is a copy in the source.
|
||||
fn dup_counts(d: &ImportDialog) -> (usize, usize, usize) {
|
||||
let trashed = d.trashed.iter().filter(|t| **t).count();
|
||||
let in_source = d.candidates.iter().filter(|c| c.duplicate.as_deref() == Some("content") && c.existing.is_none()).count();
|
||||
let dups = d.candidates.iter().filter(|c| c.duplicate.is_some()).count().saturating_sub(trashed).saturating_sub(in_source);
|
||||
(dups, in_source, trashed)
|
||||
}
|
||||
|
||||
/// The dialog body: options, then the candidate grid.
|
||||
pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let n = d.candidates.len();
|
||||
let trashed = d.trashed.iter().filter(|t| **t).count();
|
||||
// files of photos in Recently Deleted are not "already in the library": they are counted apart
|
||||
let dups = d.candidates.iter().filter(|c| c.duplicate.is_some()).count().saturating_sub(trashed);
|
||||
let (dups, in_source, trashed) = dup_counts(d);
|
||||
let sel = d.selected_paths().len();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("{n} found · {sel} selected", n = n, sel = sel)).color(t.text));
|
||||
if dups > 0 {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("· {dups} already in the library", dups = dups)).color(t.text_dim));
|
||||
}
|
||||
if in_source > 0 {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("· {in_source} identical copies", in_source = in_source)).color(t.text_dim))
|
||||
.on_hover_text(crate::i18n::tr("Files with the same content as another file in this import: only the first is imported."));
|
||||
}
|
||||
if trashed > 0 {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("· {trashed} in Recently Deleted", trashed = trashed)).color(t.text_dim));
|
||||
}
|
||||
|
|
@ -1545,6 +1557,19 @@ mod tests {
|
|||
ImportDialog::new(candidates)
|
||||
}
|
||||
|
||||
/// Identical copies inside the source folder are not "already in the library" (a fresh library
|
||||
/// said "8 already in the library").
|
||||
#[test]
|
||||
fn copies_within_the_source_are_not_counted_as_in_the_library() {
|
||||
let mut d = dialog();
|
||||
assert_eq!(dup_counts(&d), (0, 1, 0), "same bytes as an earlier file, no library photo");
|
||||
d.candidates[4].duplicate = Some("content".into());
|
||||
d.candidates[4].existing = Some(7);
|
||||
d.candidates[5].duplicate = Some("path".into());
|
||||
d.candidates[5].existing = Some(8);
|
||||
assert_eq!(dup_counts(&d), (2, 1, 0));
|
||||
}
|
||||
|
||||
/// A toggle per file type: the raws only, without the JPEGs beside them (issue #344).
|
||||
#[test]
|
||||
fn file_types_check_and_uncheck_together() {
|
||||
|
|
|
|||
|
|
@ -394,6 +394,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
};
|
||||
app.ui.grid_info = next;
|
||||
app.ui.show_filenames = true;
|
||||
// the caption is drawn on Square Grid cells only: from Photo Grid, show it there
|
||||
if app.ui.view == ViewMode::PhotoGrid {
|
||||
app.ui.view = ViewMode::SquareGrid;
|
||||
}
|
||||
Ok(json!({"info": app.ui.grid_info}))
|
||||
}
|
||||
"tool.keywordPainter" => {
|
||||
|
|
|
|||
|
|
@ -1247,6 +1247,10 @@ fn grid_info_cycles_caption() {
|
|||
exec(&mut h, "view.squareGrid", json!({}));
|
||||
exec(&mut h, "view.gridInfo", json!({"info": "exposure"}));
|
||||
h.settle(SETTLE);
|
||||
// Photo Grid has no captions: picking one shows it in Square Grid
|
||||
exec(&mut h, "view.photoGrid", json!({}));
|
||||
exec(&mut h, "view.gridInfo", json!({"info": "date"}));
|
||||
assert_eq!(h.app.ui.view, crate::state::ViewMode::SquareGrid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -701,6 +701,8 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
|||
| LRC-LIB-TETHER | Tethered capture | P2 | ⬜ | | |
|
||||
| LRC-LIB-VIEWS | Grid / loupe / compare / survey / people | P1 | 🟡 | `cmd:view.photoGrid`, `cmd:view.squareGrid`, `cmd:view.detail`, `cmd:view.compare`, `cmd:view.survey`, `cmd:view.gridInfo` | grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info); no people view |
|
||||
| LRC-LIB-COMPARE | Compare view | P1 | ✅ | `cmd:view.compare` | ⇧C (C is Crop here); swap, make select; a filmstrip click replaces the active side; zoom always linked; each pane says “Rendering…” or that the photo can't be opened, as the loupe does (see LR-VIEW-COMPARE) |
|
||||
| LRC-LIB-VIEWS | Grid / loupe / compare / survey / people | P1 | 🟡 | `cmd:view.photoGrid`, `cmd:view.squareGrid`, `cmd:view.detail`, `cmd:view.compare`, `cmd:view.survey`, `cmd:view.gridInfo` | grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info; picking one in Photo Grid switches to Square Grid); no people view |
|
||||
| LRC-LIB-COMPARE | Compare view | P1 | ✅ | `cmd:view.compare` | ⇧C (C is Crop here); swap, make select; a filmstrip click replaces the active side; zoom always linked |
|
||||
| LRC-LIB-SURVEY | Survey view | P2 | ✅ | `cmd:view.survey` | N |
|
||||
| LRC-LIB-REFVIEW | Reference view | P2 | ✅ | `cmd:view.reference`, `cmd:photo.setReference`, `crates/ui-egui/src/panels/compare.rs` (`show_reference`) | View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo; each pane says “Rendering…” (widget `compare:rendering:<pane>`) or “This photo can't be opened” (`compare:renderFailed:<pane>`) as the loupe does |
|
||||
| LRC-LIB-CATALOG-PANEL | Catalog sets | P1 | ✅ | `cmd:library.source` (`missing`) | all, recently added, picks, recently deleted, missing photos (when any) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue