From 142ddcff29838603fa153bc1497457f10becfb75 Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:05:18 +0200 Subject: [PATCH 1/4] LRC-LIB-MISSING: the Missing Photos count is pending sidebar work The sidebar's Missing Photos row appears when the background count of missing files lands, and moves every row below it down. That worker was not counted in fs_cached_running, so Headless::settle() did not wait for it: on a busy machine the row appeared after settling, and a click aimed at a folder row from the settled layout landed on the row above it (a_folder_menu_locates_its_missing_tree_only located `other` instead of `trip`). - missing_count adds itself to the fs_cached_running counter before it starts and takes itself off after the count is stored, as fs_cached does - the worker starts with thread::Builder::spawn instead of thread::spawn, which panics when the OS refuses a thread; on failure the last count stays and the next refresh tries again - settling_waits_for_the_missing_photos_count delays the worker by 1 s (test-only field on LightcraftApp, which ui.set does not rebuild) and fails without the fix Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/app.rs | 2 ++ crates/ui-egui/src/lib.rs | 4 +++ crates/ui-egui/src/panels/left/local.rs | 11 +++++--- crates/ui-egui/src/panels/left/sections.rs | 24 ++++++++++++++--- crates/ui-egui/src/tests/panels.rs | 30 +++++++++++++++++++++- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index fa10210f0..f93020281 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -64,6 +64,8 @@ impl LightcraftApp { ui_passes: 0, #[cfg(test)] library_passes: vec![], + #[cfg(test)] + missing_count_test_delay: None, shadow: None, synthetic: vec![], pointer_cleanup: false, diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8bf57b341..d1c1d342d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -216,6 +216,10 @@ pub struct LightcraftApp { /// effects (which the global shortcut guard also hides). #[cfg(test)] pub(crate) library_passes: Vec, + /// Test-only: a delay for the sidebar's Missing Photos count worker (a slow disk). Kept here, + /// not in `ui`, because `ui.set` rebuilds `ui` and would drop it. + #[cfg(test)] + pub(crate) missing_count_test_delay: Option, /// Offscreen context for headless screenshots of the windowed app. shadow: Option, /// Synthetic input events (from the control channel) injected one step per frame. diff --git a/crates/ui-egui/src/panels/left/local.rs b/crates/ui-egui/src/panels/left/local.rs index 1308059c8..744ca422a 100644 --- a/crates/ui-egui/src/panels/left/local.rs +++ b/crates/ui-egui/src/panels/left/local.rs @@ -224,14 +224,17 @@ fn list_subfolders(path: &str) -> std::sync::Arc> { v.into() } -/// How many [`fs_cached`] answers for `ctx` are being worked out right now. Rows appear (and -/// the sidebar below them moves) when they land, so the headless driver counts them as pending -/// work and waits for them before acting on widget positions. +/// How many file-system answers for `ctx` are being worked out right now: [`fs_cached`]'s and the +/// Missing Photos count's. Rows appear (and the sidebar below them moves) when they land, so the +/// headless driver counts them as pending work and waits for them before acting on widget +/// positions. pub(crate) fn fs_cached_running(ctx: &egui::Context) -> usize { fs_running_counter(ctx).load(std::sync::atomic::Ordering::Acquire) } -fn fs_running_counter(ctx: &egui::Context) -> std::sync::Arc { +/// The counter behind [`fs_cached_running`]: a worker adds one before it starts and takes it off +/// after its answer is stored. +pub(super) fn fs_running_counter(ctx: &egui::Context) -> std::sync::Arc { ctx.data_mut(|d| d.get_temp_mut_or_default::>(egui::Id::unique("fs-cached-running")).clone()) } diff --git a/crates/ui-egui/src/panels/left/sections.rs b/crates/ui-egui/src/panels/left/sections.rs index 5597904a8..16a4d652c 100644 --- a/crates/ui-egui/src/panels/left/sections.rs +++ b/crates/ui-egui/src/panels/left/sections.rs @@ -381,9 +381,11 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// How many library photos have no file (Local browse records are not checked; see /// `cmd::missing::checked_path`). Checking stats every file, which on a network share takes /// seconds, so it runs on a worker thread: the count shown is the last finished one, refreshed -/// at most every 5 s, and at once (after the running check) when the catalog changed. +/// at most every 5 s, and at once (after the running check) when the catalog changed. The check +/// counts as pending sidebar work ([`super::fs_cached_running`]): the Missing Photos row it adds +/// moves every row below it. pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize { - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::AcqRel, Ordering::Relaxed}; #[derive(Clone, Default)] struct Job { n: std::sync::Arc, @@ -402,15 +404,29 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize job.running.store(true, Relaxed); job.at = now; job.rev = rev; - let (n, running, ctx) = (job.n.clone(), job.running.clone(), ui.ctx().clone()); + let pending = super::local::fs_running_counter(ui.ctx()); + pending.fetch_add(1, AcqRel); + #[cfg(test)] + let test_delay = app.missing_count_test_delay; + let (n, running, ctx, done) = (job.n.clone(), job.running.clone(), ui.ctx().clone(), pending.clone()); let work = move || { + #[cfg(test)] + if let Some(d) = test_delay { + std::thread::sleep(d); + } let missing = if cfg!(target_arch = "wasm32") { 0 } else { paths.iter().filter(|p| !std::path::Path::new(p).exists()).count() }; n.store(missing, Relaxed); running.store(false, Relaxed); + // after the count is stored: a frame that sees the pending count drop also sees the row + done.fetch_sub(1, AcqRel); ctx.request_repaint(); }; #[cfg(not(target_arch = "wasm32"))] - std::thread::spawn(work); + if std::thread::Builder::new().name("lc-missing-count".into()).spawn(work).is_err() { + // no thread to spare: keep the last count and try again after the next 5 s + job.running.store(false, Relaxed); + pending.fetch_sub(1, AcqRel); + } #[cfg(target_arch = "wasm32")] work(); ui.data_mut(|d| d.insert_temp(id, job.clone())); diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs index 5208de9b7..79b8267fd 100644 --- a/crates/ui-egui/src/tests/panels.rs +++ b/crates/ui-egui/src/tests/panels.rs @@ -1495,6 +1495,11 @@ fn folders_app_sized(paths: &[&str], size: [f32; 2]) -> Headless { } fn folders_app_with_services(paths: &[&str], size: [f32; 2], services: Services) -> Headless { + folders_app_prepared(paths, size, services, |_| {}) +} + +/// [`folders_app_with_services`], with `prepare` run on the app before its first frame. +fn folders_app_prepared(paths: &[&str], size: [f32; 2], services: Services, prepare: impl FnOnce(&mut LightcraftApp)) -> Headless { use lightcraft_catalog::{Op, Photo, Source}; let mut session = lightcraft_engine::Session::new(); for path in paths { @@ -1502,7 +1507,8 @@ fn folders_app_with_services(paths: &[&str], size: [f32; 2], services: Services) let p = Photo::new(id, Source::File { path: (*path).into() }, "x.jpg", "JPEG", 60, 40, "2026-01-01T10:00:00"); session.catalog.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap(); } - let app = LightcraftApp::new(session, services); + let mut app = LightcraftApp::new(session, services); + prepare(&mut app); let mut h = Headless::new(app, size, 1.0); let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T); assert_eq!(r["ok"], true, "{r}"); @@ -1857,6 +1863,28 @@ fn a_folder_menu_locates_its_missing_tree_only() { assert!(sources.contains(&format!("{old}/other/c.jpg")), "another missing folder stays untouched: {sources:?}"); } +/// The Missing Photos row appears when the background count of missing files lands, and moves +/// every row below it down. Settling waits for that count, so a click aimed at a folder row from +/// the settled layout doesn't land on the row above it (on a busy machine, Locate Missing Files… +/// above ran for `other` instead of `trip`). +#[test] +fn settling_waits_for_the_missing_photos_count() { + // longer than settling takes without the count (a few hundred ms here) + let delay = Duration::from_secs(1); + let paths = ["/lc-missing-count/other/a.jpg", "/lc-missing-count/trip/b.jpg"]; + let services = Services { png: None, ..Default::default() }; + let mut h = folders_app_prepared(&paths, [1400.0, 900.0], services, |app| app.missing_count_test_delay = Some(delay)); + assert!(has(&h, "source:missing"), "the Missing Photos row is on screen once the sidebar settled"); + click(&mut h, "libraryFolderToggle:/lc-missing-count"); + let settled = widget(&h, "source:libfolder:/lc-missing-count/trip"); + let t0 = std::time::Instant::now(); + while t0.elapsed() < delay / 4 { + h.step(); + std::thread::sleep(Duration::from_millis(5)); + } + assert_eq!(widget(&h, "source:libfolder:/lc-missing-count/trip"), settled, "nothing moves the folder rows after settling"); +} + /// Right-click a folder ▸ Set Color Label ▸ a colour: the folder's row shows the label's dot (a /// row without a label shows none), and None takes it off again. #[test] From 9f7936c3c65fee7363dc2e87cef7658c2e2b08b9 Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:32:15 +0200 Subject: [PATCH 2/4] LRC-LIB-MISSING: a slow count's timed refresh doesn't hold up settling While a headless run settles, its simulated clock runs several times faster than wall time, so with every Missing Photos count pending, a count that takes about a second started the next 5 s refresh before the last one finished and settle() never ended (120 s timeout with a 1 s count, 48 s with 600 ms). That would also stall lightcraft-cli snapshot on a library on a slow share. Only the first count and a recount after a catalog change, which add or take away the Missing Photos row, are pending work now; the timed refresh is not. Settling takes as long as the count (1.0 s for 1 s, 3.0 s for 3 s). The regression test also checks that settling ends while a slow count refreshes. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/left/sections.rs | 22 +++++++++++++++------- crates/ui-egui/src/tests/panels.rs | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/panels/left/sections.rs b/crates/ui-egui/src/panels/left/sections.rs index 16a4d652c..7c59cf845 100644 --- a/crates/ui-egui/src/panels/left/sections.rs +++ b/crates/ui-egui/src/panels/left/sections.rs @@ -381,9 +381,11 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// How many library photos have no file (Local browse records are not checked; see /// `cmd::missing::checked_path`). Checking stats every file, which on a network share takes /// seconds, so it runs on a worker thread: the count shown is the last finished one, refreshed -/// at most every 5 s, and at once (after the running check) when the catalog changed. The check -/// counts as pending sidebar work ([`super::fs_cached_running`]): the Missing Photos row it adds -/// moves every row below it. +/// at most every 5 s, and at once (after the running check) when the catalog changed. The first +/// count and a recount after a catalog change are pending sidebar work +/// ([`super::fs_cached_running`]): the Missing Photos row they add or take away moves every row +/// below it. The timed refresh is not: it rarely changes the count, and on a share where counting +/// takes seconds, a headless run's fast simulated clock would start the next one before settling. pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize { use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::AcqRel, Ordering::Relaxed}; #[derive(Clone, Default)] @@ -401,11 +403,13 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize if !fresh && !job.running.load(Relaxed) { // the same scope as the Missing Photos view: library photos only, never Local browse records let paths = lightcraft_engine::cmd::missing::candidates(&app.session.catalog); + let pending = (job.rev != rev).then(|| super::local::fs_running_counter(ui.ctx())); + if let Some(p) = &pending { + p.fetch_add(1, AcqRel); + } job.running.store(true, Relaxed); job.at = now; job.rev = rev; - let pending = super::local::fs_running_counter(ui.ctx()); - pending.fetch_add(1, AcqRel); #[cfg(test)] let test_delay = app.missing_count_test_delay; let (n, running, ctx, done) = (job.n.clone(), job.running.clone(), ui.ctx().clone(), pending.clone()); @@ -418,14 +422,18 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize n.store(missing, Relaxed); running.store(false, Relaxed); // after the count is stored: a frame that sees the pending count drop also sees the row - done.fetch_sub(1, AcqRel); + if let Some(d) = done { + d.fetch_sub(1, AcqRel); + } ctx.request_repaint(); }; #[cfg(not(target_arch = "wasm32"))] if std::thread::Builder::new().name("lc-missing-count".into()).spawn(work).is_err() { // no thread to spare: keep the last count and try again after the next 5 s job.running.store(false, Relaxed); - pending.fetch_sub(1, AcqRel); + if let Some(p) = &pending { + p.fetch_sub(1, AcqRel); + } } #[cfg(target_arch = "wasm32")] work(); diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs index 79b8267fd..bc37df9fc 100644 --- a/crates/ui-egui/src/tests/panels.rs +++ b/crates/ui-egui/src/tests/panels.rs @@ -1883,6 +1883,9 @@ fn settling_waits_for_the_missing_photos_count() { std::thread::sleep(Duration::from_millis(5)); } assert_eq!(widget(&h, "source:libfolder:/lc-missing-count/trip"), settled, "nothing moves the folder rows after settling"); + // the timed refresh of a slow count is not waited for: the fast simulated clock would start + // the next one before the last finished, and settling would never end + assert!(h.settle(Duration::from_secs(30)), "the sidebar settles while a slow count refreshes"); } /// Right-click a folder ▸ Set Color Label ▸ a colour: the folder's row shows the label's dot (a From 1def7587f5887f0ba0273741b2e5a6a1251dc614 Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:16:05 +0200 Subject: [PATCH 3/4] LRC-LIB-MISSING: a catalog change during a timed refresh is pending at once A catalog change seen while a timed refresh of the Missing Photos count is running cannot start its recount until the refresh ends, and the refresh is not pending work, so settle() could return before either finished and the row the recount adds moved the folder rows afterwards (CodeRabbit on #3392). The change is now counted as pending from the frame it is seen (`owed`); the recount takes that entry over when it starts and drops it once its count is stored. settling_waits_for_a_recount_owed_during_a_timed_refresh adds a missing photo while a slow refresh runs and fails without this. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/left/sections.rs | 35 ++++++++++++++++------ crates/ui-egui/src/tests/panels.rs | 24 +++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/panels/left/sections.rs b/crates/ui-egui/src/panels/left/sections.rs index 7c59cf845..9d8abcd83 100644 --- a/crates/ui-egui/src/panels/left/sections.rs +++ b/crates/ui-egui/src/panels/left/sections.rs @@ -184,7 +184,7 @@ pub fn section_drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) { /// A section's header in `r`: a disclosure chevron in the gutter (where tree rows have their /// chevron), then the bold title; a click folds or unfolds the section (kept in the UI state, so /// it survives restarts), and what the section adds to its header (its button, see `header_button`) sits at -/// the visible edge. A folded section says how many rows it holds (`rows`, with View ▸ Show Photo +/// the visible edge. A folded section says how many rows it holds (`rows`, with View β–Έ Show Photo /// Counts). `pinned`: this is the copy kept at the top of the sidebar (see `pinned_header`). fn section_header(app: &mut LightcraftApp, ui: &mut egui::Ui, section: SidebarSection, r: Rect, rows: usize, pinned: bool) -> HeaderOutcome { let t = Tokens::get(ui.ctx()); @@ -350,12 +350,12 @@ pub(super) fn sections_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, on: Opti } } -/// The menu of Albums' +: Create Album, Smart Album, Folder… +/// The menu of Albums' +: Create Album, Smart Album, Folder… fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { - if ui.button(crate::i18n::tr("Create Album…")).clicked() { + if ui.button(crate::i18n::tr("Create Album…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: false, parent: None, include_selected: true }); } - if ui.button(crate::i18n::tr("Create Smart Album…")).clicked() { + if ui.button(crate::i18n::tr("Create Smart Album…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::SmartRules { id: None, name: String::new(), @@ -363,16 +363,16 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { parent: None, }); } - if ui.button(crate::i18n::tr("Create Smart Album from Filter…")).clicked() { + if ui.button(crate::i18n::tr("Create Smart Album from Filter…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewSmartAlbum { name: String::new(), parent: None }); } - if ui.button(crate::i18n::tr("Create Folder…")).clicked() { + if ui.button(crate::i18n::tr("Create Folder…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: true, parent: None, include_selected: false }); } // only once the albums were put in an order by hand if app.session.catalog.album_children_are_ordered(None) { ui.separator(); - if ui.button(crate::i18n::tr("Sort Albums A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked() { + if ui.button(crate::i18n::tr("Sort Albums A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked() { let _ = app.run("album.sort", json!({})); } } @@ -386,6 +386,8 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// ([`super::fs_cached_running`]): the Missing Photos row they add or take away moves every row /// below it. The timed refresh is not: it rarely changes the count, and on a share where counting /// takes seconds, a headless run's fast simulated clock would start the next one before settling. +/// A catalog change during a timed refresh is pending from the frame it is seen, although its +/// recount starts only when the refresh ends. pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize { use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::AcqRel, Ordering::Relaxed}; #[derive(Clone, Default)] @@ -394,19 +396,34 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize running: std::sync::Arc, at: f64, rev: u64, + /// A catalog change arrived while a count was running: its recount is pending work + /// already, held until the recount stores its result. + owed: bool, } let id = egui::Id::unique("missing-count"); let now = ui.input(|i| i.time); let rev = app.session.catalog.revision; let mut job: Job = ui.data(|d| d.get_temp(id)).unwrap_or(Job { rev: u64::MAX, at: f64::MIN, ..Default::default() }); + let running = job.running.load(Relaxed); + if running && job.rev != rev && !job.owed { + // the recount waits for the running count (a timed refresh, not waited for), but settling + // must wait for it from now on + super::local::fs_running_counter(ui.ctx()).fetch_add(1, AcqRel); + job.owed = true; + ui.data_mut(|d| d.insert_temp(id, job.clone())); + } let fresh = now - job.at < 5.0 && job.rev == rev; - if !fresh && !job.running.load(Relaxed) { + if !fresh && !running { // the same scope as the Missing Photos view: library photos only, never Local browse records let paths = lightcraft_engine::cmd::missing::candidates(&app.session.catalog); let pending = (job.rev != rev).then(|| super::local::fs_running_counter(ui.ctx())); - if let Some(p) = &pending { + if let Some(p) = &pending + && !job.owed + { p.fetch_add(1, AcqRel); } + // the recount takes over the entry held while it waited + job.owed = false; job.running.store(true, Relaxed); job.at = now; job.rev = rev; diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs index bc37df9fc..194e235e4 100644 --- a/crates/ui-egui/src/tests/panels.rs +++ b/crates/ui-egui/src/tests/panels.rs @@ -1888,6 +1888,30 @@ fn settling_waits_for_the_missing_photos_count() { assert!(h.settle(Duration::from_secs(30)), "the sidebar settles while a slow count refreshes"); } +/// A catalog change while a slow timed refresh of the count runs: its recount starts only when +/// the refresh ends, but settling waits for it from the frame the change is seen, so the Missing +/// Photos row it adds is on screen once the sidebar settled. +#[test] +fn settling_waits_for_a_recount_owed_during_a_timed_refresh() { + use lightcraft_catalog::{Op, Photo, Source}; + let dir = std::env::temp_dir().join(format!("lc-ui-missing-owed-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let here = dir.join("a.jpg"); + std::fs::write(&here, b"photo").unwrap(); + // settling takes the first count (2 s), in which the simulated clock passes the 5 s refresh + // interval: a timed refresh is running when it returns + let delay = Duration::from_secs(2); + let services = Services { png: None, ..Default::default() }; + let mut h = folders_app_prepared(&[&here.to_string_lossy()], [1400.0, 900.0], services, |app| app.missing_count_test_delay = Some(delay)); + assert!(!has(&h, "source:missing"), "no photo is missing yet"); + let id = h.app.session.catalog.alloc_photo_id(); + let gone = Photo::new(id, Source::File { path: "/lc-missing-owed/gone.jpg".into() }, "gone.jpg", "JPEG", 60, 40, "2026-01-01T10:00:00"); + h.app.session.catalog.apply(Op::AddPhoto { photo: Box::new(gone) }).unwrap(); + assert!(h.settle(Duration::from_secs(30)), "the sidebar settles"); + assert!(has(&h, "source:missing"), "the Missing Photos row is on screen once the sidebar settled"); + let _ = std::fs::remove_dir_all(dir); +} + /// Right-click a folder ▸ Set Color Label ▸ a colour: the folder's row shows the label's dot (a /// row without a label shows none), and None takes it off again. #[test] From eb693b33b0bccff65b108ee27528ffe6d6fd5f6d Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:49:51 +0200 Subject: [PATCH 4/4] =?UTF-8?q?LRC-LIB-MISSING:=20restore=20the=20album-me?= =?UTF-8?q?nu=20labels'=20=E2=80=A6=20and=20=E2=80=93=20characters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit's sections.rs went through a PowerShell read and write in the Windows ANSI code page, which turned …, – and ▸ into mojibake in five album-menu labels (Create Album…, Sort Albums A–Z, …) and two doc comments, so the labels missed their translation keys (CodeRabbit on #3392). The file's text outside missing_count is now byte for byte what it was before. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/left/sections.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/panels/left/sections.rs b/crates/ui-egui/src/panels/left/sections.rs index 9d8abcd83..76550d14c 100644 --- a/crates/ui-egui/src/panels/left/sections.rs +++ b/crates/ui-egui/src/panels/left/sections.rs @@ -184,7 +184,7 @@ pub fn section_drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) { /// A section's header in `r`: a disclosure chevron in the gutter (where tree rows have their /// chevron), then the bold title; a click folds or unfolds the section (kept in the UI state, so /// it survives restarts), and what the section adds to its header (its button, see `header_button`) sits at -/// the visible edge. A folded section says how many rows it holds (`rows`, with View β–Έ Show Photo +/// the visible edge. A folded section says how many rows it holds (`rows`, with View ▸ Show Photo /// Counts). `pinned`: this is the copy kept at the top of the sidebar (see `pinned_header`). fn section_header(app: &mut LightcraftApp, ui: &mut egui::Ui, section: SidebarSection, r: Rect, rows: usize, pinned: bool) -> HeaderOutcome { let t = Tokens::get(ui.ctx()); @@ -350,12 +350,12 @@ pub(super) fn sections_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, on: Opti } } -/// The menu of Albums' +: Create Album, Smart Album, Folder… +/// The menu of Albums' +: Create Album, Smart Album, Folder… fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { - if ui.button(crate::i18n::tr("Create Album…")).clicked() { + if ui.button(crate::i18n::tr("Create Album…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: false, parent: None, include_selected: true }); } - if ui.button(crate::i18n::tr("Create Smart Album…")).clicked() { + if ui.button(crate::i18n::tr("Create Smart Album…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::SmartRules { id: None, name: String::new(), @@ -363,16 +363,16 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { parent: None, }); } - if ui.button(crate::i18n::tr("Create Smart Album from Filter…")).clicked() { + if ui.button(crate::i18n::tr("Create Smart Album from Filter…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewSmartAlbum { name: String::new(), parent: None }); } - if ui.button(crate::i18n::tr("Create Folder…")).clicked() { + if ui.button(crate::i18n::tr("Create Folder…")).clicked() { app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: true, parent: None, include_selected: false }); } // only once the albums were put in an order by hand if app.session.catalog.album_children_are_ordered(None) { ui.separator(); - if ui.button(crate::i18n::tr("Sort Albums A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked() { + if ui.button(crate::i18n::tr("Sort Albums A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked() { let _ = app.run("album.sort", json!({})); } }