mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-10 18:39:51 -06:00
Merge PR #2699
This commit is contained in:
@@ -240,6 +240,17 @@ fn has_person(p: &Photo, name: &str) -> bool {
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
/// The labels that `label` and `labels` let through together (they AND), in `labels`' order;
|
||||
/// `None` when neither is set, empty when they exclude each other. The chip and the filter
|
||||
/// bar's buttons show this, so they match what is filtered.
|
||||
pub fn labels_shown(&self) -> Option<Vec<ColorLabel>> {
|
||||
match (self.label, self.labels.is_empty()) {
|
||||
(None, true) => None,
|
||||
(Some(l), true) => Some(vec![l]),
|
||||
(l, false) => Some(self.labels.iter().copied().filter(|x| l.is_none_or(|l| l == *x)).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable summary of the active rules (smart album tooltips, `album.list`).
|
||||
pub fn describe(&self) -> String {
|
||||
self.describe_in(None)
|
||||
|
||||
@@ -314,6 +314,29 @@ fn smart_album_rule_sets() {
|
||||
assert!(a.to_string().contains("rating is ≥ 4"), "{a}");
|
||||
}
|
||||
|
||||
/// `label` and `labels` set together (only through `library.filter`, or a filter saved with the
|
||||
/// single label) AND, and the chip says what they let through, not their union.
|
||||
#[test]
|
||||
fn filter_label_and_labels_together_chip() {
|
||||
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", &json!({"ids": [id], "label": l})).unwrap();
|
||||
}
|
||||
let reds = s.catalog.photos().filter(|p| p.in_library() && p.label == Some(ColorLabel::Red)).count();
|
||||
let chip = |s: &Session| crate::filter_chips(&s.filter, &s.catalog).into_iter().find(|c| c.label.starts_with("Label")).expect("a label chip");
|
||||
s.execute("library.filter", &json!({"label": "red", "labels": ["yellow", "red"]})).unwrap();
|
||||
assert_eq!(s.visible_cloned().len(), reds);
|
||||
assert_eq!(chip(&s).label, "Label: Red");
|
||||
// nothing passes both: the chip names both constraints
|
||||
s.execute("library.filter", &json!({"label": "red", "labels": ["yellow", "green"]})).unwrap();
|
||||
assert_eq!(s.visible_cloned().len(), 0);
|
||||
assert_eq!(chip(&s).label, "Label: Red and (Yellow or Green)");
|
||||
s.execute("library.filter", &chip(&s).clear).unwrap();
|
||||
assert_eq!((s.filter.label, s.filter.labels.len()), (None, 0));
|
||||
}
|
||||
|
||||
/// The filter bar's label multi-select: any of the chosen labels.
|
||||
#[test]
|
||||
fn filter_any_of_several_labels() {
|
||||
|
||||
@@ -161,11 +161,17 @@ pub fn filter_chips(f: &Filter, cat: &Catalog) -> Vec<FilterChip> {
|
||||
if let Some(fl) = f.flag {
|
||||
add(format!("Flag: {}", format!("{fl:?}").to_lowercase()), json!({"flag": Null}));
|
||||
}
|
||||
let mut labels = f.labels.clone();
|
||||
labels.extend(f.label.filter(|l| !labels.contains(l)));
|
||||
if !labels.is_empty() {
|
||||
let names: Vec<String> = labels.iter().map(|l| cat.label_name(*l)).collect();
|
||||
add(format!("Label: {}", names.join(" or ")), json!({"label": Null, "labels": []}));
|
||||
if let Some(shown) = f.labels_shown() {
|
||||
let or = |ls: &[lightcraft_catalog::ColorLabel]| ls.iter().map(|l| cat.label_name(*l)).collect::<Vec<_>>().join(" or ");
|
||||
// empty: `label` and `labels` exclude each other, so name both constraints
|
||||
let names = if shown.is_empty() {
|
||||
// parenthesised when plural, so "Red and Yellow or Green" cannot read as "(Red and Yellow) or Green"
|
||||
let plural = if f.labels.len() > 1 { format!("({})", or(&f.labels)) } else { or(&f.labels) };
|
||||
format!("{} and {plural}", f.label.map(|l| cat.label_name(l)).unwrap_or_default())
|
||||
} else {
|
||||
or(&shown)
|
||||
};
|
||||
add(format!("Label: {names}"), json!({"label": Null, "labels": []}));
|
||||
}
|
||||
if let Some(k) = f.kind {
|
||||
let n = match k {
|
||||
|
||||
@@ -2276,10 +2276,12 @@
|
||||
"or drop a .onnx file on the window": "tai pudota .onnx-tiedosto ikkunaan",
|
||||
"or drop photos and folders into this window": "tai pudota kuvia ja kansioita tähän ikkunaan",
|
||||
"or drop photos into this window": "tai pudota kuvia tähän ikkunaan",
|
||||
"pick": "valittu kuva",
|
||||
"photo has its denoised picture.": "kuvalla on kohinanpoistettu kuvansa.",
|
||||
"photo waiting": "kuva odottaa",
|
||||
"photos have their denoised picture.": "kuvilla on kohinanpoistetut kuvansa.",
|
||||
"photos waiting": "kuvaa odottaa",
|
||||
"reject": "hylätty",
|
||||
"saved in": "tallennettu kohteeseen",
|
||||
"second": "sekunti",
|
||||
"starts using it for AI Denoise": "alkaa käyttää sitä tekoälykohinanpoistoon",
|
||||
|
||||
@@ -2020,6 +2020,30 @@ mod tests {
|
||||
assert!(deep.is_some_and(|n| n > 500), "scrolled rows: {later:?}");
|
||||
}
|
||||
|
||||
/// With `label` and `labels` both set (a saved single-label filter, or an agent), the filter
|
||||
/// bar lights only the labels that pass both, and a click starts from those, not their union.
|
||||
#[test]
|
||||
fn filter_bar_labels_from_label_and_labels() {
|
||||
use lightcraft_catalog::ColorLabel;
|
||||
let mut h = demo([1400.0, 800.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.request("engine.execute", json!({"command": "view.filterBar"}), t);
|
||||
h.app.session.execute("library.filter", &json!({"label": "red", "labels": ["red", "yellow"]})).unwrap();
|
||||
// a frame so the bar is laid out for the new filter before the click
|
||||
h.step();
|
||||
let r = h.request("ui.clickWidget", json!({"id": "filter:label-green"}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let f = h.app.session.filter.clone();
|
||||
assert_eq!((f.label, f.labels), (None, vec![ColorLabel::Red, ColorLabel::Green]));
|
||||
// nothing passes both: nothing is lit, so a click shows just that label
|
||||
h.app.session.execute("library.filter", &json!({"label": "red", "labels": ["yellow"]})).unwrap();
|
||||
h.step();
|
||||
h.request("ui.clickWidget", json!({"id": "filter:label-green"}), t);
|
||||
let f = h.app.session.filter.clone();
|
||||
assert_eq!((f.label, f.labels), (None, vec![ColorLabel::Green]));
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// The filter bar drives `library.filter` and saves the view as a smart album.
|
||||
#[test]
|
||||
fn filter_bar_filters_and_saves_a_smart_album() {
|
||||
|
||||
@@ -1149,6 +1149,20 @@ mod tests {
|
||||
let no = format!(" {}", bool_text_in(Locale::De, false));
|
||||
assert!(rules_label(&rules, &catalog).ends_with(&no), "{}", rules_label(&rules, &catalog));
|
||||
}
|
||||
let both = lightcraft_catalog::Filter {
|
||||
label: Some(lightcraft_catalog::ColorLabel::Red),
|
||||
labels: vec![lightcraft_catalog::ColorLabel::Blue, lightcraft_catalog::ColorLabel::Red],
|
||||
..Default::default()
|
||||
};
|
||||
let name = |label| color_label(&app.session.catalog, label);
|
||||
let (red, blue, green) =
|
||||
(name(lightcraft_catalog::ColorLabel::Red), name(lightcraft_catalog::ColorLabel::Blue), name(lightcraft_catalog::ColorLabel::Green));
|
||||
let (label, and, or) = (tr("Label"), tr("and"), tr("or"));
|
||||
assert_eq!(filter_label(&both, &app.session.catalog), format!("{label}: {red}"));
|
||||
let both = lightcraft_catalog::Filter { labels: vec![lightcraft_catalog::ColorLabel::Blue], ..both };
|
||||
assert_eq!(filter_label(&both, &app.session.catalog), format!("{label}: {red} {and} {blue}"));
|
||||
let both = lightcraft_catalog::Filter { labels: vec![lightcraft_catalog::ColorLabel::Blue, lightcraft_catalog::ColorLabel::Green], ..both };
|
||||
assert_eq!(filter_label(&both, &app.session.catalog), format!("{label}: {red} {and} ({blue} {or} {green})"));
|
||||
let filter = lightcraft_catalog::Filter {
|
||||
labels: vec![lightcraft_catalog::ColorLabel::Red, lightcraft_catalog::ColorLabel::Blue],
|
||||
..Default::default()
|
||||
|
||||
@@ -41,10 +41,16 @@ pub(crate) fn display_label(chip: &FilterChip, filter: &lightcraft_catalog::Filt
|
||||
return format!("{}: {}", tr("Rules"), rules_label(rules, catalog));
|
||||
}
|
||||
if chip.clear.get("label").is_some() {
|
||||
let mut labels = filter.labels.clone();
|
||||
labels.extend(filter.label.filter(|label| !labels.contains(label)));
|
||||
let names = labels.iter().map(|label| crate::i18n::color_label(catalog, *label)).collect::<Vec<_>>();
|
||||
return format!("{}: {}", tr("Label"), names.join(&format!(" {} ", tr("or"))));
|
||||
let shown = filter.labels_shown().unwrap_or_default();
|
||||
let name = |label: &lightcraft_catalog::ColorLabel| crate::i18n::color_label(catalog, *label);
|
||||
let or = |labels: &[lightcraft_catalog::ColorLabel]| labels.iter().map(name).collect::<Vec<_>>().join(&format!(" {} ", tr("or")));
|
||||
let names = if shown.is_empty() {
|
||||
let plural = if filter.labels.len() > 1 { format!("({})", or(&filter.labels)) } else { or(&filter.labels) };
|
||||
format!("{} {} {plural}", filter.label.as_ref().map(name).unwrap_or_default(), tr("and"))
|
||||
} else {
|
||||
or(&shown)
|
||||
};
|
||||
return format!("{}: {names}", tr("Label"));
|
||||
}
|
||||
if chip.clear.get("dateFrom").is_some() || chip.clear.get("dateTo").is_some() {
|
||||
return match (&filter.date_from, &filter.date_to) {
|
||||
|
||||
@@ -158,13 +158,8 @@ 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);
|
||||
}
|
||||
// several labels can be on at once (any of them matches); lit are those `label` and `labels` both let through
|
||||
let chosen: Vec<ColorLabel> = f.labels_shown().unwrap_or_default();
|
||||
for l in ColorLabel::ALL {
|
||||
let on = chosen.contains(&l);
|
||||
let name = format!("{l:?}").to_lowercase();
|
||||
|
||||
Reference in New Issue
Block a user