LRC-LIB-FILTERBAR: colour-label multi-select (any of the chosen labels)
Filter.labels: a photo matches when its label is any of them. The filter bar's label toggles add / remove labels from the set instead of replacing the single label; the summary reads "label red or yellow". The filter already persists across albums (locked, as in the desktop app). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a3d487d22d
commit
b8b2379215
|
|
@ -24,6 +24,9 @@ pub struct Filter {
|
|||
pub rating_op: RatingOp,
|
||||
pub flag: Option<Flag>,
|
||||
pub label: Option<ColorLabel>,
|
||||
/// Any of these labels (the filter bar's multi-select); empty = no constraint.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub labels: Vec<ColorLabel>,
|
||||
pub kind: Option<MediaKind>,
|
||||
pub edited: Option<bool>,
|
||||
pub album: Option<AlbumId>,
|
||||
|
|
@ -137,6 +140,9 @@ impl Filter {
|
|||
if let Some(l) = self.label {
|
||||
v.push(format!("label {}", format!("{l:?}").to_lowercase()));
|
||||
}
|
||||
if !self.labels.is_empty() {
|
||||
v.push(format!("label {}", self.labels.iter().map(|l| format!("{l:?}").to_lowercase()).collect::<Vec<_>>().join(" or ")));
|
||||
}
|
||||
if let Some(k) = self.kind {
|
||||
v.push(format!("kind {}", format!("{k:?}").to_lowercase()));
|
||||
}
|
||||
|
|
@ -201,6 +207,9 @@ impl Filter {
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if !self.labels.is_empty() && !p.label.is_some_and(|l| self.labels.contains(&l)) {
|
||||
return false;
|
||||
}
|
||||
if let Some(want) = &self.merged {
|
||||
match merged_kind(&p.file_name) {
|
||||
Some(k) if want == "any" || want == k => {}
|
||||
|
|
|
|||
|
|
@ -189,3 +189,23 @@ fn smart_album_rule_sets() {
|
|||
let a = list.as_array().unwrap().iter().find(|a| a["name"] == "Good ones").unwrap().clone();
|
||||
assert!(a.to_string().contains("rating is ≥ 4"), "{a}");
|
||||
}
|
||||
|
||||
/// The filter bar's label multi-select: any of the chosen labels.
|
||||
#[test]
|
||||
fn filter_any_of_several_labels() {
|
||||
use lightcraft_catalog::ColorLabel;
|
||||
let mut s = crate::Session::with_demo();
|
||||
let ids: Vec<u64> = s.catalog.photos().take(3).map(|p| p.id.0).collect();
|
||||
for (id, l) in ids.iter().zip(["red", "yellow", "green"]) {
|
||||
s.execute("photo.label", &serde_json::json!({"ids": [id], "label": l})).unwrap();
|
||||
}
|
||||
s.execute("library.filter", &serde_json::json!({"labels": ["red", "yellow"]})).unwrap();
|
||||
let vis = s.visible_cloned();
|
||||
let want = s.catalog.photos().filter(|p| p.in_library() && matches!(p.label, Some(ColorLabel::Red | ColorLabel::Yellow))).count();
|
||||
assert_eq!(want, 2);
|
||||
assert_eq!(vis.len(), want);
|
||||
assert!(vis.iter().all(|id| matches!(s.catalog.photo(*id).unwrap().label, Some(ColorLabel::Red | ColorLabel::Yellow))));
|
||||
assert!(s.filter.describe().contains("label red or yellow"));
|
||||
s.execute("library.filter", &serde_json::json!({"labels": []})).unwrap();
|
||||
assert!(s.visible_cloned().len() > want);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,15 +168,30 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
|
||||
// colour labels
|
||||
caption(ui, "Label");
|
||||
// several labels can be on at once (any of them matches)
|
||||
let mut chosen: Vec<ColorLabel> = f.labels.clone();
|
||||
if let Some(l) = f.label
|
||||
&& !chosen.contains(&l)
|
||||
{
|
||||
chosen.push(l);
|
||||
}
|
||||
for l in ColorLabel::ALL {
|
||||
let on = f.label == Some(l);
|
||||
let on = chosen.contains(&l);
|
||||
let name = format!("{l:?}").to_lowercase();
|
||||
let tip = format!("{} label", app.session.catalog.label_name(l));
|
||||
let tip = format!("{} label (click more labels to show any of them)", app.session.catalog.label_name(l));
|
||||
let resp = toggle(ui, &format!("label-{name}"), on, &tip, |p, r, _| {
|
||||
p.circle_filled(r.center(), 5.5, label_color(l));
|
||||
});
|
||||
if resp.clicked() {
|
||||
filter(app, json!({"label": if on { Value::Null } else { json!(name) }}));
|
||||
let mut next = chosen.clone();
|
||||
if on {
|
||||
next.retain(|x| *x != l);
|
||||
} else {
|
||||
next.push(l);
|
||||
}
|
||||
next.sort();
|
||||
let names: Vec<String> = next.iter().map(|x| format!("{x:?}").to_lowercase()).collect();
|
||||
filter(app, json!({"label": Value::Null, "labels": names}));
|
||||
}
|
||||
}
|
||||
ui.add_space(14.0);
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table
|
|||
| X. Cross-cutting behaviours (BEHAV) | 13 | 0 | 4 | 1 | 8/8 (100%) | 5/5 (100%) |
|
||||
| Y. Menus | 70 | 7 | 8 | 8 | 46/47 (98%) | 19/23 (83%) |
|
||||
| Z. Keyboard shortcuts (desktop) | 68 | 8 | 4 | 1 | 49/52 (94%) | 17/23 (74%) |
|
||||
| Lightroom Classic extras | 16 | 14 | 49 | 9 | — | 15/21 (71%) |
|
||||
| **Total** | 331 | 45 | 124 | 37 | 193/198 (97%) | 118/144 (82%) |
|
||||
| Lightroom Classic extras | 17 | 13 | 49 | 9 | — | 16/21 (76%) |
|
||||
| **Total** | 332 | 44 | 124 | 37 | 193/198 (97%) | 119/144 (83%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **70.7%** of 500 in-scope rows — P0 98.7% of 198 · P1 89.6% of 144 · P2 18.4% of 158.
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **70.8%** of 500 in-scope rows — P0 98.7% of 198 · P1 89.9% of 144 · P2 18.4% of 158.
|
||||
<!-- /parity:summary -->
|
||||
|
||||
## Top gaps
|
||||
|
|
@ -697,7 +697,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
|||
| LRC-LIB-COLLECTIONS | Collections & sets | P1 | 🟡 | `cmd:album.create` | albums + folders; no smart / quick / target collections |
|
||||
| LRC-LIB-SMARTCOLL | Smart-collection rules | P1 | ✅ | `cmd:album.createSmart`, `cmd:album.setRules`, `cmd:album.ruleFields`, `cmd:dialog.smartAlbum`, `crates/catalog/src/rules.rs`, `crates/ui-egui/src/panels/rules_editor.rs` | rule editor: match all / any / none, nested groups (⌥+ or + Group), 26 fields (rating, flag, label, type, edits, keywords, any text, filename, format, title, caption, camera, lens, location, creator, copyright, capture / import / edit date, ISO, aperture, focal length, megapixels, GPS, virtual copy, album) with text / number / date / in-the-last operators; live match count; also usable as a library filter (`ruleSet`) |
|
||||
| LRC-LIB-PUBLISH | Publish services | P2 | ⬜ | | |
|
||||
| LRC-LIB-FILTERBAR | Library filter bar | P1 | 🟡 | `cmd:view.filterBar`, `cmd:filter.applyPreset`, `crates/ui-egui/src/panels/filterbar.rs` | rating/flag/label/kind/edited/camera/lens/keyword, clear, save as smart album, filter presets; no lock or multi-select columns |
|
||||
| LRC-LIB-FILTERBAR | Library filter bar | P1 | ✅ | `cmd:view.filterBar`, `cmd:filter.applyPreset`, `cmd:library.filter` (`labels`, `ruleSet`), `crates/ui-egui/src/panels/filterbar.rs` | rating/flag/label (several labels at once = any of them)/kind/edited/camera/lens/keyword, clear, save as smart album, filter presets; the filter stays as you change albums (always locked, as in the desktop app); arbitrary conditions via the smart-album rule editor |
|
||||
| LRC-LIB-STACKS | Stacks (full) | P1 | ✅ | `crates/catalog/src/stacks.rs`, `cmd:stack.group`, `cmd:stack.split`, `cmd:stack.moveUp`, `cmd:stack.moveDown` | group / ungroup / toggle / set top / remove / auto by time / split / move up and down |
|
||||
| LRC-LIB-VC | Virtual copies | P1 | ✅ | `cmd:photo.virtualCopy` | ⌘' |
|
||||
| LRC-LIB-LABELS | Colour-label sets | P1 | ✅ | `cmd:label.sets`, `cmd:label.applySet`, `cmd:label.saveSet`, `cmd:label.deleteSet`, `cmd:label.setNames`, `crates/engine/src/cmd/manage.rs` | built-in Colors / Review sets + user sets (Photo ▸ Set Color Label, Edit Label Names… dialog); names written to and read from `xmp:Label` |
|
||||
|
|
|
|||
Loading…
Reference in New Issue