feat(dupe): add Dupe Grid view to Duplicates workbench (#138) #140

Merged
ValleyGeek merged 8 commits from 0.2.4/issue-138-dupe-grid-view into main 2026-06-25 06:05:48 +00:00
Member

Summary

  • Adds a Dupe Grid view to the Duplicates workbench: one thumbnail per duplicate group (left of the Multi/Single buttons), so groups can be browsed visually instead of one at a time.
  • Thumbnail is the first non-deleted image in the group (or the first image if every member is marked DELETE); a small icon shows the group's review state. Hide Deleted hides a group only once every member is marked DELETE.
  • E and double-click bounce between whichever two of Dupe Grid / Multi / Single were used most recently (defaulting to Dupe Grid ↔ Multi); W/S move the grid selection instead of switching groups while Dupe Grid is active; Delete marks the whole selected group DELETE.
  • Includes 4 perf/UX fixes found during real-world testing at scale (2,200 images / 270 duplicate groups):
    • Dupe Grid reload/select/delete were slow — rebuilding every group's thumbnail row issued one DB query per group instead of one batched query (WorkingDB.get_dupe_group_representatives), and every click also reloaded the invisible Multi view.
    • Selecting a group no longer recenters the grid; only entering/returning to it does.
    • W/S stopped working if Single was opened directly from Dupe Grid (skipping Multi).
    • Double-clicking an image in Multi always opened Single instead of bouncing to whichever of Dupe Grid/Single was used most recently, like every other double-click in the workbench.

Bumps VERSION to 0.3.0, starting the v0.3.x tranche (issues #138, #136, #113, #131, #116 are stacked on this branch in that order).

Test plan

  • pytest — full suite passes (629 passed, 1 skipped)
  • ruff check / ruff format --check pass
  • scripts/smoke-ui-exit.sh — clean Qt teardown
  • New regression tests for the batched representative lookup, the no-recenter/ensure-visible scroll behavior, the W/S re-sync after Grid→Single, and the Multi double-click bounce target
  • Manually exercised in a real directory with 2,200 images / 270 duplicate groups
## Summary - Adds a **Dupe Grid** view to the Duplicates workbench: one thumbnail per duplicate group (left of the Multi/Single buttons), so groups can be browsed visually instead of one at a time. - Thumbnail is the first non-deleted image in the group (or the first image if every member is marked DELETE); a small icon shows the group's review state. Hide Deleted hides a group only once every member is marked DELETE. - `E` and double-click bounce between whichever two of Dupe Grid / Multi / Single were used most recently (defaulting to Dupe Grid ↔ Multi); `W`/`S` move the grid selection instead of switching groups while Dupe Grid is active; `Delete` marks the whole selected group DELETE. - Includes 4 perf/UX fixes found during real-world testing at scale (2,200 images / 270 duplicate groups): - Dupe Grid reload/select/delete were slow — rebuilding every group's thumbnail row issued one DB query per group instead of one batched query (`WorkingDB.get_dupe_group_representatives`), and every click also reloaded the invisible Multi view. - Selecting a group no longer recenters the grid; only entering/returning to it does. - `W`/`S` stopped working if Single was opened directly from Dupe Grid (skipping Multi). - Double-clicking an image in Multi always opened Single instead of bouncing to whichever of Dupe Grid/Single was used most recently, like every other double-click in the workbench. Bumps VERSION to 0.3.0, starting the v0.3.x tranche (issues #138, #136, #113, #131, #116 are stacked on this branch in that order). ## Test plan - [x] `pytest` — full suite passes (629 passed, 1 skipped) - [x] `ruff check` / `ruff format --check` pass - [x] `scripts/smoke-ui-exit.sh` — clean Qt teardown - [x] New regression tests for the batched representative lookup, the no-recenter/ensure-visible scroll behavior, the W/S re-sync after Grid→Single, and the Multi double-click bounce target - [x] Manually exercised in a real directory with 2,200 images / 270 duplicate groups
Adds a "Dupe Grid" button (left of Multi) that switches the Duplicates
workbench into a thumbnail-per-group grid, reusing the main grid's
IconMode/QListView/delegate pattern with a new row-per-group model and
a review-state icon overlay (all_delete / keep_one / viewed).

- Thumbnail = first non-deleted image in the group, else the first image.
- Hide Deleted drops a group from this view only when every member is
  deleted; the toolbox group list is unaffected.
- Tag filters reuse the existing aggregate (any-member-matches) dupe
  tag summary query, so no separate aggregation was needed.
- 'e'/double-click now bounces between the last two distinct dupe
  views used (default Grid<->Multi), generalizing the old hardcoded
  Multi<->Single toggle.
- W/S move the grid selection (via the existing GridWasdNavFilter);
  the dupe_group_prev/next shortcuts are disabled while Grid is active
  the same way they're already scoped to the dupes workbench.
- Delete marks the whole group DELETE, reusing the Delete Group
  toolbar action's undo/advance/refresh logic.
- Grid has no thumbnail-size slider of its own; size mirrors the main
  grid's slider via _apply_display_size().

Threading notes: no new worker. The grid renders from the same
plain-dict image records already produced by existing DB/enrichment
code on the main thread; no QWidget/QPixmap access happens off it.
Unit tests for the representative-thumbnail/hide-deleted helpers in
core/dupe_group_grid.py and the shared review-status helper, plus
widget-level tests for DupeGroupGrid (rows, selection, double-click,
Delete key) and DupeWorkbench's new view-toggle history/state machine.
docs: PR-prep — bump to 0.3.0 for issue #138 Dupe Grid view
All checks were successful
CI / lint (push) Successful in 8s
CI / test (push) Successful in 1m54s
CI / appimage (push) Successful in 8m15s
CI / build-appimage (push) Successful in 0s
CI / windows-exe (push) Successful in 11m28s
CI / build-windows-exe (push) Successful in 0s
6ef6834577
Starts the v0.3.0 tranche (new capability: Dupe Grid). Updates the
bundled USER_GUIDE.md, DESIGN.md, and TECHNICAL_OUTLINE.md for the new
view, and adds the CHANGELOG entry.
perf(dupe): batch Dupe Grid thumbnail lookups, skip wasted Multi refresh
Some checks failed
CI / lint (push) Successful in 14s
CI / test (push) Successful in 2m40s
CI / build-appimage (push) Has been cancelled
CI / build-windows-exe (push) Has been cancelled
CI / windows-exe (push) Has been cancelled
CI / appimage (push) Has been cancelled
f187b8963e
Real-world testing at scale (2,200 images / 270 duplicate groups, issue #138
follow-up) surfaced two dominant costs:

- _refresh_dupe_group_grid() called get_image_dicts_for_dupe_tag() once per
  visible group -- each opening its own SQLite connection and fetching every
  member's full row including the thumbnail BLOB, just to pick one
  representative. With hundreds of groups this made Dupe Grid reload
  (switching back from Multi/Single) and group deletion (which re-syncs the
  grid afterward) noticeably slow. Replaced with WorkingDB.get_dupe_group_representatives(),
  a single batched round trip (~12x faster on a synthetic 270-group/2,160-image
  benchmark). The now-superseded pure-Python picking helpers in
  core/dupe_group_grid.py are removed; their semantics are folded into the SQL
  and covered by new WorkingDB tests.

- _apply_dupe_group_filter() unconditionally refreshed the Multi view's images
  (plus adjacent-group preload) on every call, including every Dupe Grid
  selection change -- even though Multi wasn't on screen. Skipped while Dupe
  Grid is active; _on_dupe_multi_requested() already loads on demand the
  moment Multi is actually shown. _update_image_count_status() now reads the
  active group's member count from the already-cached tag summary while Grid
  is active, instead of the (no longer eagerly refreshed) Multi widget.

Threading notes: no worker/threading changes -- both fixes are main-thread
DB-query and UI-refresh reductions.
fix(dupe): stop Dupe Grid from recentering, fix W/S after Grid->Single
All checks were successful
CI / lint (push) Successful in 13s
CI / test (push) Successful in 2m47s
CI / windows-exe (push) Successful in 11m20s
CI / build-windows-exe (push) Successful in 0s
CI / appimage (push) Successful in 15m23s
CI / build-appimage (push) Successful in 0s
d699c138bf
Real-world testing at scale (issue #138 follow-up) surfaced two more UX bugs:

- DupeGroupGrid.select_tag() unconditionally scrolled to dead-center on every
  call, including routine resyncs after every click/mutation while the grid
  was already on screen -- jarring, and not what the issue asked for. It now
  defaults to no scroll at all; callers entering/returning to the view (or
  syncing a toolbox-driven selection that may be off-screen) opt in via a new
  ensure_visible=True, which uses the minimal EnsureVisible scroll hint
  instead of PositionAtCenter so the target lands at whichever edge it
  approaches from rather than jumping to center.

- _on_dupe_single_requested() (double-click/'e' from Dupe Grid straight into
  Single) never re-synced the W/S "previous/next group" shortcuts, which Dupe
  Grid scopes off so it can use raw W/S for its own up/down navigation. They
  stayed disabled until the user happened to pass through Multi, which does
  re-sync them. Fixed by calling _sync_dupe_nav_shortcuts_enabled() there too.

Threading notes: no worker/threading changes -- both fixes are main-thread
signal/shortcut wiring.
VERSION stays at 0.3.0 -- these are fixes to the not-yet-released Dupe Grid
feature already documented under that section, not a further bump.
fix(dupe): double-clicking an image in Multi always opened Single, not the last dupe view
All checks were successful
CI / lint (push) Successful in 7s
CI / test (push) Successful in 2m0s
CI / appimage (push) Successful in 8m36s
CI / build-appimage (push) Successful in 0s
CI / windows-exe (push) Successful in 12m51s
CI / build-windows-exe (push) Successful in 0s
3f90fd6e05
Double-clicking an image tile inside the Multi comparison grid hardcoded
set_view_mode_single() + open the viewer, bypassing the Grid<->Multi<->Single
"bounce to whichever of the last two distinct views isn't active" logic that
double-click/'e' already use correctly everywhere else (Dupe Grid and Single
both call toggle_target_view() for the same gesture). So if the user's last
two views were Grid and Multi, double-clicking an image in Multi should land
back in Grid, not Single.

_on_dupe_multi_open_single() now checks toggle_target_view() first: if it
resolves to "grid", switches to Dupe Grid for the active group instead of
opening the viewer; otherwise keeps the existing Single-viewer behavior
(including which image was clicked).

Threading notes: no change -- still a main-thread signal handler from
dupe_multi_grid's per-tile double-click signal.
ValleyGeek deleted branch 0.2.4/issue-138-dupe-grid-view 2026-06-25 06:05:48 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ai-collab/bulk-image-organizer!140
No description provided.