diff --git a/.coderabbit.yaml b/.coderabbit.yaml
new file mode 100644
index 000000000..2408084cf
--- /dev/null
+++ b/.coderabbit.yaml
@@ -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]"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..2c4b8cb1a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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)"
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index d992f7433..01cb9cd66 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index 5d7f5111a..a49ec38db 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index cf21739d8..c76095d2d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/README.md b/README.md
index 38ff85898..42b164d6c 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,9 @@
+
+
+
diff --git a/crates/engine/src/tests_export.rs b/crates/engine/src/tests_export.rs
index 2f70ae208..0ac8df804 100644
--- a/crates/engine/src/tests_export.rs
+++ b/crates/engine/src/tests_export.rs
@@ -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 = 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 = 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();
diff --git a/crates/ui-egui/locales/cs.json b/crates/ui-egui/locales/cs.json
index 1e84313ae..e1b8e7f62 100644
--- a/crates/ui-egui/locales/cs.json
+++ b/crates/ui-egui/locales/cs.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json
index 000aea2d4..31ad71666 100644
--- a/crates/ui-egui/locales/de.json
+++ b/crates/ui-egui/locales/de.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json
index 77ba4c08e..91ff5ee25 100644
--- a/crates/ui-egui/locales/es.json
+++ b/crates/ui-egui/locales/es.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json
index fa57e373e..bcfe1956f 100644
--- a/crates/ui-egui/locales/fr.json
+++ b/crates/ui-egui/locales/fr.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json
index 382fb80c9..c82236aae 100644
--- a/crates/ui-egui/locales/it.json
+++ b/crates/ui-egui/locales/it.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json
index 2dc80934f..7a4e06d08 100644
--- a/crates/ui-egui/locales/ja.json
+++ b/crates/ui-egui/locales/ja.json
@@ -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": "読み込みサムネイルのサイズ"
}
diff --git a/crates/ui-egui/locales/ka.json b/crates/ui-egui/locales/ka.json
index f6530929d..bf0cab098 100644
--- a/crates/ui-egui/locales/ka.json
+++ b/crates/ui-egui/locales/ka.json
@@ -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": "დააწკაპუნეთ მნიშვნელობის შესაყვანად, გადაათრიეთ მის შესაცვლელად"
}
diff --git a/crates/ui-egui/locales/pl.json b/crates/ui-egui/locales/pl.json
index 7416ce241..0c4dbfa8f 100644
--- a/crates/ui-egui/locales/pl.json
+++ b/crates/ui-egui/locales/pl.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json
index 85a0ae039..2c7c994ec 100644
--- a/crates/ui-egui/locales/pt-br.json
+++ b/crates/ui-egui/locales/pt-br.json
@@ -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"
}
diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json
index 5ead5101f..a1a675a63 100644
--- a/crates/ui-egui/locales/ru.json
+++ b/crates/ui-egui/locales/ru.json
@@ -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": "Размер миниатюр при импорте"
}
diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json
index fbdef1c5f..88dfbd225 100644
--- a/crates/ui-egui/locales/uk.json
+++ b/crates/ui-egui/locales/uk.json
@@ -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": "Розмір мініатюр імпорту"
}
diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json
index 2750dd4c8..7a8a1da7a 100644
--- a/crates/ui-egui/locales/zh-hans.json
+++ b/crates/ui-egui/locales/zh-hans.json
@@ -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": "导入缩略图大小"
}
diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json
index a1723fc22..4880ca22b 100644
--- a/crates/ui-egui/locales/zh-hant.json
+++ b/crates/ui-egui/locales/zh-hant.json
@@ -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": "匯入縮圖大小"
}
diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs
index 7c202c1a7..5de6890bf 100644
--- a/crates/ui-egui/src/import.rs
+++ b/crates/ui-egui/src/import.rs
@@ -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();
diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs
index 0b6b28ccc..70c915981 100644
--- a/crates/ui-egui/src/state.rs
+++ b/crates/ui-egui/src/state.rs
@@ -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,
diff --git a/crates/ui-egui/src/tests_folder_drop.rs b/crates/ui-egui/src/tests_folder_drop.rs
index c498e5922..9b4cb860a 100644
--- a/crates/ui-egui/src/tests_folder_drop.rs
+++ b/crates/ui-egui/src/tests_folder_drop.rs
@@ -50,13 +50,8 @@ fn widget(h: &Headless, id: &str) -> Option {
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 {
- 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);
diff --git a/crates/ui-egui/src/tests_import_thumbnails.rs b/crates/ui-egui/src/tests_import_thumbnails.rs
index 1e0b669d8..6817b72ec 100644
--- a/crates/ui-egui/src/tests_import_thumbnails.rs
+++ b/crates/ui-egui/src/tests_import_thumbnails.rs
@@ -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");
+ }
+ }
+ }
+}
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
index f67bd4fcd..a9288904b 100644
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -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)
diff --git a/docs/gaps.md b/docs/gaps.md
index 2de817113..79ff45e9e 100644
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -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 |
diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md
index b8d8438ef..085c3cba6 100644
--- a/docs/parity-checklist.md
+++ b/docs/parity-checklist.md
@@ -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) |