Fix: import of metadata-less exports — file date as capture date, in-source copies not "already in the library", Grid Info from Photo Grid

- Files with no capture date in their metadata or sidecar (e.g. exports saved without
  metadata) take the file's modification time, as Lightroom does, instead of all landing
  under "Unknown Date" (the loupe overlay showed the import date for them). The import
  review and its folder/rename templates use the same date.
- The import review counted files with the same bytes as another file in the source as
  "already in the library" (8 on a brand-new, empty library); they are now shown apart as
  "identical copies".
- View > Grid Info captions are drawn on Square Grid only; choosing one from Photo Grid now
  switches to Square Grid instead of seeming to do nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Andrew FrahnandClaude Opus 5.5 committed 2026-10-12 21:32:45 +11:00
1 parent 25ef65c81e
commit 978312b5cc
28 files changed
+140 -24

No files matched your search

+38 -1
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,
@@ -856,11 +858,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).
@@ -985,6 +997,31 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// 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);
}
/// Issue #367: probes read headers, not pixels, and report what the decode-based probe did:
/// oriented dimensions (EXIF orientation 6 swaps them), metadata, the error for a truncated
/// file or one that isn't an image.
+6
View File
@@ -641,6 +641,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) {
@@ -974,6 +976,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)
+4
View File
@@ -1479,6 +1479,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
+3
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 {
+3
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 {
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "nicht zu Apple Fotos hinzugefügt: {e}",
"{n} added to Apple Photos album “{album}”": "{n} zum Apple-Fotos-Album „{album}“ hinzugefügt",
"{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"
"{n} photos opened for editing; each is stacked with its original": "{n} Fotos zur Bearbeitung geöffnet; jedes ist mit seinem Original gestapelt",
"· {in_source} identical copies": "· {in_source} identische Kopien"
}
+2 -1
View File
@@ -2109,5 +2109,6 @@
"Adding to Apple Photos…": "Wird zu Apple Fotos hinzugefügt…",
"not added to Apple Photos (cancelled)": "nicht zu Apple Fotos hinzugefügt (abgebrochen)",
"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"
"Adding to Apple Photos": "Wird zu Apple Fotos hinzugefügt",
"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."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "no se añadieron a Fotos de Apple: {e}",
"{n} added to Apple Photos album “{album}”": "{n} añadidas al álbum “{album}” de Fotos de Apple",
"{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"
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abiertas para editar; cada una está apilada con su original",
"· {in_source} identical copies": "· {in_source} copias idénticas"
}
+2 -1
View File
@@ -1951,5 +1951,6 @@
"Adding to Apple Photos…": "Añadiendo a Fotos de Apple…",
"not added to Apple Photos (cancelled)": "no se añadieron a Fotos de Apple (cancelado)",
"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"
"Adding to Apple Photos": "Añadiendo a Fotos de Apple",
"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."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "non ajouté à Apple Photos : {e}",
"{n} added to Apple Photos album “{album}”": "{n} ajouté(s) à l'album Apple Photos « {album} »",
"{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"
"{n} photos opened for editing; each is stacked with its original": "{n} photos ouvertes pour modification ; chacune est empilée avec son original",
"· {in_source} identical copies": "· {in_source} copies identiques"
}
+2 -1
View File
@@ -1951,5 +1951,6 @@
"Adding to Apple Photos…": "Ajout à Apple Photos…",
"not added to Apple Photos (cancelled)": "non ajouté à Apple Photos (annulé)",
"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"
"Adding to Apple Photos": "Ajout à Apple Photos",
"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é."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "Appleの写真に追加されませんでした:{e}",
"{n} added to Apple Photos album “{album}”": "{n}件をAppleの写真のアルバム「{album}」に追加しました",
"{n} added to Apple Photos": "{n}件をAppleの写真に追加しました",
"{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています"
"{n} photos opened for editing; each is stacked with its original": "{n}枚の写真を編集用に開きました。それぞれ元の写真とスタックされています",
"· {in_source} identical copies": "・同一のコピー {in_source} 件"
}
+2 -1
View File
@@ -1953,5 +1953,6 @@
"Adding to Apple Photos…": "Appleの写真に追加中…",
"not added to Apple Photos (cancelled)": "Appleの写真には追加されませんでした(キャンセル)",
"The first time, macOS asks whether LightCraft may control Photos.": "初回は、LightCraftに写真の操作を許可するかmacOSが確認します。",
"Adding to Apple Photos": "Appleの写真に追加中"
"Adding to Apple Photos": "Appleの写真に追加中",
"Files with the same content as another file in this import: only the first is imported.": "この読み込み内の別のファイルと内容が同じファイルです。最初のファイルだけが読み込まれます。"
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "não adicionadas ao Fotos da Apple: {e}",
"{n} added to Apple Photos album “{album}”": "{n} adicionadas ao álbum “{album}” do Fotos da Apple",
"{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"
"{n} photos opened for editing; each is stacked with its original": "{n} fotos abertas para edição; cada uma está empilhada com a original",
"· {in_source} identical copies": "· {in_source} cópias idênticas"
}
+2 -1
View File
@@ -1951,5 +1951,6 @@
"Adding to Apple Photos…": "Adicionando ao Fotos da Apple…",
"not added to Apple Photos (cancelled)": "não adicionadas ao Fotos da Apple (cancelado)",
"The first time, macOS asks whether LightCraft may control Photos.": "Na primeira vez, o macOS pergunta se o LightCraft pode controlar o Fotos.",
"Adding to Apple Photos": "Adicionando ao Fotos da Apple"
"Adding to Apple Photos": "Adicionando ao Fotos da Apple",
"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."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "не добавлено в Apple Фото: {e}",
"{n} added to Apple Photos album “{album}”": "Добавлено в альбом «{album}» Apple Фото: {n}",
"{n} added to Apple Photos": "Добавлено в Apple Фото: {n}",
"{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом"
"{n} photos opened for editing; each is stacked with its original": "Открыто для редактирования фото: {n}; каждое объединено в стопку с оригиналом",
"· {in_source} identical copies": "· идентичных копий: {in_source}"
}
+2 -1
View File
@@ -2143,5 +2143,6 @@
"Adding to Apple Photos…": "Добавление в Apple Фото…",
"not added to Apple Photos (cancelled)": "не добавлено в Apple Фото (отменено)",
"The first time, macOS asks whether LightCraft may control Photos.": "В первый раз macOS спросит, может ли LightCraft управлять приложением «Фото».",
"Adding to Apple Photos": "Добавление в Apple Фото"
"Adding to Apple Photos": "Добавление в Apple Фото",
"Files with the same content as another file in this import: only the first is imported.": "Файлы с тем же содержимым, что и другой файл этого импорта: импортируется только первый."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "не додано до Apple Фото: {e}",
"{n} added to Apple Photos album “{album}”": "Додано до альбому Apple Фото «{album}»: {n}",
"{n} added to Apple Photos": "Додано до Apple Фото: {n}",
"{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом"
"{n} photos opened for editing; each is stacked with its original": "Відкрито для редагування фото: {n}; кожне об’єднано в стос з оригіналом",
"· {in_source} identical copies": "· ідентичних копій: {in_source}"
}
+2 -1
View File
@@ -2145,5 +2145,6 @@
"Adding to Apple Photos…": "Додавання до Apple Фото…",
"not added to Apple Photos (cancelled)": "не додано до Apple Фото (скасовано)",
"The first time, macOS asks whether LightCraft may control Photos.": "Першого разу macOS запитає, чи може LightCraft керувати програмою «Фото».",
"Adding to Apple Photos": "Додавання до Apple Фото"
"Adding to Apple Photos": "Додавання до Apple Фото",
"Files with the same content as another file in this import: only the first is imported.": "Файли з тим самим вмістом, що й інший файл цього імпорту: імпортується лише перший."
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "未添加到 Apple 照片:{e}",
"{n} added to Apple Photos album “{album}”": "已将 {n} 个添加到 Apple 照片相册“{album}”",
"{n} added to Apple Photos": "已将 {n} 个添加到 Apple 照片",
"{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起"
"{n} photos opened for editing; each is stacked with its original": "已打开 {n} 张照片进行编辑;每张都与其原片堆叠在一起",
"· {in_source} identical copies": "· {in_source} 个相同副本"
}
+2 -1
View File
@@ -1989,5 +1989,6 @@
"Adding to Apple Photos…": "正在添加到 Apple 照片…",
"not added to Apple Photos (cancelled)": "未添加到 Apple 照片(已取消)",
"The first time, macOS asks whether LightCraft may control Photos.": "首次使用时,macOS 会询问是否允许 LightCraft 控制“照片”。",
"Adding to Apple Photos": "正在添加到 Apple 照片"
"Adding to Apple Photos": "正在添加到 Apple 照片",
"Files with the same content as another file in this import: only the first is imported.": "与本次导入中另一个文件内容相同的文件:只导入第一个。"
}
+2 -1
View File
@@ -244,5 +244,6 @@
"not added to Apple Photos: {e}": "未加入 Apple 照片:{e}",
"{n} added to Apple Photos album “{album}”": "已將 {n} 個加入 Apple 照片相簿「{album}」",
"{n} added to Apple Photos": "已將 {n} 個加入 Apple 照片",
"{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起"
"{n} photos opened for editing; each is stacked with its original": "已開啟 {n} 張相片進行編輯;每張都與其原始相片堆疊在一起",
"· {in_source} identical copies": "· {in_source} 個相同副本"
}
+2 -1
View File
@@ -1951,5 +1951,6 @@
"Adding to Apple Photos…": "正在加入 Apple 照片…",
"not added to Apple Photos (cancelled)": "未加入 Apple 照片(已取消)",
"The first time, macOS asks whether LightCraft may control Photos.": "第一次使用時,macOS 會詢問是否允許 LightCraft 控制「照片」。",
"Adding to Apple Photos": "正在加入 Apple 照片"
"Adding to Apple Photos": "正在加入 Apple 照片",
"Files with the same content as another file in this import: only the first is imported.": "與本次匯入中另一個檔案內容相同的檔案:只匯入第一個。"
}
+13 -1
View File
@@ -3282,10 +3282,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;
@@ -3335,6 +3341,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() };
+28 -3
View File
@@ -911,19 +911,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));
}
@@ -1524,6 +1536,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() {
+4
View File
@@ -379,6 +379,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" => {
+4
View File
@@ -1122,6 +1122,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]
+1 -1
View File
@@ -684,7 +684,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
| LRC-LIB-IMPORT | Full import dialog | P1 | ✅ | `cmd:library.import` (`mode`, `destination`, `organize`, `rename`, `metadataPreset`, `preset`, `keywords`, `album`), `crates/ui-egui/src/import.rs` | review grid with duplicates skipped; add in place or copy (library Originals or any folder; by day / by month / one folder / custom folder template such as `{date:%Y}/{date:%Y%m%d}` → 2026/20260114, kept inside the destination; example destination shown; rename template with live example, Tags picker (insert at cursor) and unknown-tag warning, numbered across the import); develop preset, metadata preset, keywords, album on import |
| LRC-LIB-AUTOIMPORT | Watched-folder import | P2 | ✅ | `cmd:library.autoImport` | see LR-IMP-AUTO |
| 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-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 |