# 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:
bflatastic 2026-10-13 11:06:28 +07:00
commit 84d6849cf3
28 changed files with 138 additions and 23 deletions

View File

@ -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);
}

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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"
}

View File

@ -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."
}

View File

@ -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"
}

View File

@ -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."
}

View File

@ -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"
}

View File

@ -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é."
}

View File

@ -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} 件"
}

View File

@ -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.": "この読み込み内の別のファイルと内容が同じファイルです。最初のファイルだけが読み込まれます。"
}

View File

@ -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"
}

View File

@ -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."
}

View File

@ -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}"
}

View File

@ -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.": "Файлы с тем же содержимым, что и другой файл этого импорта: импортируется только первый."
}

View File

@ -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}"
}

View File

@ -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.": "Файли з тим самим вмістом, що й інший файл цього імпорту: імпортується лише перший."
}

View File

@ -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} 个相同副本"
}

View File

@ -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.": "与本次导入中另一个文件内容相同的文件:只导入第一个。"
}

View File

@ -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} 個相同副本"
}

View File

@ -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.": "與本次匯入中另一個檔案內容相同的檔案:只匯入第一個。"
}

View File

@ -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() };

View File

@ -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() {

View File

@ -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" => {

View File

@ -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]

View File

@ -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) |