Files
DarkRoom/ui/dr-ui/src/collections_ui/mod.rs
T
dtourolle 220e9af222 Add the duplicate originals review, from the sidebar and from Settings
"Duplicate originals" appears under the trash in the collections
sidebar while the catalog holds any, and Settings says how many there
are beside the other whole-library passes. Both open one page: every
group with its picture and paths, the copy that stays (tap another path
to change it), a per-group Include box, what the survivor will gain and
any flag, label or face conflict, and why a group was skipped.

The summary is the dry run -- "N groups, M files to trash, K skipped" --
and nothing moves until "Check" has read the copies and "Move M copies
to trash" is pressed. Both run on workers with progress on the page, in
the activity register and, for the move, on the library status line;
Stop ends a job between groups. When it ends the grid, the sidebar and
the trash are refreshed and the survivors' judgements are written to
their sidecars and XMP the way a rating keystroke writes them.

The page is paginated at 30 groups, so a redraw decodes 30 thumbnails
and previews 30 merges whatever the size of the library. Back and
Escape leave it like its own Back button.
2026-09-26 07:19:13 -04:00

76 lines
3.2 KiB
Rust

//! TRACES: FR-CAT-7 | FR-UI-5 | NFR-P9
//! The collections sidebar, grid selection, and the drag between them.
//!
//! `dr_catalog::collections` owns the data rules — hierarchy, membership,
//! revisions, cycles. This module owns the *interaction*: what is selected,
//! what a drag is carrying, and where a release lands.
//!
//! # What this module does and does not own
//!
//! Slint's `DragArea`/`DropArea` own the *gesture* — pointer capture, the
//! threshold separating a click from a drag, arbitration against the grid's
//! Flickable, the image under the cursor, and hit-testing the release. So the
//! drop arrives already addressed to a collection, and nothing here tracks
//! pointer positions or guesses a target.
//!
//! What is left here is what Slint cannot know:
//!
//! - **the payload** — which images the drag carries, built from the selection
//! at the moment the drag starts;
//! - **the spring** — a dwell timer that opens a collapsed collection so a
//! nested child can be reached mid-drag, and closes again what the drag only
//! passed over.
//!
//! An earlier version hand-rolled the whole gesture on `TouchArea` and did not
//! work, for a reason worth keeping: an interactive `Flickable` claims any drag
//! starting inside it for scrolling and cancels the child TouchArea's press, so
//! the drag could never leave the grid.
//!
//! # Selection
//!
//! Selection is by **catalog image id**, never by row index. The grid is a
//! window over the catalog (FR-CAT-4) and scrubbing replaces every row, so an
//! index-based selection would silently come to mean forty different
//! photographs after a scrub. Ids survive that; they also survive a rescan.
//!
//! # Split into one module per area (docs/dev/code-health.md CH-1)
//!
//! `controller` holds the running state and the pure drop/delete/release
//! decisions; `press` the click-and-release selection policy; `tree_sync`
//! rebuilding the sidebar and pushing catalog state into the grid; `drag` the
//! cursor bitmap and the spring; `trash` the soft delete; `wiring_grid` and
//! `wiring_tree` the callbacks, split in two because together they were the
//! largest single piece; and `rename_menu` creating, naming and the row's
//! context menu. `mod.rs` re-exports what the rest of the crate calls by
//! `collections_ui::` path, so nothing outside this module needs to change.
mod controller;
mod drag;
mod press;
mod rename_menu;
mod trash;
mod tree_sync;
mod wiring_grid;
mod wiring_tree;
pub use controller::CollectionsController;
pub use press::{cancel_press, commit_press, select_row};
pub use trash::refresh_trash;
pub use tree_sync::{refresh_tree, sync_badges, sync_selection};
pub use wiring_grid::wire;
/// Test-only helpers shared by more than one of this module's submodules.
///
/// `ids` was defined once in the pre-split file and called from tests all
/// over it — both the pure `decide_drop` tests in `controller` and the press
/// tests in `press`. Splitting the tests with the code they exercise left it
/// needed in both, so it lives here once instead of being copied.
#[cfg(test)]
pub(super) mod test_support {
use dr_types::ImageId;
pub(super) fn ids(n: u64) -> Vec<ImageId> {
(1..=n).map(ImageId).collect()
}
}