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 @@ MCP server included License: MIT OR Apache-2.0 Status: young and moving fast +
+ Scanned by SonarCloud + Reviewed by CodeRabbit

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