Filters: open a library unfiltered, and show the active filters in the loupe
A date, keyword or person left over from the last session silently hid photos behind a small badge, so view.json no longer stores or restores the filter (the source, sort and selection still come back). In the loupe a small pill at the canvas' top left names the active filters (the filmstrip and Next / Previous follow them) and clears them on a click; it sits in the canvas margin, so the photo does not move. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e2e6db0f14
commit
e5848aa1b2
|
|
@ -4,7 +4,7 @@
|
|||
//! ```text
|
||||
//! LightCraft Library/
|
||||
//! catalog.snap catalog.log (lightcraft-catalog journal)
|
||||
//! presets.json view.json (user presets + favourites; last source/filter/sort/selection)
|
||||
//! presets.json view.json (user presets + favourites; last source/sort/selection)
|
||||
//! prefs.json (library preferences: XMP sidecars, import defaults, cache size, last export)
|
||||
//! thumbs/ (rendered thumbnail cache, safe to delete)
|
||||
//! Originals/ (photos imported with "copy into library")
|
||||
|
|
@ -102,7 +102,8 @@ struct PresetsFile {
|
|||
struct ViewFile {
|
||||
source: LibrarySource,
|
||||
browse: Option<crate::Browse>,
|
||||
filter: lightcraft_catalog::Filter,
|
||||
// No filter: a library opens unfiltered. A date, keyword or person left over from the last session
|
||||
// would silently hide photos, with only a small badge to say so.
|
||||
sort: lightcraft_catalog::Sort,
|
||||
selection: Selection,
|
||||
}
|
||||
|
|
@ -346,7 +347,6 @@ impl Session {
|
|||
if let Some(v) = settings.read::<ViewFile>(files.as_mut(), "view.json") {
|
||||
self.source = v.source;
|
||||
self.browse = v.browse;
|
||||
self.filter = v.filter;
|
||||
self.sort = v.sort;
|
||||
self.selection = v.selection;
|
||||
self.selection.ids.retain(|id| self.catalog.photo(*id).is_some());
|
||||
|
|
@ -494,17 +494,11 @@ impl Session {
|
|||
}
|
||||
|
||||
fn view_json(&self) -> Vec<u8> {
|
||||
let view = ViewFile {
|
||||
source: self.source,
|
||||
browse: self.browse.clone(),
|
||||
filter: self.filter.clone(),
|
||||
sort: self.sort,
|
||||
selection: self.selection.clone(),
|
||||
};
|
||||
let view = ViewFile { source: self.source, browse: self.browse.clone(), sort: self.sort, selection: self.selection.clone() };
|
||||
serde_json::to_vec_pretty(&view).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Save the view state (source, filter, sort, selection) if it changed since it was last
|
||||
/// Save the view state (source, sort, selection) if it changed since it was last
|
||||
/// written. Native hosts get this from [`Session::close_library`]; the browser host calls it
|
||||
/// periodically, since a tab can be closed without notice.
|
||||
pub fn save_view(&mut self) {
|
||||
|
|
|
|||
|
|
@ -201,6 +201,25 @@ fn album_source_reports_in_library_state_and_persists_the_view() {
|
|||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A library opens unfiltered: the source and sort come back, a filter from the last session does not
|
||||
/// (it would hide photos behind a small badge).
|
||||
#[test]
|
||||
fn filters_are_not_restored_when_a_library_reopens() {
|
||||
let dir = temp_dir("filter-not-restored");
|
||||
let mut s = open(&dir);
|
||||
let all = s.visible_cloned().len();
|
||||
s.execute("library.sort", &json!({"key": "fileName"})).unwrap();
|
||||
s.execute("library.filter", &json!({"rating": 5, "date": "2026-01-16", "person": "Jane Doe"})).unwrap();
|
||||
assert!(s.visible_cloned().len() < all);
|
||||
s.close_library().unwrap();
|
||||
drop(s);
|
||||
let mut s2 = open(&dir);
|
||||
assert_eq!(s2.filter, lightcraft_catalog::Filter::default());
|
||||
assert_eq!(s2.visible_cloned().len(), all);
|
||||
assert_eq!(serde_json::to_value(s2.sort).unwrap()["key"], "fileName", "the rest of the view is restored");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_chips_clear_one_filter_and_total_ignores_the_filter() {
|
||||
let dir = temp_dir("chips");
|
||||
|
|
|
|||
|
|
@ -333,6 +333,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let map = CanvasMap::new(&frame, img_rect);
|
||||
let resp = ui.interact(canvas, egui::Id::new("loupe"), Sense::click_and_drag());
|
||||
info_overlay(app, &p, canvas, &photo);
|
||||
if !fullscreen {
|
||||
filter_pill(app, ui, canvas);
|
||||
}
|
||||
if app.ui.face_boxes
|
||||
&& let Some(index) = region_overlay(ui, &p, &map, &photo)
|
||||
{
|
||||
|
|
@ -476,6 +479,34 @@ fn region_overlay(ui: &egui::Ui, p: &egui::Painter, map: &CanvasMap, photo: &lig
|
|||
remove
|
||||
}
|
||||
|
||||
/// A small pill at the loupe's top left naming the active filters: the filmstrip and Next / Previous
|
||||
/// follow them, so a filter must never be invisible here. A click clears them all. It sits in the
|
||||
/// canvas margin, so the photo does not move.
|
||||
fn filter_pill(app: &mut LightcraftApp, ui: &mut egui::Ui, canvas: Rect) {
|
||||
let chips = lightcraft_engine::filter_chips(&app.session.filter, &app.session.catalog);
|
||||
if chips.is_empty() {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let p = ui.painter_at(canvas);
|
||||
let label = chips.iter().map(|c| c.label.as_str()).collect::<Vec<_>>().join(" · ");
|
||||
let g = p.layout_no_wrap(format!("Filtered: {label}"), t.font(12.0), Color32::from_gray(225));
|
||||
let w = (g.size().x + 40.0).min((canvas.width() - 24.0).max(60.0));
|
||||
let r = Rect::from_min_size(canvas.min + vec2(12.0, 3.0), vec2(w, 20.0));
|
||||
let resp = ui.interact(r, egui::Id::new("loupe-filter-pill"), Sense::click());
|
||||
register(ui.ctx(), "loupe:filters", r);
|
||||
p.rect_filled(r, 10.0, Color32::from_black_alpha(if resp.hovered() { 225 } else { 185 }));
|
||||
let text_clip = Rect::from_min_max(r.min, pos2(r.right() - 24.0, r.max.y));
|
||||
p.with_clip_rect(text_clip).galley(pos2(r.left() + 10.0, r.center().y - g.size().y / 2.0), g, Color32::from_gray(225));
|
||||
let (c, m) = (pos2(r.right() - 13.0, r.center().y), 3.0);
|
||||
let cross = Stroke::new(1.3, if resp.hovered() { Color32::WHITE } else { Color32::from_gray(190) });
|
||||
p.line_segment([c - vec2(m, m), c + vec2(m, m)], cross);
|
||||
p.line_segment([c - vec2(m, -m), c + vec2(m, -m)], cross);
|
||||
if resp.on_hover_text("Click to clear these filters").clicked() {
|
||||
let _ = app.run("library.clearFilter", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw shown from its embedded JPEG (issue #10): a pill at the canvas' top centre saying so;
|
||||
/// hovering it explains (registered as `notice:previewOnly:loupe`).
|
||||
fn preview_only_pill(ui: &mut egui::Ui, canvas: Rect, reason: &str) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue