M1: show bottom confirmation for colour label actions

This commit is contained in:
Hanmo 2026-10-08 21:25:02 +08:00
parent 6277af55bf
commit 57c9e252c1
8 changed files with 54 additions and 2 deletions

View File

@ -147,6 +147,7 @@
"Cloudy": "曇り",
"Collapse All Stacks": "すべてのスタックを閉じる",
"Color": "カラー",
"Color label cleared": "カラーラベルを解除しました",
"Color Grading": "カラーグレーディング",
"Color Mixer": "カラーミキサー",
"Color Noise Reduction": "カラーノイズ軽減",

View File

@ -147,6 +147,7 @@
"Cloudy": "阴天",
"Collapse All Stacks": "折叠所有堆叠",
"Color": "颜色",
"Color label cleared": "已清除颜色标签",
"Color Grading": "颜色分级",
"Color Mixer": "颜色混合器",
"Color Noise Reduction": "颜色降噪",

View File

@ -187,6 +187,7 @@
"Cloudy": "陰天",
"Collapse All Stacks": "收合所有堆疊",
"Color": "色彩",
"Color label cleared": "已清除色彩標籤",
"Color Grading": "色彩分級",
"Color Label Names": "色彩標籤名稱",
"Color Label Sets": "色彩標籤集",

View File

