mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
This commit is contained in:
@@ -1010,7 +1010,7 @@ pub fn label_param(l: Option<ColorLabel>) -> String {
|
||||
}
|
||||
|
||||
/// Stack badge at the cell's top-left: the photo count on a collapsed stack's top, `i/n` on the
|
||||
/// members of an expanded stack. Clicking it expands/collapses the stack.
|
||||
/// members of an expanded stack. Clicking or double-clicking it expands/collapses the stack.
|
||||
pub fn stack_badge(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, sid: lightcraft_catalog::StackId, pos: usize, r: Rect, square: bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.catalog.stack(sid) else { return };
|
||||
@@ -1034,7 +1034,8 @@ pub fn stack_badge(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, sid:
|
||||
}
|
||||
paint(p, Rect::from_min_size(br.min + vec2(6.0, 3.0), vec2(14.0, 14.0)), Icon::Stack, Color32::WHITE);
|
||||
p.galley(pos2(br.min.x + 24.0, br.center().y - g.size().y / 2.0), g, Color32::WHITE);
|
||||
if resp.clicked() {
|
||||
// a double-click (Lightroom's way) toggles once: its second click would undo the first
|
||||
if resp.clicked() && !resp.double_clicked() {
|
||||
let _ = app.run("stack.toggle", json!({"ids": [id.0]}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,3 +172,48 @@ fn an_unedited_raws_thumbnail_skips_the_sensor_decode() {
|
||||
assert!(!skips_the_real_render(false, true));
|
||||
assert!(!skips_the_real_render(false, false));
|
||||
}
|
||||
|
||||
/// Press and release the primary button at `at`, one frame apart from the next click (as a real
|
||||
/// double-click comes in: one click per frame).
|
||||
fn click_at(h: &mut Headless, at: egui::Pos2) {
|
||||
h.events.push(egui::Event::PointerMoved(at));
|
||||
h.events.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
|
||||
h.step();
|
||||
h.events.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
|
||||
/// Double-clicking a stack badge expands or collapses the stack, as in Lightroom (issue #2233): its
|
||||
/// second click used to undo the first, and the stack stayed as it was. A single click still toggles it.
|
||||
#[test]
|
||||
fn double_clicking_a_stack_badge_toggles_the_stack_once() {
|
||||
let mut h = grid(ViewMode::PhotoGrid);
|
||||
let top = PhotoId(2001);
|
||||
let sid = StackId(21);
|
||||
let collapsed = |h: &Headless| h.app.session.catalog.stack(sid).unwrap().collapsed;
|
||||
select(&mut h, top);
|
||||
let r = h.request("engine.execute", json!({"command": "stack.toggle", "params": {"ids": [top.0]}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert!(collapsed(&h));
|
||||
for want in [false, true] {
|
||||
let badge = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn").center();
|
||||
click_at(&mut h, badge);
|
||||
// the first click re-lays the grid; the second must still land on this badge, or the test
|
||||
// would pass without the fix (a missed second click toggles nothing)
|
||||
let moved = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn");
|
||||
assert!(moved.contains(badge), "the badge moved from under the pointer: {moved:?}");
|
||||
click_at(&mut h, badge);
|
||||
h.settle(SETTLE);
|
||||
assert_eq!(collapsed(&h), want, "a double-click toggles the stack once");
|
||||
assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "a double-click on the badge doesn't open the photo");
|
||||
// the next double-click is apart from this one
|
||||
for _ in 0..60 {
|
||||
h.step();
|
||||
}
|
||||
}
|
||||
let badge = widget(&h, &format!("stack:{}", top.0)).unwrap().center();
|
||||
click_at(&mut h, badge);
|
||||
h.settle(SETTLE);
|
||||
assert!(!collapsed(&h), "a single click toggles it");
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| 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 plus translucent colour on square-grid/filmstrip surrounds and photo-grid footers (photo pixels unchanged); colour-label toasts have pale matching backgrounds; 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; keywords reorganize by drag and drop in the Keyword List (LRC-LIB-KEYWORDS) |
|
||||
| LR-LIB-PEOPLE | People / faces | P2 | 🟡 | `cmd:faces.detect`, `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. Photo ▸ Detect Faces (`cmd:faces.detect`) finds faces with the YuNet detector (pure Rust, about 200 ms a photo, matches OpenCV's own YuNet on a 45-photo test set; the 232 KB model is a one-time download offered in the same step, never part of LightCraft) and adds them as unnamed boxes; names are suggested from the faces already named (recognition models are opt-in downloads, run on the CPU in a background thread; a face is named only when you confirm it); a person's page shows their cropped faces and the unnamed faces that look like them (LRC-LIB-PEOPLE), with a user-triggered ↑ action or targeted A key to name all shown suggestions at or above one score in one undo step; with recognition on, a background scan finds faces in photos that have none and embeds every face (one to three worker threads, raws through their embedded preview); an Unnamed faces section orders faces by likeness and names a selected group at once; no separated clusters, 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`, `cmd:photo.delete`, `crates/catalog/src/stacks.rs` | grid/filmstrip count badges, expand/collapse, auto-stack by capture time; delete / remove folder drop the photo from the stack so `i/n` updates (#1493); no visual-similarity auto-stack |
|
||||
| LR-LIB-STACK | Stacks | P1 | ✅ | `cmd:stack.group`, `cmd:stack.ungroup`, `cmd:stack.toggle`, `cmd:stack.setTop`, `cmd:stack.remove`, `cmd:stack.auto`, `cmd:photo.delete`, `crates/catalog/src/stacks.rs` | grid/filmstrip count badges, expand/collapse (a click or double-click on a grid badge toggles the stack once, #2233), auto-stack by capture time; delete / remove folder drop the photo from the stack so `i/n` updates (#1493); no visual-similarity auto-stack |
|
||||
| LR-LIB-VERSIONS | Versions | P1 | ✅ | `cmd:version.create` | see section M |
|
||||
| LR-LIB-DELETE | Delete / Recently Deleted | P0 | ✅ | `cmd:photo.delete`, `cmd:photo.restore`, `cmd:photo.deletePermanently` | Delete and Delete Permanently retain surviving selected photos; if none remain, select the next visible survivor (previous at the end) in the current source/filter/sort; permanent deletion removes catalog records, not files on disk; Restore and Delete Permanently in the Photo menu and the photo context menus for photos in Recently Deleted (before, the UI had no way to restore); importing a file that is in Recently Deleted again restores the photo with its edits, rating and albums (issues #298, #706: before, the import skipped it as "already in the library" while the grid showed it deleted) — the review dialog checks it, badges it "Will be restored", counts it as "in Recently Deleted" rather than "already in the library", and its choice Restore them / Import as new / Leave them starts on Restore; with Leave them such a file starts unchecked and Check All and the file-type toggles leave it out, but a click still checks it and it is restored (issue #1285: before, the click did nothing and the "In Recently Deleted" badge was drawn over the checkbox, which now sits above it); dropped files restore too ("Restored 1 photo from Recently Deleted"); a deleted photo matched by content whose file is gone is relinked to the imported file; Photo ▸ Empty Recently Deleted (`cmd:library.emptyRecentlyDeleted`, one undo step, shown while the trash is the source); import takes `onDeleted` restore (default) / skip / fresh for files in the trash (`cmd:library.import`; the report lists `restored`, and with skip flags the duplicate `existingDeleted`); browsing a folder in Local (`cmd:library.browse`), Auto Import and a Lightroom catalog import never restore a deleted photo; no confirmation dialog, no auto-purge after N days (needs a deleted-at date in the catalog); Delete asks first ("Move this photo to Recently Deleted?") from the key, the Photo menu and the photo context menu, on by default and switched off in Settings ▸ General (#1239: before, it was off by default and the context menu never asked); no auto-purge after N days (needs a deleted-at date in the catalog) |
|
||||
| LR-LIB-REMOVE-ALBUM | Remove from album | P0 | ✅ | `cmd:album.removePhotos`, `crates/engine/src/tests_album_remove.rs`, `crates/ui-egui/src/tests_album_remove.rs` | changes the album only: the photos stay in the library, their library folder and their folder on disk (issue #1805); `id` defaults to the album on screen; returns `{removed, album}`. In a normal album ⌫ removes from the album (as in Lightroom; no confirmation, one undo step), in a smart album it changes nothing and says to edit its rules |
|
||||
|
||||
Reference in New Issue
Block a user