LR-IMP-ADD-DIALOG: keep import thumbnail review changes focused (#2237)

Update from main and remove Windows fixtures shared with #2004. Supply both
missing labels in all 13 catalogs and use the shared ControlSpec slider for
keyboard steps, exact entry and reset. Add localization and idle-keyboard
regressions, document control IDs, and retain the partial #2056 scope.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
tylerphotos
2026-10-21 16:30:45 -05:00
co-authored by Codex
27 changed files with 210 additions and 84 deletions
+6
View File
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
auto_review:
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
ignore_usernames:
- "dependabot[bot]"
+30
View File
@@ -0,0 +1,30 @@
version: 2
updates:
# Maintain Rust / Cargo dependencies
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
time: "06:00"
timezone: "America/New_York"
# Consolidate updates into a single PR rather than one PR per crate
groups:
cargo-dependencies:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"
# Maintain GitHub Actions workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
time: "06:00"
timezone: "America/New_York"
groups:
actions:
patterns:
- "*"
commit-message:
prefix: "ci(deps)"
+3 -8
View File
@@ -18,11 +18,6 @@ concurrency:
cancel-in-progress: true
jobs:
eval:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: nix flake check --no-build
run: nix flake check --no-build --all-systems
nix:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
Generated
+33 -33
View File
@@ -3078,7 +3078,7 @@ dependencies = [
[[package]]
name = "lightcraft"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"eframe",
"egui",
@@ -3103,7 +3103,7 @@ dependencies = [
[[package]]
name = "lightcraft-catalog"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"crc32fast",
"lightcraft-develop",
@@ -3120,7 +3120,7 @@ dependencies = [
[[package]]
name = "lightcraft-cli"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"dhat",
"lightcraft-codecs",
@@ -3136,7 +3136,7 @@ dependencies = [
[[package]]
name = "lightcraft-codecs"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"avif-serialize",
"bytemuck",
@@ -3167,7 +3167,7 @@ dependencies = [
[[package]]
name = "lightcraft-color"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-geom",
"proptest",
@@ -3176,7 +3176,7 @@ dependencies = [
[[package]]
name = "lightcraft-denoise"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"faer-core",
"lightcraft-denoise-core",
@@ -3193,7 +3193,7 @@ dependencies = [
[[package]]
name = "lightcraft-denoise-core"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-raster",
"miniz_oxide 0.8.9",
@@ -3208,7 +3208,7 @@ dependencies = [
[[package]]
name = "lightcraft-develop"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-color",
"lightcraft-geom",
@@ -3220,7 +3220,7 @@ dependencies = [
[[package]]
name = "lightcraft-engine"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"ab_glyph",
"craft-docpath",
@@ -3263,7 +3263,7 @@ dependencies = [
[[package]]
name = "lightcraft-faces"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-testkit",
"matrixmultiply",
@@ -3278,7 +3278,7 @@ dependencies = [
[[package]]
name = "lightcraft-fetch"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"log",
"rustls",
@@ -3289,7 +3289,7 @@ dependencies = [
[[package]]
name = "lightcraft-geocode"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"miniz_oxide 0.8.9",
"serde",
@@ -3297,7 +3297,7 @@ dependencies = [
[[package]]
name = "lightcraft-geom"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"proptest",
"serde",
@@ -3305,7 +3305,7 @@ dependencies = [
[[package]]
name = "lightcraft-gpu"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"bytemuck",
"lightcraft-color",
@@ -3325,7 +3325,7 @@ dependencies = [
[[package]]
name = "lightcraft-heif"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"heic-rs",
"rayon",
@@ -3333,14 +3333,14 @@ dependencies = [
[[package]]
name = "lightcraft-inpaint"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"rayon",
]
[[package]]
name = "lightcraft-lensdb"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-develop",
"lightcraft-geom",
@@ -3349,7 +3349,7 @@ dependencies = [
[[package]]
name = "lightcraft-mcp"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-codecs",
"lightcraft-engine",
@@ -3360,7 +3360,7 @@ dependencies = [
[[package]]
name = "lightcraft-merge"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-codecs",
"lightcraft-color",
@@ -3376,7 +3376,7 @@ dependencies = [
[[package]]
name = "lightcraft-meta"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-geom",
"lightcraft-tiff",
@@ -3388,7 +3388,7 @@ dependencies = [
[[package]]
name = "lightcraft-pipeline"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"image",
"lightcraft-color",
@@ -3406,7 +3406,7 @@ dependencies = [
[[package]]
name = "lightcraft-preview"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"jpeg-encoder",
"lightcraft-raster",
@@ -3418,7 +3418,7 @@ dependencies = [
[[package]]
name = "lightcraft-raster"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"libc",
"lightcraft-color",
@@ -3430,7 +3430,7 @@ dependencies = [
[[package]]
name = "lightcraft-raw"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"dhat",
"jpeg-encoder",
@@ -3454,7 +3454,7 @@ dependencies = [
[[package]]
name = "lightcraft-scenes"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"image",
"lightcraft-color",
@@ -3465,7 +3465,7 @@ dependencies = [
[[package]]
name = "lightcraft-segment"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"candle-core",
"candle-nn",
@@ -3479,7 +3479,7 @@ dependencies = [
[[package]]
name = "lightcraft-slideshow"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"lightcraft-raster",
"serde",
@@ -3488,15 +3488,15 @@ dependencies = [
[[package]]
name = "lightcraft-sysmem"
version = "0.9.1"
version = "0.10.0"
[[package]]
name = "lightcraft-testkit"
version = "0.9.1"
version = "0.10.0"
[[package]]
name = "lightcraft-tiff"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"proptest",
"serde",
@@ -3505,7 +3505,7 @@ dependencies = [
[[package]]
name = "lightcraft-ui-egui"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"craft-ui",
"egui",
@@ -3534,7 +3534,7 @@ dependencies = [
[[package]]
name = "lightcraft-web"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"crc32fast",
"eframe",
@@ -7004,7 +7004,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"brotli",
"flate2",
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/*", "apps/*", "xtask"]
[workspace.package]
version = "0.9.1"
version = "0.10.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
+3
View File
@@ -28,6 +28,9 @@
<img alt="MCP server included" src="https://img.shields.io/badge/MCP-ready-8a5800?style=flat-square">
<img alt="License: MIT OR Apache-2.0" src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-8a5800?style=flat-square">
<a href="ROADMAP.md"><img alt="Status: young and moving fast" src="https://img.shields.io/badge/status-young%20%26%20moving%20fast-f2a516?style=flat-square"></a>
<br>
<a href="https://sonarcloud.io/project/overview?id=storytold_lightcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/lightcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<p align="center">
+3 -7
View File
@@ -991,19 +991,15 @@ fn same_folder_as_original_exports_next_to_each_photo() {
let exists = |p: &str| std::path::Path::new(p).exists();
let files = crate::export::export_batch(&mut s, &ids, &o, &to, &mut crate::export::write_file, &exists).unwrap();
let written: Vec<&str> = files.iter().map(|f| f["path"].as_str().unwrap()).collect();
// Compare path components: Windows accepts both separators in returned paths.
let mut want = vec![a.join("Exports").join("IMG_1.jpg"), b.join("Exports").join("IMG_1.jpg")];
let mut got: Vec<std::path::PathBuf> = written.iter().map(std::path::PathBuf::from).collect();
let mut want = vec![a.join("Exports/IMG_1.jpg").to_string_lossy().to_string(), b.join("Exports/IMG_1.jpg").to_string_lossy().to_string()];
let mut got: Vec<String> = written.iter().map(|p| p.to_string()).collect();
want.sort();
got.sort();
assert_eq!(got, want);
assert!(!dir.join("unused").exists(), "nothing written to the specific folder");
// again: each folder's own collision gets the next free name
let files = crate::export::export_batch(&mut s, &ids, &o, &to, &mut crate::export::write_file, &exists).unwrap();
assert!(
files.iter().all(|f| std::path::Path::new(f["path"].as_str().unwrap()).ends_with(std::path::Path::new("Exports").join("IMG_1-2.jpg"))),
"{files:?}"
);
assert!(files.iter().all(|f| f["path"].as_str().unwrap().ends_with("Exports/IMG_1-2.jpg")), "{files:?}");
// no subfolder: next to the original, which is never written over
let beside = ExportOptions { subfolder: String::new(), conflict: Conflict::Overwrite, ..o.clone() };
let err = crate::export::export_batch(&mut s, &ids, &beside, &to, &mut crate::export::write_file, &exists).unwrap_err();
+3 -1
View File
@@ -2063,5 +2063,7 @@
"Open Settings": "Otevřít nastavení",
"Select none": "Zrušit celý výběr",
"Select all": "Vybrat vše",
"Visualize HDR": "Zobrazit rozsah HDR"
"Visualize HDR": "Zobrazit rozsah HDR",
"Thumbnail size": "Velikost miniatur",
"Import Thumbnail Size": "Velikost miniatur při importu"
}
+3 -1
View File
@@ -2341,5 +2341,7 @@
"Checking the GPU…": "GPU wird geprüft …",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Ein Gesichtsmodell nutzt die GPU nur, wenn eine Prüfung zeigt, dass sie dieselben Ergebnisse schneller liefert.",
"Checking the GPU for faces": "GPU für Gesichter wird geprüft"
"Checking the GPU for faces": "GPU für Gesichter wird geprüft",
"Thumbnail size": "Vorschaubildgröße",
"Import Thumbnail Size": "Vorschaubildgröße beim Import"
}
+3 -1
View File
@@ -2340,5 +2340,7 @@
"Checking the GPU…": "Comprobando la GPU…",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Un modelo de caras usa la GPU solo cuando una comprobación muestra que da los mismos resultados más rápido.",
"Checking the GPU for faces": "Comprobando la GPU para las caras"
"Checking the GPU for faces": "Comprobando la GPU para las caras",
"Thumbnail size": "Tamaño de las miniaturas",
"Import Thumbnail Size": "Tamaño de las miniaturas de importación"
}
+3 -1
View File
@@ -2338,5 +2338,7 @@
"Checking the GPU…": "Vérification du GPU…",
"CPU": "Processeur",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Un modèle de visages n’utilise le GPU que si une vérification montre qu’il donne les mêmes résultats plus vite.",
"Checking the GPU for faces": "Vérification du GPU pour les visages"
"Checking the GPU for faces": "Vérification du GPU pour les visages",
"Thumbnail size": "Taille des vignettes",
"Import Thumbnail Size": "Taille des vignettes d’importation"
}
+4 -1
View File
@@ -1990,5 +1990,8 @@
"Test Face Model": "Testa il modello dei volti",
"This build cannot run recognition models: they can be added and chosen, not used.": "Questa versione non può eseguire modelli di riconoscimento: si possono aggiungere e scegliere, non usare.",
"This build has nowhere to keep face models, so the model list is not available. (The desktop app has.)": "Questa versione non ha dove tenere i modelli dei volti, quindi l'elenco dei modelli non è disponibile. (L'app desktop ce l'ha.)",
"Unnamed Faces": "Volti senza nome"
"Unnamed Faces": "Volti senza nome",
"Thumbnail size": "Dimensione delle miniature",
"Import Thumbnail Size": "Dimensione delle miniature di importazione",
"Click to type a value, drag to change it": "Fai clic per digitare un valore, trascina per modificarlo"
}
+3 -1
View File
@@ -2340,5 +2340,7 @@
"Checking the GPU…": "GPU を確認中…",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "顔モデルが GPU を使うのは、同じ結果をより速く出せると確認できた場合だけです。",
"Checking the GPU for faces": "顔用の GPU を確認中"
"Checking the GPU for faces": "顔用の GPU を確認中",
"Thumbnail size": "サムネイルのサイズ",
"Import Thumbnail Size": "読み込みサムネイルのサイズ"
}
+4 -1
View File
@@ -2718,5 +2718,8 @@
"≥ at least": "≥ არანაკლებ",
"⌘-drag to straighten, ⇧G Guided Upright, a grid while transforming.": "⌘-გადათრევა გასასწორებლად, ⇧G მართვადი Upright, ბადე გარდაქმნისას.",
"日本語": "日本語",
"繁體中文(台灣)": "繁體中文(台灣)"
"繁體中文(台灣)": "繁體中文(台灣)",
"Thumbnail size": "მინიატურების ზომა",
"Import Thumbnail Size": "იმპორტის მინიატურების ზომა",
"Click to type a value, drag to change it": "დააწკაპუნეთ მნიშვნელობის შესაყვანად, გადაათრიეთ მის შესაცვლელად"
}
+3 -1
View File
@@ -2157,5 +2157,7 @@
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "To jest katalog Lightrooma, a nie biblioteka LightCraft. Zaimportować go zamiast tego do biblioteki LightCraft?",
"Unnamed Faces": "Nienazwane twarze",
"Vertical inset (% of short edge)": "Margines pionowy (% krótszego boku)",
"not added to Apple Photos (cancelled)": "nie dodano do aplikacji Zdjęcia (anulowano)"
"not added to Apple Photos (cancelled)": "nie dodano do aplikacji Zdjęcia (anulowano)",
"Thumbnail size": "Rozmiar miniatur",
"Import Thumbnail Size": "Rozmiar miniatur importu"
}
+9 -1
View File
@@ -2180,5 +2180,13 @@
"Checking the GPU…": "Verificando a GPU…",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Um modelo de rostos só usa a GPU quando uma verificação mostra que ela dá os mesmos resultados mais rápido.",
"Checking the GPU for faces": "Verificando a GPU para rostos"
"Checking the GPU for faces": "Verificando a GPU para rostos",
"Include selected photos": "Incluir fotos selecionadas",
"Color Overlay": "Sobreposição de cor",
"Color Overlay on B&W": "Sobreposição de cor em P&B",
"Image on Black": "Imagem sobre preto",
"Image on White": "Imagem sobre branco",
"White on Black": "Branco sobre preto",
"Thumbnail size": "Tamanho das miniaturas",
"Import Thumbnail Size": "Tamanho das miniaturas de importação"
}
+3 -1
View File
@@ -2333,5 +2333,7 @@
"Checking the GPU…": "Проверка ГП…",
"CPU": "ЦП",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Модель лиц использует ГП, только если проверка показала, что он даёт те же результаты быстрее.",
"Checking the GPU for faces": "Проверка ГП для лиц"
"Checking the GPU for faces": "Проверка ГП для лиц",
"Thumbnail size": "Размер миниатюр",
"Import Thumbnail Size": "Размер миниатюр при импорте"
}
+3 -1
View File
@@ -2338,5 +2338,7 @@
"Checking the GPU…": "Перевірка ГП…",
"CPU": "ЦП",
"A face model uses the GPU only when a check shows it gives the same results faster.": "Модель облич використовує ГП, лише якщо перевірка показала, що він дає ті самі результати швидше.",
"Checking the GPU for faces": "Перевірка ГП для облич"
"Checking the GPU for faces": "Перевірка ГП для облич",
"Thumbnail size": "Розмір мініатюр",
"Import Thumbnail Size": "Розмір мініатюр імпорту"
}
+3 -1
View File
@@ -2179,5 +2179,7 @@
"Checking the GPU…": "正在检查 GPU…",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "只有检查表明 GPU 能更快给出相同结果时,人脸模型才会使用 GPU。",
"Checking the GPU for faces": "正在检查用于人脸的 GPU"
"Checking the GPU for faces": "正在检查用于人脸的 GPU",
"Thumbnail size": "缩略图大小",
"Import Thumbnail Size": "导入缩略图大小"
}
+3 -1
View File
@@ -2141,5 +2141,7 @@
"Checking the GPU…": "正在檢查 GPU…",
"CPU": "CPU",
"A face model uses the GPU only when a check shows it gives the same results faster.": "只有在檢查顯示 GPU 能更快得出相同結果時,臉孔模型才會使用 GPU。",
"Checking the GPU for faces": "正在檢查用於臉孔的 GPU"
"Checking the GPU for faces": "正在檢查用於臉孔的 GPU",
"Thumbnail size": "縮圖大小",
"Import Thumbnail Size": "匯入縮圖大小"
}
+21 -10
View File
@@ -52,6 +52,26 @@ fn thumb_edge(size: f32, pixels_per_point: f32) -> usize {
[192, 384, 512, 768, 1024].into_iter().find(|edge| *edge as f32 >= pixels).unwrap_or(1024)
}
/// Use the shared control contract for pointer, keyboard, exact-value entry and reset interactions.
fn thumb_size_slider(app: &mut LightcraftApp, ui: &mut egui::Ui) {
use lightcraft_develop::controls::{ControlSpec, Section, Track};
const SIZE: ControlSpec = ControlSpec {
id: "importThumbSize",
label: "Thumbnail size",
section: Section::Light,
min: THUMB_SIZE_MIN as f64,
max: THUMB_SIZE_MAX as f64,
default: THUMB_SIZE_DEFAULT as f64,
step: 1.0,
decimals: 0,
track: Track::Plain,
};
let out = crate::widgets::slider(ui, &SIZE, f64::from(thumb_size(app.ui.import_thumb_size)), true, None);
if let Some(size) = out.value {
let _ = app.run("view.importThumbSize", json!({"size": SIZE.clamp(size)}));
}
}
/// Height of the photo grid: what is `available` once `below` (options and buttons) is left free,
/// in whole points so the window never grows by a fraction each frame, and at least one `row`.
pub(crate) fn grid_height(available: f32, below: f32, row: f32) -> f32 {
@@ -1043,16 +1063,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
}
});
}
ui.horizontal(|ui| {
let mut size = thumb_size(app.ui.import_thumb_size);
let response =
ui.add(egui::Slider::new(&mut size, THUMB_SIZE_MIN..=THUMB_SIZE_MAX).text(crate::i18n::tr("Thumbnail size")).show_value(false));
// Slider's response includes its label; automation addresses the track itself.
register(ui.ctx(), "slider:importThumbSize", Rect::from_min_size(response.rect.min, vec2(ui.spacing().slider_width, response.rect.height())));
if response.changed() {
let _ = app.run("view.importThumbSize", json!({"size": size}));
}
});
thumb_size_slider(app, ui);
// candidate grid
let cell = thumb_size(app.ui.import_thumb_size).min(ui.available_width().max(1.0));
let avail = ui.available_width();
+1
View File
@@ -1263,6 +1263,7 @@ pub struct SyncCounts {
}
impl Default for UiState {
/// Initialize the library and import review sizes as independent app preferences.
fn default() -> Self {
UiState {
window: None,
+5 -10
View File
@@ -50,13 +50,8 @@ fn widget(h: &Headless, id: &str) -> Option<Rect> {
h.app.widgets.iter().find(|(w, _)| w == id).map(|(_, r)| *r)
}
/// Match FolderNode widget ids, whose paths use forward slashes on every platform.
fn folder_id(prefix: &str, dir: &Path) -> String {
format!("{prefix}:{}", dir.to_string_lossy().replace('\\', "/"))
}
fn folder_row(h: &Headless, dir: &Path) -> Option<Rect> {
widget(h, &folder_id("source:libfolder", dir))
widget(h, &format!("source:libfolder:{}", dir.to_string_lossy()))
}
fn id_of(h: &Headless, file: &str) -> u64 {
@@ -185,7 +180,7 @@ fn photos_already_in_the_folder_stay_put() {
let (root, trip, home) = folders("here");
let mut h = app(&[(trip.join("beach.png"), false), (home.join("garden.png"), false)]);
// open the folder holding both
let toggle = widget(&h, &folder_id("libraryFolderToggle", &root)).unwrap();
let toggle = widget(&h, &format!("libraryFolderToggle:{}", root.to_string_lossy())).unwrap();
h.app.synthetic.push(egui::Event::PointerMoved(toggle.center()));
h.app.synthetic.push(egui::Event::PointerButton {
pos: toggle.center(),
@@ -289,7 +284,7 @@ fn cancelling_the_question_moves_nothing() {
let target = if folder_row(&h, &home).is_some() {
target
} else {
click(&mut h, &folder_id("libraryFolderToggle", &root));
click(&mut h, &format!("libraryFolderToggle:{}", root.to_string_lossy()));
folder_row(&h, &home).unwrap()
};
let undo = h.app.session.undo.len();
@@ -307,7 +302,7 @@ fn dont_ask_again_moves_the_next_drop_at_once() {
let (root, trip, home) = folders("dont-ask");
std::fs::copy(trip.join("beach.png"), trip.join("dunes.png")).unwrap();
let mut h = app(&[(trip.join("beach.png"), false), (trip.join("dunes.png"), false), (home.join("garden.png"), false)]);
click(&mut h, &folder_id("libraryFolderToggle", &root));
click(&mut h, &format!("libraryFolderToggle:{}", root.to_string_lossy()));
let target = folder_row(&h, &home).unwrap();
let beach = id_of(&h, "beach.png");
h.app.session.selection = lightcraft_engine::Selection::single(lightcraft_catalog::PhotoId(beach));
@@ -332,7 +327,7 @@ fn dont_ask_again_moves_the_next_drop_at_once() {
fn a_question_left_open_while_another_library_opens_moves_nothing() {
let (root, trip, home) = folders("other-library");
let mut h = app(&[(trip.join("beach.png"), false), (home.join("garden.png"), false)]);
click(&mut h, &folder_id("libraryFolderToggle", &root));
click(&mut h, &format!("libraryFolderToggle:{}", root.to_string_lossy()));
let beach = id_of(&h, "beach.png");
let target = folder_row(&h, &home).unwrap();
drop_beach(&mut h, beach, target);
@@ -157,3 +157,52 @@ fn import_thumbnail_size_rejects_bad_commands_and_restores_old_preferences() {
assert_eq!(state.import_thumb_size, 116.0);
}
}
/// A selected import slider keeps keyboard nudges after idle frames without changing candidate checks.
#[test]
fn import_thumbnail_slider_keeps_the_shared_keyboard_contract() {
let mut review = Review::new(1.0);
let checked = review.checks();
review.request("ui.clickWidget", json!({"id": "slider:importThumbSize", "fx": 0.4}));
assert!(review.h.settle(T));
for _ in 0..6 {
review.h.step();
}
let before = review.h.app.ui.import_thumb_size;
review.request("ui.key", json!({"key": "ArrowUp"}));
assert!(review.h.settle(T));
assert_eq!(review.h.app.ui.import_thumb_size, before + 1.0);
assert_eq!(review.checks(), checked);
}
/// Every shipped language translates the slider and command labels, with the real control visible at high density.
#[test]
fn import_thumbnail_labels_are_localized_in_every_shipped_language() {
use crate::i18n::Locale;
let command =
crate::menus::UI_COMMANDS.iter().find(|command| command.0 == "view.importThumbSize").expect("the import-size command is registered");
assert_eq!(command.1, "Import Thumbnail Size", "the catalog key matches the command's actual label");
for theme in ["charcoal", "paper"] {
for scale in [1.0, 2.25] {
let mut app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() });
app.ui.dialog = Some(Dialog::Import { opts: Box::new(ImportDialog::new(Vec::new())) });
let mut h = Headless::new(app, [960.0, 720.0], scale);
assert!(h.app.run("view.theme", json!({"theme": theme})).is_ok());
for locale in Locale::ALL {
for source in ["Thumbnail size", "Import Thumbnail Size", "Click to type a value, drag to change it"] {
assert!(!locale.tr(source).is_empty());
if *locale != Locale::En {
assert_ne!(locale.tr(source), source, "{locale:?}: {source}");
}
}
let result = h.request("ui.set", json!({"language": locale.code()}), T);
assert_eq!(result["ok"], true, "{result}");
assert!(h.settle(T));
assert!(h.painted_text().iter().any(|text| text == locale.tr("Thumbnail size")), "{locale:?}: translated slider label");
let track = h.app.widgets.iter().find(|(id, _)| id == "slider:importThumbSize").expect("the import slider is visible").1;
let viewport = egui::Rect::from_min_size(egui::Pos2::ZERO, h.size);
assert!(viewport.contains_rect(track), "{locale:?} {theme} {scale}: slider stays inside the viewport");
}
}
}
}
+4
View File
@@ -244,6 +244,10 @@ The import review's **Thumbnail size** slider is `slider:importThumbSize` in `ui
(80–320, clamped); omitting `size` reads it. Both return `{size}`. Non-numeric or non-finite
values are refused. The preference is `importThumbSize` in the UI state, independent of the
library grid's `thumbSize`; resizing keeps the import checkboxes and duplicate markers.
The shared slider also exposes `sliderValue:importThumbSize` for exact entry and
`sliderLabel:importThumbSize` for double-click reset to 116 pt. Up/Down nudges the selected
slider by 1 pt; Return commits typed values and Escape cancels them. Slider and command
labels, including edit/reset feedback, are localized in every shipped catalog.
## Headless rendering (no window, no GPU)
+2 -1
View File
@@ -1,6 +1,6 @@
# Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** minor (import grid thumbnail size adjustable; gap 30 tracks the remaining pre-import loupe, #2056; upstream Print Page Options and camera updates retained)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-21 · **Change:** patch (import thumbnail localization and shared slider interactions; gap/status counts unchanged)
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
@@ -81,6 +81,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | patch | LR-IMP-ADD-DIALOG: translate the import thumbnail labels in all 13 catalogs; use the shared ControlSpec slider for keyboard, exact entry and reset; retain the remaining pre-import loupe gap and unchanged counts |
| 2026-10-17 | minor | Import thumbnail-size slider (part of #2056); gap 30 records the remaining pre-import loupe and keyboard culling |
| 2026-10-18 | patch | Gap 1: six X-Trans CMOS 4 Fujifilm bodies (X-T30 …) take the X-T3's spectral matrices instead of the neutral matrix |
| 2026-10-15 | minor | Gap 15 shrinks: the Page panel's Page Options (Page Numbers, Page Info, Crop Marks) print on Single Image and Contact Sheet pages; identity plate, watermark, photo info, Cut Guides, direct printing and JPEG output remain |
+2 -1
View File
@@ -1,6 +1,6 @@
# Lightroom parity checklist
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (import grid thumbnail-size slider, #2056; upstream Print Page Options and raw/export updates retained; status and summary counts unchanged) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** patch (import thumbnail localization and shared slider interactions; gap/status counts unchanged) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
@@ -797,6 +797,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | patch | LR-IMP-ADD-DIALOG: translate the import thumbnail labels in all 13 catalogs; use the shared ControlSpec slider for keyboard, exact entry and reset; retain the remaining pre-import loupe gap and unchanged counts |
| 2026-10-17 | minor | LR-IMP-ADD-DIALOG: import grid thumbnail-size slider, density-aware previews, bounded visible textures and preserved culling; remaining loupe tracked in gap 30 (#2056); status and summary counts unchanged |
| 2026-10-14 | patch | LR-EXP-TYPE: opaque 8-bit PNG/TIFF/WebP use RGB; transparent exports retain alpha (#1670). Statuses and feature counts unchanged. |
| 2026-10-15 | minor | LRC-PRINT-PAGE: Page Options (Page Numbers, Page Info, Crop Marks) on Single Image and Contact Sheet pages, in the Print dialog, `print.set` and templates; still 🟡 (no identity plate, watermark, photo info or Cut Guides) |