@ -52,6 +52,8 @@ use serde_json::Value;
pub use control::{ControlRequest, ControlResponse};
pub use state::UiState;
const TOAST_SECONDS: f64 = 1.4;
pub type PickFiles = Box<dyn FnMut() -> Vec<String>>;
/// A save dialog: suggested file name → chosen path (`None` = cancelled).
pub type SaveFile = Box<dyn FnMut(&str) -> Option<String>>;
@ -265,6 +267,19 @@ impl LightcraftApp {
return r;
}
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
if r.is_ok() && id == "photo.label" {
let text = match params.get("label").and_then(Value::as_str).and_then(lightcraft_catalog::ColorLabel::parse) {
Some(label) => {
let name =
self.session.catalog.custom_label_name(label).map(str::to_owned).unwrap_or_else(|| i18n::tr(&format!("{label:?}")).into());
i18n::tr_format!("{name} Label", name = name)
}
None => i18n::tr("Color label cleared").into(),
};
// Native menu clicks can arrive before logic() updates last_time after an idle gap.
let now = self.tasks.repaint.as_ref().map(|ctx| ctx.input(|i| i.time)).unwrap_or(self.last_time);
self.ui.toast = Some((text, now + TOAST_SECONDS));
}
if let Err(e) = &r {
log::warn!("{id}: {e}");
self.ui.status = e.clone();
@ -351,7 +366,7 @@ impl LightcraftApp {
}
pub fn toast(&mut self, ctx: &egui::Context, text: impl Into<String>) {
self.toast_for(ctx, text, 1.4);
self.toast_for(ctx, text, TOAST_SECONDS);
}
/// A toast that stays `secs` seconds (messages that say where to look or what to do next).

View File

@ -424,6 +424,37 @@ mod tests {
}
}
#[test]
fn color_label_actions_show_feedback_only_after_success() {
use lightcraft_catalog::PhotoId;
let mut h = library();
for shift in [false, true] {
for (k, name) in [("6", "Red"), ("7", "Yellow"), ("8", "Green"), ("9", "Blue")] {
h.app.session.execute("library.select", &json!({"ids": [1]})).unwrap();
h.app.ui.toast = None;
key(&mut h, k, shift);
assert_eq!(h.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some(format!("{name} Label").as_str()));
assert_eq!(h.app.session.active(), Some(PhotoId(if shift { 2 } else { 1 })));
}
}
// Menus and the Info-panel swatches dispatch the same command, including purple/clear.
h.app.run("photo.label", json!({"label": "purple"})).unwrap();
assert_eq!(h.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Purple Label"));
h.app.run("photo.label", json!({"label": "none"})).unwrap();
assert_eq!(h.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Color label cleared"));
h.app.session.execute("label.setNames", &json!({"names": {"red": "Needs review"}})).unwrap();
key(&mut h, "6", false);
assert_eq!(h.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Needs review Label"));
// Native menu callbacks run before logic() catches up to the current frame's clock.
let now = h.view.ctx.input(|i| i.time);
h.app.last_time = now - 10.0;
h.app.run("photo.label", json!({"label": "blue"})).unwrap();
assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.1 > now), "menu feedback must survive an idle gap");
h.app.ui.toast = None;
assert!(h.app.run("photo.label", json!({"label": "invalid"})).is_err());
assert!(h.app.ui.toast.is_none(), "failed actions must not announce success");
}
#[test]
fn shifted_number_punctuation_rates_and_advances() {
use lightcraft_catalog::PhotoId;

View File

@ -15,6 +15,8 @@ Select photos in Photo Grid or Square Grid, then use:
Purple and clearing a colour label are available in Photo → Set Color Label and the photo context
menu. Purple has no default number key, matching Lightroom Classic. Rejection marks a photo for
culling; it does not delete the original or remove it from the library.
Setting a colour label shows the same brief bottom toast as a rating, using the colour or your
custom label name. Clearing it shows a confirmation too, whether applied by a key, menu or swatch.
Grid actions apply to all selected photos and support Undo / Redo. In Compare and Survey, culling
actions apply to the active photo. Auto Advance moves forward after an action; holding Shift with

View File

@ -151,7 +151,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-LIB-TARGET | Target album | P2 | ✅ | `cmd:album.setTarget`, `cmd:album.toggleTarget` | see LRC-LIB-COLLECTIONS |
| LR-LIB-RATING | Star ratings | P0 | ✅ | `cmd:photo.rate` (`advance`), `crates/ui-egui/src/shortcuts.rs` | 0 clears; 1–5 set; Shift+0–5 applies and advances once, including with Auto Advance. Batch grid and undo/redo regressions; macOS native ownership corrected (PR #261) |
| LR-LIB-FLAG | Pick / reject flags | P0 | ✅ | `cmd:photo.pick`, `cmd:photo.reject`, `cmd:photo.unflag`, `cmd:photo.flag` | P pick, X reject, U clear; Shift+P picks and advances in both grids (Presets elsewhere); Shift+X/U and legacy Shift+Z advance; no flag cycle; rejection does not delete the file |
| LR-LIB-LABEL | Colour labels | P1 | ✅ | `cmd:photo.label`, `cmd:label.setNames`, `cmd:label.names`, `cmd:dialog.labelNames`, keys 6–9 in `crates/ui-egui/src/shortcuts.rs` | Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot in grid cells; editable label names (undoable, journaled); no purple key (as in Lightroom), no custom extra labels |
| LR-LIB-LABEL | Colour labels | P1 | ✅ | `cmd:photo.label`, `cmd:label.setNames`, `cmd:label.names`, `cmd:dialog.labelNames`, keys 6–9 in `crates/ui-egui/src/shortcuts.rs` | Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot in grid cells; setting/clearing through keys (including Shift), menus or Info swatches shows the same bottom toast as ratings, with the colour/custom name; editable label names (undoable, journaled); no purple key (as in Lightroom), no custom extra labels |
| LR-LIB-KEYWORD | Keywords | P0 | ✅ | `cmd:panel.keywords`, `cmd:photo.setMeta` (`addKeywords`/`removeKeywords`), `cmd:keyword.list`, `cmd:keyword.rename`, `cmd:keyword.delete`, `cmd:keyword.merge`, `cmd:keyword.suggest`, `crates/catalog/src/keywords.rs`, `crates/ui-egui/src/panels/left.rs` | left-panel keyword tree with counts (click filters, children included; context menu rename / merge / delete / add / remove); library-wide ops are one undo step and replay from the op log; suggestions (co-occurring / most used / completions) in the Keywords panel; no keyword drag-and-drop |
| LR-LIB-PEOPLE | People / faces | P2 | 🟡 | `cmd:view.people`, `cmd:view.faceBoxes`, `cmd:photo.removeRegion`, `cmd:photo.setRegion`, `cmd:library.filter`, `crates/ui-egui/src/panels/people.rs`, `crates/catalog/src/query.rs` (`people`), `crates/engine/src/media.rs` (`face_job`), `crates/meta/src/xmp.rs` | MVP: named face regions read from XMP (MWG-RS, read-only, see `docs/xmp-interop.md`; clipped to the photo; Lightroom's `Rotation` of ±π/2 and π handled, mirrored orientations taken as written; boxes follow Rotate Left/Right and flips; a sidecar stating `mwg-rs:Regions`, even empty, replaces the photo's on re-read, one without leaves them) become cards in the People view (View ▸ People, or the People button in the toolbar): a close-up of the person's largest face, name and photo count; a click filters the grid (`person` in `library.filter`, `person:` search token, filter chip). Face boxes in the loupe can be switched off (View ▸ Face Boxes, toolbar) resized with eight drag handles and removed with the × shown on hover (catalog-only and undoable, one undo step per drag; the sidecar is never rewritten for either); no moving a box by dragging it yet. No face detection, no unnamed people / suggestions, no manual tagging, no writing regions back |
| LR-LIB-STACK | Stacks | P1 | ✅ | `cmd:stack.group`, `cmd:stack.ungroup`, `cmd:stack.toggle`, `cmd:stack.setTop`, `cmd:stack.remove`, `cmd:stack.auto`, `crates/catalog/src/stacks.rs` | grid/filmstrip count badges, expand/collapse, auto-stack by capture time; no visual-similarity auto-stack |

View File

@ -3,6 +3,7 @@
## October 2026
### Library keyboard culling
- Setting or clearing a colour label shows a brief bottom confirmation, like rating a photo; custom label names appear in the message too.
- On macOS, ratings `0–5`, labels `6–9` and pick/unflag `P/U` now reach the app even when shown in the native menu (issue #283; adapted from PR #261).
- `Shift+6–9` labels and advances. `Shift+P` picks and advances in Photo Grid and Square Grid; it opens Presets in other views. With Auto Advance on, Shift still moves only once.
- Help → Keyboard Shortcuts includes the number-key bindings. See [library shortcuts](library-shortcuts.md).