diff --git a/core/dr-catalog/src/collections.rs b/core/dr-catalog/src/collections.rs index b34105a..e68c868 100644 --- a/core/dr-catalog/src/collections.rs +++ b/core/dr-catalog/src/collections.rs @@ -552,6 +552,44 @@ pub fn kind(conn: &Connection, id: CollectionId) -> Result Result, CatalogError> { + Ok(conn + .query_row( + "SELECT uuid FROM collections WHERE id = ?1 AND deleted = 0", + [id.0 as i64], + |r| r.get::<_, String>(0), + ) + .optional()?) +} + +/// TRACES: FR-UI-8 +/// The local id of a collection, from the name every device knows it by. +/// +/// `None` where this device has never heard of it, or has deleted it — a place +/// recorded on the tablet inside a collection this machine has not yet merged. +/// The caller falls back to the whole library rather than to an empty grid. +pub fn id_for_uuid(conn: &Connection, uuid: &str) -> Result, CatalogError> { + Ok(conn + .query_row( + "SELECT id FROM collections WHERE uuid = ?1 AND deleted = 0", + [uuid], + |r| r.get::<_, i64>(0), + ) + .optional()? + .map(|id| CollectionId(id as u64))) +} + /// A collection and everything beneath it, including itself. /// /// Used for cycle checks and for scoping the grid to a parent: selecting a diff --git a/core/dr-types/src/lib.rs b/core/dr-types/src/lib.rs index 505f930..9839b3d 100644 --- a/core/dr-types/src/lib.rs +++ b/core/dr-types/src/lib.rs @@ -9,11 +9,13 @@ use std::fmt; use std::ops::Range; pub mod colour; +pub mod place; pub mod selector; pub mod settings; pub mod time; pub use colour::{Chromaticities, Transfer}; +pub use place::{Place, PlaceScope, Screen, StoredFilter}; pub use selector::{ColourLabel, DateSelector, FlagState, Selector, Tier}; pub use settings::{ CacheSettings, CollisionPolicy, ColourSpace, DevelopSettings, ExportFormat, ExportSettings, diff --git a/core/dr-types/src/place.rs b/core/dr-types/src/place.rs new file mode 100644 index 0000000..390cd0a --- /dev/null +++ b/core/dr-types/src/place.rs @@ -0,0 +1,166 @@ +//! TRACES: FR-UI-8 | FR-NC-3 | FR-CAT-6 +//! Where the photographer was, so the next launch starts there. +//! +//! The record only. Reading and writing it is `dr_ui::place`'s job, for the +//! reason this crate's manifest gives about `settings.json`: a serialiser here +//! would be paid for by every crate in `core/`. +//! +//! # Why this is not a setting +//! +//! [`crate::settings`] holds preferences: how big a cache may grow, how +//! hard the grouping pass tries. Those are answers to questions about *this +//! device*, and its module doc says so. A place is not one of those. It is a +//! fact about a session — the collection that was open, the filter that was +//! narrowing it, the photograph on screen — and the whole point of recording it +//! is that it should follow the photographer to the tablet. Two records, two +//! lifetimes, and folding them together would give the sync one file holding +//! both a cache budget that must not travel and a position that must. +//! +//! # What a place is addressed by, and why it is not an ordinal +//! +//! A row number would be the obvious thing to store and it is worthless. The +//! grid is a window over a query whose ordering depends on the scope, the +//! rating filter and which bursts are folded up, so "row 8,412" names a +//! different photograph after any of those change — and on a second device, +//! which has its own catalog with its own `images.id` values, it names a +//! different photograph immediately. The same goes for `collections.id`, which +//! the schema itself calls local and colliding. +//! +//! So a place names things every device agrees on: +//! +//! - the photograph by its **remote path**, which is what the catalog's +//! `UNIQUE(root_id, source_ref)` is built on and what the sidecar, the +//! thumbnail store and the develop view all key on already; +//! - the collection by its **UUID**, which is what a catalog merge keys on; +//! - and, as a fallback, **when the photograph was taken**, so a place whose +//! photograph has since been moved or deleted still lands in the right week +//! instead of at the top of the library. +//! +//! # Last writer wins, and why that is enough +//! +//! Every record carries the instant it was made and the device that made it. +//! There is no merge to do: two devices cannot both be the place you are, and +//! the interesting question — "which of these did I do most recently" — is +//! answered by a timestamp. `device` is not used to resolve that. It is here so +//! a human reading the file, or a log line explaining why the grid moved, can +//! say *which* machine put it there. +//! +//! A clock-skewed device can therefore win an exchange it should have lost. The +//! cost is that the grid opens somewhere other than where you left it, which is +//! the state the application was in before any of this existed — so it is worth +//! a timestamp rather than the revision counter the collections merge needs, +//! where the same skew would silently discard an edit. +//! +//! # Every field is optional in practice +//! +//! `#[serde(default)]` throughout and no `version` field, the same shape and for +//! the same reason [`crate::settings`] gives: an older file is missing +//! fields rather than wrong about them, and a newer file read by an older build +//! ignores what it does not know. A place is also, unlike a setting, entirely +//! disposable — the worst a malformed one can do is open the library at the top. + +use serde::{Deserialize, Serialize}; + +use crate::FlagState; + +/// Which view the photographer was in. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Screen { + /// The grid. The default, and where an unreadable or partial record lands: + /// arriving in the library is never wrong, where arriving in develop on a + /// photograph that could not be resolved would be an empty canvas. + #[default] + Library, + Develop, +} + +/// What the grid was narrowed to. +/// +/// The trash is a variant rather than a collection with a reserved UUID, for +/// the reason `dr_ui::collections_ui` gives: it is not +/// a collection, and its query inverts the predicate every other view applies. +/// A sentinel id would put that inversion somewhere nothing reading a scope +/// expects it. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum PlaceScope { + /// The whole library. + #[default] + Library, + /// TRACES: FR-CAT-15 + Trash, + /// One collection, by the UUID a merge keys on — never by `collections.id`, + /// which the schema calls local and colliding across devices. + Collection { uuid: String }, +} + +/// The rating filter, in a shape that can be written down. +/// +/// A projection of [`RatingFilter`] rather than `Serialize` on the type itself. +/// That type is the one every query path threads and it gains terms as the grid +/// gains ways to be narrowed; deriving on it would make the on-disk format +/// hostage to a struct whose job is to describe a SQL predicate, and a term +/// added for a query would silently become part of a record two devices +/// exchange. Here the two are converted explicitly, so adding a term is a +/// decision about whether it travels. +/// +/// `people` travels as ids, which are local like every other integer key here — +/// a person on this device is a different row number on the tablet. It is kept +/// anyway because dropping it would restore a place into a grid narrowed to +/// nobody, showing far *more* than the user left rather than less; the ids are +/// simply ignored where they do not resolve. Making people travel properly +/// needs a stable person identity, which the catalog does not yet have. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct StoredFilter { + pub min_rating: u8, + pub unjudged: bool, + pub flag: Option, + pub local_only: bool, + pub captured_from: Option, + pub captured_to: Option, + pub people: Vec, + /// Whether `people` is an intersection. A bool rather than the enum, + /// because `PeopleMode` lives in `library.rs` beside the query it shapes and + /// does not derive serde — and "all of them" is the only thing the second + /// variant means. + pub people_all: bool, +} + +/// Where the photographer was, at the moment they were there. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Place { + /// When this was recorded, in UTC seconds. The only thing that decides + /// which of two records wins — see the module doc. + pub at: i64, + /// Which device recorded it, for logs and for a human reading the file. + /// Never consulted to resolve a conflict. + pub device: String, + pub screen: Screen, + pub scope: PlaceScope, + /// The remote path of the photograph that was open, or — in the grid — the + /// first one visible. Empty means the top of whatever the scope is, which + /// is what a record made before anything was on screen says. + pub path: String, + /// When that photograph was taken, so a place survives its subject being + /// moved, renamed or deleted. `None` where the catalog had no date for it + /// yet, which is common early in a scan. + pub captured_at: Option, + pub filter: StoredFilter, +} + +impl Place { + /// Whether this record should replace `other`. + /// + /// Strictly newer, so a re-read of the same record does not count as a + /// change and an exchange between two idle devices settles instead of + /// ping-ponging. A record with no counterpart always wins. + pub fn supersedes(&self, other: Option<&Place>) -> bool { + match other { + Some(o) => self.at > o.at, + None => true, + } + } +} diff --git a/docs/requirements.md b/docs/requirements.md index efed529..5db8d96 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -540,6 +540,30 @@ gradient placement, brush strokes (`WidgetKind` in FR-DEV-3a) — size their int the active modality while rendering identically. A crop handle drawn at 8px may carry a 44pt touch region. +**FR-UI-8 — Resumed place.** The application shall remember where the photographer was and return +them to it. "Where" is the view (grid or develop), the scope (whole library, a collection, or the +trash), the rating filter narrowing it, and the photograph on screen — the open one in develop, the +first visible one in the grid. It shall be restored on launch, and preserved across every transition +between screens within a session: leaving the grid for Settings, Import, People or the develop view +and returning shall land where it was left, never at the top of the library. + +*A place is addressed by what every device agrees on.* The photograph by its remote path, the +collection by its UUID; never by a grid ordinal, `images.id` or `collections.id`, all of which are +local to one catalog and to one ordering. Where the ordinal is needed it shall be computed through +the same ordering the grid draws with, so that a restored position names the photograph the +photographer actually left. Capture time is the permitted fallback when the photograph is gone. + +*A place travels.* One record per library is exchanged through the same derived folder as the +thumbnail shards and the catalog snapshot (FR-NC-3), so that a session begun on one device can be +continued on another. The newer of two records wins; there is nothing to merge, because two devices +cannot both be where the photographer is. + +*And it is always advisory.* A place that cannot be read, names a collection this device has not +merged, or points at a photograph that has since been deleted shall degrade to the nearest sensible +position and never to an error, an empty view, or a refusal to start. A record arriving from another +device shall not be applied once the photographer has begun working in this session — it is a +handover, not an interruption. + ### 3.6 Export **FR-EXP-1 — Formats.** Export to JPEG, PNG, and TIFF (8 and 16-bit). Quality, chroma subsampling, diff --git a/docs/traceability.md b/docs/traceability.md index f3e3c5a..58e7d92 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| | Source files scanned | 333 | -| TRACES tags found | 1066 | +| TRACES tags found | 1062 | | Requirements defined | 180 | | Requirements covered | 127 | | **Coverage** | **70.6%** (127/180) | @@ -37,16 +37,16 @@ _None._ | FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:206`](../core/dr-types/src/settings.rs#L206), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1261`](../ui/dr-ui/src/lib.rs#L1261), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:1244`](../ui/dr-ui/ui/library.slint#L1244), [`ui/dr-ui/ui/library.slint:974`](../ui/dr-ui/ui/library.slint#L974) | | FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1261`](../ui/dr-ui/src/lib.rs#L1261), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:2511`](../ui/dr-ui/src/library.rs#L2511), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119) | -| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:753`](../core/dr-catalog/src/schema.rs#L753), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:563`](../core/dr-sync/src/scan.rs#L563), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1283`](../ui/dr-ui/src/collections_ui.rs#L1283), [`ui/dr-ui/src/collections_ui.rs:2189`](../ui/dr-ui/src/collections_ui.rs#L2189), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:4137`](../ui/dr-ui/src/library.rs#L4137), [`ui/dr-ui/src/library.rs:4171`](../ui/dr-ui/src/library.rs#L4171), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:838`](../ui/dr-ui/src/library_ui.rs#L838), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:621`](../ui/dr-ui/ui/collections.slint#L621) | +| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:753`](../core/dr-catalog/src/schema.rs#L753), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:563`](../core/dr-sync/src/scan.rs#L563), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1283`](../ui/dr-ui/src/collections_ui.rs#L1283), [`ui/dr-ui/src/collections_ui.rs:2189`](../ui/dr-ui/src/collections_ui.rs#L2189), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:4137`](../ui/dr-ui/src/library.rs#L4137), [`ui/dr-ui/src/library.rs:4171`](../ui/dr-ui/src/library.rs#L4171), [`ui/dr-ui/src/library_ui.rs:235`](../ui/dr-ui/src/library_ui.rs#L235), [`ui/dr-ui/src/library_ui.rs:834`](../ui/dr-ui/src/library_ui.rs#L834), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:621`](../ui/dr-ui/ui/collections.slint#L621) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:56`](../core/dr-types/src/lib.rs#L56), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) | | FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | -| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2984`](../ui/dr-ui/src/library.rs#L2984), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4221`](../ui/dr-ui/src/library_ui.rs#L4221), [`ui/dr-ui/src/library_ui.rs:5382`](../ui/dr-ui/src/library_ui.rs#L5382), [`ui/dr-ui/ui/app.slint:428`](../ui/dr-ui/ui/app.slint#L428), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2984`](../ui/dr-ui/src/library.rs#L2984), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4112`](../ui/dr-ui/src/library_ui.rs#L4112), [`ui/dr-ui/src/library_ui.rs:5611`](../ui/dr-ui/src/library_ui.rs#L5611), [`ui/dr-ui/ui/app.slint:428`](../ui/dr-ui/ui/app.slint#L428), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:222`](../ui/dr-ui/src/library.rs#L222), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) | -| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1171`](../core/dr-catalog/src/schema.rs#L1171), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/collections_ui.rs:1579`](../ui/dr-ui/src/collections_ui.rs#L1579), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1734`](../ui/dr-ui/src/collections_ui.rs#L1734), [`ui/dr-ui/src/collections_ui.rs:238`](../ui/dr-ui/src/collections_ui.rs#L238), [`ui/dr-ui/src/collections_ui.rs:335`](../ui/dr-ui/src/collections_ui.rs#L335), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4185`](../ui/dr-ui/src/library.rs#L4185), [`ui/dr-ui/src/library_ui.rs:713`](../ui/dr-ui/src/library_ui.rs#L713), [`ui/dr-ui/src/library_ui.rs:7330`](../ui/dr-ui/src/library_ui.rs#L7330), [`ui/dr-ui/src/library_ui.rs:7341`](../ui/dr-ui/src/library_ui.rs#L7341), [`ui/dr-ui/src/library_ui.rs:7354`](../ui/dr-ui/src/library_ui.rs#L7354), [`ui/dr-ui/src/library_ui.rs:7369`](../ui/dr-ui/src/library_ui.rs#L7369), [`ui/dr-ui/src/library_ui.rs:7378`](../ui/dr-ui/src/library_ui.rs#L7378), [`ui/dr-ui/ui/app.slint:349`](../ui/dr-ui/ui/app.slint#L349), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/library.slint:1350`](../ui/dr-ui/ui/library.slint#L1350), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1427`](../ui/dr-ui/ui/library.slint#L1427), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101) | -| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:4661`](../ui/dr-ui/src/library.rs#L4661), [`ui/dr-ui/src/library_ui.rs:389`](../ui/dr-ui/src/library_ui.rs#L389), [`ui/dr-ui/src/library_ui.rs:470`](../ui/dr-ui/src/library_ui.rs#L470), [`ui/dr-ui/src/library_ui.rs:6060`](../ui/dr-ui/src/library_ui.rs#L6060), [`ui/dr-ui/src/library_ui.rs:6113`](../ui/dr-ui/src/library_ui.rs#L6113), [`ui/dr-ui/src/library_ui.rs:7470`](../ui/dr-ui/src/library_ui.rs#L7470), [`ui/dr-ui/ui/app.slint:361`](../ui/dr-ui/ui/app.slint#L361), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/app.slint:874`](../ui/dr-ui/ui/app.slint#L874), [`ui/dr-ui/ui/library.slint:111`](../ui/dr-ui/ui/library.slint#L111), [`ui/dr-ui/ui/library.slint:1537`](../ui/dr-ui/ui/library.slint#L1537), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | -| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1073`](../ui/dr-ui/src/collections_ui.rs#L1073), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1598`](../ui/dr-ui/src/collections_ui.rs#L1598), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1913`](../ui/dr-ui/src/collections_ui.rs#L1913), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:965`](../ui/dr-ui/src/collections_ui.rs#L965), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4466`](../ui/dr-ui/src/library.rs#L4466), [`ui/dr-ui/src/library.rs:4492`](../ui/dr-ui/src/library.rs#L4492), [`ui/dr-ui/src/library.rs:4547`](../ui/dr-ui/src/library.rs#L4547), [`ui/dr-ui/src/library_ui.rs:2686`](../ui/dr-ui/src/library_ui.rs#L2686), [`ui/dr-ui/src/library_ui.rs:4083`](../ui/dr-ui/src/library_ui.rs#L4083), [`ui/dr-ui/src/library_ui.rs:4834`](../ui/dr-ui/src/library_ui.rs#L4834), [`ui/dr-ui/ui/app.slint:352`](../ui/dr-ui/ui/app.slint#L352), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:2886`](../ui/dr-ui/ui/library.slint#L2886), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | -| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1669`](../core/dr-pipeline/src/sidecar.rs#L1669), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3991`](../ui/dr-ui/src/develop.rs#L3991), [`ui/dr-ui/src/develop.rs:4020`](../ui/dr-ui/src/develop.rs#L4020), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1039`](../ui/dr-ui/src/lib.rs#L1039), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:2045`](../ui/dr-ui/src/lib.rs#L2045), [`ui/dr-ui/src/lib.rs:2180`](../ui/dr-ui/src/lib.rs#L2180), [`ui/dr-ui/src/lib.rs:591`](../ui/dr-ui/src/lib.rs#L591), [`ui/dr-ui/src/library.rs:1923`](../ui/dr-ui/src/library.rs#L1923), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5700`](../ui/dr-ui/src/library_ui.rs#L5700), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/develop.rs:3421`](../ui/dr-ui/src/develop.rs#L3421), [`ui/dr-ui/src/library.rs:1242`](../ui/dr-ui/src/library.rs#L1242), [`ui/dr-ui/src/library.rs:1295`](../ui/dr-ui/src/library.rs#L1295), [`ui/dr-ui/src/library.rs:1326`](../ui/dr-ui/src/library.rs#L1326), [`ui/dr-ui/src/library.rs:1431`](../ui/dr-ui/src/library.rs#L1431), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1883`](../ui/dr-ui/src/library.rs#L1883), [`ui/dr-ui/src/library.rs:1959`](../ui/dr-ui/src/library.rs#L1959), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4769`](../ui/dr-ui/src/library.rs#L4769), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1318`](../ui/dr-ui/src/library_ui.rs#L1318), [`ui/dr-ui/src/library_ui.rs:1909`](../ui/dr-ui/src/library_ui.rs#L1909), [`ui/dr-ui/src/library_ui.rs:1953`](../ui/dr-ui/src/library_ui.rs#L1953), [`ui/dr-ui/src/library_ui.rs:1969`](../ui/dr-ui/src/library_ui.rs#L1969), [`ui/dr-ui/src/library_ui.rs:2063`](../ui/dr-ui/src/library_ui.rs#L2063), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2925`](../ui/dr-ui/src/library_ui.rs#L2925), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:325`](../ui/dr-ui/src/library_ui.rs#L325), [`ui/dr-ui/src/library_ui.rs:3363`](../ui/dr-ui/src/library_ui.rs#L3363), [`ui/dr-ui/src/library_ui.rs:3585`](../ui/dr-ui/src/library_ui.rs#L3585), [`ui/dr-ui/src/library_ui.rs:3863`](../ui/dr-ui/src/library_ui.rs#L3863), [`ui/dr-ui/src/library_ui.rs:3951`](../ui/dr-ui/src/library_ui.rs#L3951), [`ui/dr-ui/src/library_ui.rs:4134`](../ui/dr-ui/src/library_ui.rs#L4134), [`ui/dr-ui/src/library_ui.rs:4248`](../ui/dr-ui/src/library_ui.rs#L4248), [`ui/dr-ui/src/library_ui.rs:523`](../ui/dr-ui/src/library_ui.rs#L523), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/library_ui.rs:6158`](../ui/dr-ui/src/library_ui.rs#L6158), [`ui/dr-ui/src/library_ui.rs:6273`](../ui/dr-ui/src/library_ui.rs#L6273), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1171`](../core/dr-catalog/src/schema.rs#L1171), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/collections_ui.rs:1579`](../ui/dr-ui/src/collections_ui.rs#L1579), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1734`](../ui/dr-ui/src/collections_ui.rs#L1734), [`ui/dr-ui/src/collections_ui.rs:238`](../ui/dr-ui/src/collections_ui.rs#L238), [`ui/dr-ui/src/collections_ui.rs:335`](../ui/dr-ui/src/collections_ui.rs#L335), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4185`](../ui/dr-ui/src/library.rs#L4185), [`ui/dr-ui/src/library_ui.rs:709`](../ui/dr-ui/src/library_ui.rs#L709), [`ui/dr-ui/src/library_ui.rs:7568`](../ui/dr-ui/src/library_ui.rs#L7568), [`ui/dr-ui/src/library_ui.rs:7579`](../ui/dr-ui/src/library_ui.rs#L7579), [`ui/dr-ui/src/library_ui.rs:7592`](../ui/dr-ui/src/library_ui.rs#L7592), [`ui/dr-ui/src/library_ui.rs:7607`](../ui/dr-ui/src/library_ui.rs#L7607), [`ui/dr-ui/src/library_ui.rs:7616`](../ui/dr-ui/src/library_ui.rs#L7616), [`ui/dr-ui/ui/app.slint:349`](../ui/dr-ui/ui/app.slint#L349), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/library.slint:1350`](../ui/dr-ui/ui/library.slint#L1350), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1427`](../ui/dr-ui/ui/library.slint#L1427), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101) | +| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:4661`](../ui/dr-ui/src/library.rs#L4661), [`ui/dr-ui/src/library_ui.rs:386`](../ui/dr-ui/src/library_ui.rs#L386), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/library_ui.rs:6298`](../ui/dr-ui/src/library_ui.rs#L6298), [`ui/dr-ui/src/library_ui.rs:6351`](../ui/dr-ui/src/library_ui.rs#L6351), [`ui/dr-ui/src/library_ui.rs:7708`](../ui/dr-ui/src/library_ui.rs#L7708), [`ui/dr-ui/ui/app.slint:361`](../ui/dr-ui/ui/app.slint#L361), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/app.slint:874`](../ui/dr-ui/ui/app.slint#L874), [`ui/dr-ui/ui/library.slint:111`](../ui/dr-ui/ui/library.slint#L111), [`ui/dr-ui/ui/library.slint:1537`](../ui/dr-ui/ui/library.slint#L1537), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | +| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1073`](../ui/dr-ui/src/collections_ui.rs#L1073), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1598`](../ui/dr-ui/src/collections_ui.rs#L1598), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1913`](../ui/dr-ui/src/collections_ui.rs#L1913), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:965`](../ui/dr-ui/src/collections_ui.rs#L965), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4466`](../ui/dr-ui/src/library.rs#L4466), [`ui/dr-ui/src/library.rs:4492`](../ui/dr-ui/src/library.rs#L4492), [`ui/dr-ui/src/library.rs:4547`](../ui/dr-ui/src/library.rs#L4547), [`ui/dr-ui/src/library_ui.rs:2578`](../ui/dr-ui/src/library_ui.rs#L2578), [`ui/dr-ui/src/library_ui.rs:3974`](../ui/dr-ui/src/library_ui.rs#L3974), [`ui/dr-ui/src/library_ui.rs:4725`](../ui/dr-ui/src/library_ui.rs#L4725), [`ui/dr-ui/ui/app.slint:352`](../ui/dr-ui/ui/app.slint#L352), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:2886`](../ui/dr-ui/ui/library.slint#L2886), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | +| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1669`](../core/dr-pipeline/src/sidecar.rs#L1669), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3991`](../ui/dr-ui/src/develop.rs#L3991), [`ui/dr-ui/src/develop.rs:4020`](../ui/dr-ui/src/develop.rs#L4020), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1039`](../ui/dr-ui/src/lib.rs#L1039), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:2045`](../ui/dr-ui/src/lib.rs#L2045), [`ui/dr-ui/src/lib.rs:2180`](../ui/dr-ui/src/lib.rs#L2180), [`ui/dr-ui/src/lib.rs:591`](../ui/dr-ui/src/lib.rs#L591), [`ui/dr-ui/src/library.rs:1923`](../ui/dr-ui/src/library.rs#L1923), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5934`](../ui/dr-ui/src/library_ui.rs#L5934), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/develop.rs:3421`](../ui/dr-ui/src/develop.rs#L3421), [`ui/dr-ui/src/library.rs:1242`](../ui/dr-ui/src/library.rs#L1242), [`ui/dr-ui/src/library.rs:1295`](../ui/dr-ui/src/library.rs#L1295), [`ui/dr-ui/src/library.rs:1326`](../ui/dr-ui/src/library.rs#L1326), [`ui/dr-ui/src/library.rs:1431`](../ui/dr-ui/src/library.rs#L1431), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1883`](../ui/dr-ui/src/library.rs#L1883), [`ui/dr-ui/src/library.rs:1959`](../ui/dr-ui/src/library.rs#L1959), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4769`](../ui/dr-ui/src/library.rs#L4769), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1308`](../ui/dr-ui/src/library_ui.rs#L1308), [`ui/dr-ui/src/library_ui.rs:1899`](../ui/dr-ui/src/library_ui.rs#L1899), [`ui/dr-ui/src/library_ui.rs:1943`](../ui/dr-ui/src/library_ui.rs#L1943), [`ui/dr-ui/src/library_ui.rs:1959`](../ui/dr-ui/src/library_ui.rs#L1959), [`ui/dr-ui/src/library_ui.rs:2053`](../ui/dr-ui/src/library_ui.rs#L2053), [`ui/dr-ui/src/library_ui.rs:2817`](../ui/dr-ui/src/library_ui.rs#L2817), [`ui/dr-ui/src/library_ui.rs:281`](../ui/dr-ui/src/library_ui.rs#L281), [`ui/dr-ui/src/library_ui.rs:314`](../ui/dr-ui/src/library_ui.rs#L314), [`ui/dr-ui/src/library_ui.rs:322`](../ui/dr-ui/src/library_ui.rs#L322), [`ui/dr-ui/src/library_ui.rs:3255`](../ui/dr-ui/src/library_ui.rs#L3255), [`ui/dr-ui/src/library_ui.rs:3477`](../ui/dr-ui/src/library_ui.rs#L3477), [`ui/dr-ui/src/library_ui.rs:3755`](../ui/dr-ui/src/library_ui.rs#L3755), [`ui/dr-ui/src/library_ui.rs:3843`](../ui/dr-ui/src/library_ui.rs#L3843), [`ui/dr-ui/src/library_ui.rs:4025`](../ui/dr-ui/src/library_ui.rs#L4025), [`ui/dr-ui/src/library_ui.rs:4139`](../ui/dr-ui/src/library_ui.rs#L4139), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519), [`ui/dr-ui/src/library_ui.rs:582`](../ui/dr-ui/src/library_ui.rs#L582), [`ui/dr-ui/src/library_ui.rs:6396`](../ui/dr-ui/src/library_ui.rs#L6396), [`ui/dr-ui/src/library_ui.rs:6511`](../ui/dr-ui/src/library_ui.rs#L6511), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | | FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:376`](../core/dr-catalog/src/schema.rs#L376), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:120`](../ui/dr-ui/src/develop.rs#L120), [`ui/dr-ui/src/develop.rs:129`](../ui/dr-ui/src/develop.rs#L129), [`ui/dr-ui/src/develop.rs:195`](../ui/dr-ui/src/develop.rs#L195), [`ui/dr-ui/src/develop.rs:2011`](../ui/dr-ui/src/develop.rs#L2011), [`ui/dr-ui/src/develop.rs:688`](../ui/dr-ui/src/develop.rs#L688), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2152`](../ui/dr-ui/src/lib.rs#L2152), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:287`](../ui/dr-ui/src/library.rs#L287), [`ui/dr-ui/src/library.rs:317`](../ui/dr-ui/src/library.rs#L317), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | @@ -69,7 +69,7 @@ _None._ | FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:1060`](../core/dr-pipeline/src/framing.rs#L1060), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:498`](../core/dr-pipeline/src/framing.rs#L498), [`core/dr-types/src/lib.rs:339`](../core/dr-types/src/lib.rs#L339), [`core/dr-types/src/lib.rs:447`](../core/dr-types/src/lib.rs#L447), [`core/dr-types/src/lib.rs:459`](../core/dr-types/src/lib.rs#L459), [`core/dr-types/src/lib.rs:475`](../core/dr-types/src/lib.rs#L475), [`ui/dr-ui/src/develop.rs:139`](../ui/dr-ui/src/develop.rs#L139), [`ui/dr-ui/src/develop.rs:2210`](../ui/dr-ui/src/develop.rs#L2210), [`ui/dr-ui/src/segmentation.rs:451`](../ui/dr-ui/src/segmentation.rs#L451) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:351`](../core/dr-gpu/src/lib.rs#L351), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | | FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3465`](../ui/dr-ui/src/develop.rs#L3465), [`ui/dr-ui/src/develop.rs:4043`](../ui/dr-ui/src/develop.rs#L4043), [`ui/dr-ui/src/develop.rs:4073`](../ui/dr-ui/src/develop.rs#L4073), [`ui/dr-ui/src/develop.rs:4086`](../ui/dr-ui/src/develop.rs#L4086), [`ui/dr-ui/src/develop.rs:4098`](../ui/dr-ui/src/develop.rs#L4098), [`ui/dr-ui/src/develop.rs:4114`](../ui/dr-ui/src/develop.rs#L4114), [`ui/dr-ui/src/develop.rs:4146`](../ui/dr-ui/src/develop.rs#L4146), [`ui/dr-ui/src/develop.rs:4150`](../ui/dr-ui/src/develop.rs#L4150), [`ui/dr-ui/src/develop.rs:4169`](../ui/dr-ui/src/develop.rs#L4169), [`ui/dr-ui/src/develop.rs:4185`](../ui/dr-ui/src/develop.rs#L4185), [`ui/dr-ui/src/develop.rs:709`](../ui/dr-ui/src/develop.rs#L709), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:217`](../ui/dr-ui/src/labels.rs#L217), [`ui/dr-ui/src/lib.rs:1588`](../ui/dr-ui/src/lib.rs#L1588), [`ui/dr-ui/src/lib.rs:1631`](../ui/dr-ui/src/lib.rs#L1631), [`ui/dr-ui/src/lib.rs:1639`](../ui/dr-ui/src/lib.rs#L1639), [`ui/dr-ui/src/lib.rs:2625`](../ui/dr-ui/src/lib.rs#L2625), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:3985`](../ui/dr-ui/src/develop.rs#L3985), [`ui/dr-ui/src/develop.rs:4006`](../ui/dr-ui/src/develop.rs#L4006), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:2240`](../ui/dr-ui/src/lib.rs#L2240), [`ui/dr-ui/src/lib.rs:2269`](../ui/dr-ui/src/lib.rs#L2269), [`ui/dr-ui/src/library.rs:1923`](../ui/dr-ui/src/library.rs#L1923), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:3185`](../ui/dr-ui/src/library_ui.rs#L3185), [`ui/dr-ui/src/library_ui.rs:3585`](../ui/dr-ui/src/library_ui.rs#L3585), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1599`](../ui/dr-ui/ui/app.slint#L1599), [`ui/dr-ui/ui/app.slint:2736`](../ui/dr-ui/ui/app.slint#L2736), [`ui/dr-ui/ui/app.slint:784`](../ui/dr-ui/ui/app.slint#L784), [`ui/dr-ui/ui/app.slint:795`](../ui/dr-ui/ui/app.slint#L795), [`ui/dr-ui/ui/library.slint:1564`](../ui/dr-ui/ui/library.slint#L1564), [`ui/dr-ui/ui/library.slint:1570`](../ui/dr-ui/ui/library.slint#L1570), [`ui/dr-ui/ui/library.slint:4117`](../ui/dr-ui/ui/library.slint#L4117), [`ui/dr-ui/ui/library.slint:4132`](../ui/dr-ui/ui/library.slint#L4132), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:3985`](../ui/dr-ui/src/develop.rs#L3985), [`ui/dr-ui/src/develop.rs:4006`](../ui/dr-ui/src/develop.rs#L4006), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:2240`](../ui/dr-ui/src/lib.rs#L2240), [`ui/dr-ui/src/lib.rs:2269`](../ui/dr-ui/src/lib.rs#L2269), [`ui/dr-ui/src/library.rs:1923`](../ui/dr-ui/src/library.rs#L1923), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:3077`](../ui/dr-ui/src/library_ui.rs#L3077), [`ui/dr-ui/src/library_ui.rs:3477`](../ui/dr-ui/src/library_ui.rs#L3477), [`ui/dr-ui/src/library_ui.rs:550`](../ui/dr-ui/src/library_ui.rs#L550), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1599`](../ui/dr-ui/ui/app.slint#L1599), [`ui/dr-ui/ui/app.slint:2736`](../ui/dr-ui/ui/app.slint#L2736), [`ui/dr-ui/ui/app.slint:784`](../ui/dr-ui/ui/app.slint#L784), [`ui/dr-ui/ui/app.slint:795`](../ui/dr-ui/ui/app.slint#L795), [`ui/dr-ui/ui/library.slint:1564`](../ui/dr-ui/ui/library.slint#L1564), [`ui/dr-ui/ui/library.slint:1570`](../ui/dr-ui/ui/library.slint#L1570), [`ui/dr-ui/ui/library.slint:4117`](../ui/dr-ui/ui/library.slint#L4117), [`ui/dr-ui/ui/library.slint:4132`](../ui/dr-ui/ui/library.slint#L4132), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | | FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:4114`](../ui/dr-ui/src/develop.rs#L4114), [`ui/dr-ui/src/develop.rs:4146`](../ui/dr-ui/src/develop.rs#L4146), [`ui/dr-ui/src/lib.rs:1639`](../ui/dr-ui/src/lib.rs#L1639), [`ui/dr-ui/src/lib.rs:2625`](../ui/dr-ui/src/lib.rs#L2625), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | | FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:114`](../core/dr-pipeline/src/graph.rs#L114), [`core/dr-pipeline/src/graph.rs:192`](../core/dr-pipeline/src/graph.rs#L192), [`core/dr-pipeline/src/graph.rs:686`](../core/dr-pipeline/src/graph.rs#L686), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:673`](../core/dr-pipeline/src/sidecar.rs#L673), [`core/dr-pipeline/src/sidecar.rs:809`](../core/dr-pipeline/src/sidecar.rs#L809), [`core/dr-pipeline/src/sidecar.rs:863`](../core/dr-pipeline/src/sidecar.rs#L863), [`core/dr-pipeline/src/sidecar.rs:893`](../core/dr-pipeline/src/sidecar.rs#L893), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2362`](../ui/dr-ui/src/develop.rs#L2362), [`ui/dr-ui/src/develop.rs:2400`](../ui/dr-ui/src/develop.rs#L2400), [`ui/dr-ui/src/develop.rs:2465`](../ui/dr-ui/src/develop.rs#L2465), [`ui/dr-ui/src/develop.rs:2548`](../ui/dr-ui/src/develop.rs#L2548), [`ui/dr-ui/src/develop.rs:2562`](../ui/dr-ui/src/develop.rs#L2562), [`ui/dr-ui/src/develop.rs:796`](../ui/dr-ui/src/develop.rs#L796), [`ui/dr-ui/src/labels.rs:53`](../ui/dr-ui/src/labels.rs#L53), [`ui/dr-ui/src/lib.rs:1660`](../ui/dr-ui/src/lib.rs#L1660), [`ui/dr-ui/src/lib.rs:2743`](../ui/dr-ui/src/lib.rs#L2743), [`ui/dr-ui/src/lib.rs:362`](../ui/dr-ui/src/lib.rs#L362), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:832`](../ui/dr-ui/ui/adjust.slint#L832), [`ui/dr-ui/ui/app.slint:140`](../ui/dr-ui/ui/app.slint#L140), [`ui/dr-ui/ui/app.slint:1949`](../ui/dr-ui/ui/app.slint#L1949), [`ui/dr-ui/ui/app.slint:2124`](../ui/dr-ui/ui/app.slint#L2124), [`ui/dr-ui/ui/app.slint:2548`](../ui/dr-ui/ui/app.slint#L2548), [`ui/dr-ui/ui/icons.slint:338`](../ui/dr-ui/ui/icons.slint#L338), [`ui/dr-ui/ui/spots.slint:172`](../ui/dr-ui/ui/spots.slint#L172), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | | FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2126`](../core/dr-gpu/src/adjust.rs#L2126), [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`core/dr-gpu/src/adjust.rs:2288`](../core/dr-gpu/src/adjust.rs#L2288), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:548`](../core/dr-pipeline/src/graph.rs#L548), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2995`](../ui/dr-ui/src/develop.rs#L2995), [`ui/dr-ui/src/develop.rs:4440`](../ui/dr-ui/src/develop.rs#L4440), [`ui/dr-ui/src/develop.rs:4923`](../ui/dr-ui/src/develop.rs#L4923), [`ui/dr-ui/src/develop.rs:4957`](../ui/dr-ui/src/develop.rs#L4957), [`ui/dr-ui/src/lib.rs:847`](../ui/dr-ui/src/lib.rs#L847), [`ui/dr-ui/src/lib.rs:906`](../ui/dr-ui/src/lib.rs#L906), [`ui/dr-ui/src/lib.rs:93`](../ui/dr-ui/src/lib.rs#L93) | @@ -84,28 +84,28 @@ _None._ | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:452`](../ui/dr-ui/src/lib.rs#L452), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:679`](../ui/dr-ui/src/settings_ui.rs#L679) | -| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231), [`ui/dr-ui/src/lib.rs:2371`](../ui/dr-ui/src/lib.rs#L2371), [`ui/dr-ui/src/lib.rs:452`](../ui/dr-ui/src/lib.rs#L452), [`ui/dr-ui/src/lib.rs:487`](../ui/dr-ui/src/lib.rs#L487), [`ui/dr-ui/src/lib.rs:514`](../ui/dr-ui/src/lib.rs#L514), [`ui/dr-ui/src/library_ui.rs:3968`](../ui/dr-ui/src/library_ui.rs#L3968), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/library_ui.rs:7312`](../ui/dr-ui/src/library_ui.rs#L7312), [`ui/dr-ui/src/library_ui.rs:7389`](../ui/dr-ui/src/library_ui.rs#L7389), [`ui/dr-ui/src/library_ui.rs:7401`](../ui/dr-ui/src/library_ui.rs#L7401), [`ui/dr-ui/src/library_ui.rs:745`](../ui/dr-ui/src/library_ui.rs#L745), [`ui/dr-ui/src/library_ui.rs:802`](../ui/dr-ui/src/library_ui.rs#L802), [`ui/dr-ui/ui/app.slint:1123`](../ui/dr-ui/ui/app.slint#L1123), [`ui/dr-ui/ui/app.slint:1609`](../ui/dr-ui/ui/app.slint#L1609), [`ui/dr-ui/ui/library.slint:1573`](../ui/dr-ui/ui/library.slint#L1573), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | +| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231), [`ui/dr-ui/src/lib.rs:2371`](../ui/dr-ui/src/lib.rs#L2371), [`ui/dr-ui/src/lib.rs:452`](../ui/dr-ui/src/lib.rs#L452), [`ui/dr-ui/src/lib.rs:487`](../ui/dr-ui/src/lib.rs#L487), [`ui/dr-ui/src/lib.rs:514`](../ui/dr-ui/src/lib.rs#L514), [`ui/dr-ui/src/library_ui.rs:3860`](../ui/dr-ui/src/library_ui.rs#L3860), [`ui/dr-ui/src/library_ui.rs:641`](../ui/dr-ui/src/library_ui.rs#L641), [`ui/dr-ui/src/library_ui.rs:741`](../ui/dr-ui/src/library_ui.rs#L741), [`ui/dr-ui/src/library_ui.rs:7550`](../ui/dr-ui/src/library_ui.rs#L7550), [`ui/dr-ui/src/library_ui.rs:7627`](../ui/dr-ui/src/library_ui.rs#L7627), [`ui/dr-ui/src/library_ui.rs:7639`](../ui/dr-ui/src/library_ui.rs#L7639), [`ui/dr-ui/src/library_ui.rs:798`](../ui/dr-ui/src/library_ui.rs#L798), [`ui/dr-ui/ui/app.slint:1123`](../ui/dr-ui/ui/app.slint#L1123), [`ui/dr-ui/ui/app.slint:1609`](../ui/dr-ui/ui/app.slint#L1609), [`ui/dr-ui/ui/library.slint:1573`](../ui/dr-ui/ui/library.slint#L1573), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:426`](../core/dr-types/src/settings.rs#L426), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:3340`](../ui/dr-ui/src/develop.rs#L3340), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/lib.rs:452`](../ui/dr-ui/src/lib.rs#L452), [`ui/dr-ui/src/library.rs:3078`](../ui/dr-ui/src/library.rs#L3078), [`ui/dr-ui/src/library.rs:3443`](../ui/dr-ui/src/library.rs#L3443), [`ui/dr-ui/src/library.rs:3482`](../ui/dr-ui/src/library.rs#L3482), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | -| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:514`](../ui/dr-ui/src/lib.rs#L514), [`ui/dr-ui/src/library.rs:1121`](../ui/dr-ui/src/library.rs#L1121), [`ui/dr-ui/src/library.rs:1959`](../ui/dr-ui/src/library.rs#L1959), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1969`](../ui/dr-ui/src/library_ui.rs#L1969), [`ui/dr-ui/src/library_ui.rs:3968`](../ui/dr-ui/src/library_ui.rs#L3968), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:514`](../ui/dr-ui/src/lib.rs#L514), [`ui/dr-ui/src/library.rs:1121`](../ui/dr-ui/src/library.rs#L1121), [`ui/dr-ui/src/library.rs:1959`](../ui/dr-ui/src/library.rs#L1959), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1959`](../ui/dr-ui/src/library_ui.rs#L1959), [`ui/dr-ui/src/library_ui.rs:3860`](../ui/dr-ui/src/library_ui.rs#L3860), [`ui/dr-ui/src/library_ui.rs:582`](../ui/dr-ui/src/library_ui.rs#L582), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`ui/dr-ui/src/launch_ui.rs:316`](../ui/dr-ui/src/launch_ui.rs#L316), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`core/dr-sync/src/account.rs:59`](../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | -| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2984`](../ui/dr-ui/src/library.rs#L2984), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:4221`](../ui/dr-ui/src/library_ui.rs#L4221), [`ui/dr-ui/src/library_ui.rs:5382`](../ui/dr-ui/src/library_ui.rs#L5382), [`ui/dr-ui/ui/app.slint:428`](../ui/dr-ui/ui/app.slint#L428), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2984`](../ui/dr-ui/src/library.rs#L2984), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:4112`](../ui/dr-ui/src/library_ui.rs#L4112), [`ui/dr-ui/src/library_ui.rs:5611`](../ui/dr-ui/src/library_ui.rs#L5611), [`ui/dr-ui/ui/app.slint:428`](../ui/dr-ui/ui/app.slint#L428), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | -| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1126`](../core/dr-catalog/src/schema.rs#L1126), [`core/dr-catalog/src/schema.rs:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/lib.rs:2075`](../ui/dr-ui/src/lib.rs#L2075), [`ui/dr-ui/src/lib.rs:3198`](../ui/dr-ui/src/lib.rs#L3198), [`ui/dr-ui/src/library.rs:1585`](../ui/dr-ui/src/library.rs#L1585), [`ui/dr-ui/src/library.rs:1608`](../ui/dr-ui/src/library.rs#L1608), [`ui/dr-ui/src/library.rs:1883`](../ui/dr-ui/src/library.rs#L1883), [`ui/dr-ui/src/library_ui.rs:1394`](../ui/dr-ui/src/library_ui.rs#L1394), [`ui/dr-ui/src/library_ui.rs:1467`](../ui/dr-ui/src/library_ui.rs#L1467), [`ui/dr-ui/src/library_ui.rs:1568`](../ui/dr-ui/src/library_ui.rs#L1568), [`ui/dr-ui/src/library_ui.rs:1623`](../ui/dr-ui/src/library_ui.rs#L1623), [`ui/dr-ui/src/library_ui.rs:1758`](../ui/dr-ui/src/library_ui.rs#L1758), [`ui/dr-ui/src/library_ui.rs:1876`](../ui/dr-ui/src/library_ui.rs#L1876), [`ui/dr-ui/src/library_ui.rs:2643`](../ui/dr-ui/src/library_ui.rs#L2643), [`ui/dr-ui/src/library_ui.rs:341`](../ui/dr-ui/src/library_ui.rs#L341), [`ui/dr-ui/src/library_ui.rs:352`](../ui/dr-ui/src/library_ui.rs#L352), [`ui/dr-ui/src/library_ui.rs:360`](../ui/dr-ui/src/library_ui.rs#L360), [`ui/dr-ui/src/library_ui.rs:372`](../ui/dr-ui/src/library_ui.rs#L372), [`ui/dr-ui/src/library_ui.rs:381`](../ui/dr-ui/src/library_ui.rs#L381), [`ui/dr-ui/src/library_ui.rs:482`](../ui/dr-ui/src/library_ui.rs#L482), [`ui/dr-ui/src/library_ui.rs:492`](../ui/dr-ui/src/library_ui.rs#L492), [`ui/dr-ui/src/library_ui.rs:539`](../ui/dr-ui/src/library_ui.rs#L539), [`ui/dr-ui/src/library_ui.rs:602`](../ui/dr-ui/src/library_ui.rs#L602), [`ui/dr-ui/src/library_ui.rs:6175`](../ui/dr-ui/src/library_ui.rs#L6175), [`ui/dr-ui/src/library_ui.rs:6193`](../ui/dr-ui/src/library_ui.rs#L6193), [`ui/dr-ui/src/library_ui.rs:6205`](../ui/dr-ui/src/library_ui.rs#L6205), [`ui/dr-ui/src/library_ui.rs:633`](../ui/dr-ui/src/library_ui.rs#L633), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1507`](../ui/dr-ui/ui/app.slint#L1507), [`ui/dr-ui/ui/app.slint:2726`](../ui/dr-ui/ui/app.slint#L2726), [`ui/dr-ui/ui/app.slint:560`](../ui/dr-ui/ui/app.slint#L560), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:360`](../ui/dr-ui/ui/collections.slint#L360), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:372`](../ui/dr-ui/ui/collections.slint#L372), [`ui/dr-ui/ui/collections.slint:395`](../ui/dr-ui/ui/collections.slint#L395), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:596`](../ui/dr-ui/ui/collections.slint#L596), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | -| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1623`](../ui/dr-ui/src/library_ui.rs#L1623), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | -| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/derived_sync.rs:585`](../ui/dr-ui/src/derived_sync.rs#L585), [`ui/dr-ui/src/derived_sync.rs:805`](../ui/dr-ui/src/derived_sync.rs#L805), [`ui/dr-ui/src/library.rs:1705`](../ui/dr-ui/src/library.rs#L1705), [`ui/dr-ui/src/library.rs:1795`](../ui/dr-ui/src/library.rs#L1795), [`ui/dr-ui/src/library.rs:3262`](../ui/dr-ui/src/library.rs#L3262), [`ui/dr-ui/src/library.rs:3700`](../ui/dr-ui/src/library.rs#L3700), [`ui/dr-ui/src/library.rs:981`](../ui/dr-ui/src/library.rs#L981), [`ui/dr-ui/src/library_ui.rs:1394`](../ui/dr-ui/src/library_ui.rs#L1394), [`ui/dr-ui/src/library_ui.rs:1467`](../ui/dr-ui/src/library_ui.rs#L1467), [`ui/dr-ui/src/library_ui.rs:1666`](../ui/dr-ui/src/library_ui.rs#L1666), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1126`](../core/dr-catalog/src/schema.rs#L1126), [`core/dr-catalog/src/schema.rs:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/lib.rs:2075`](../ui/dr-ui/src/lib.rs#L2075), [`ui/dr-ui/src/lib.rs:3198`](../ui/dr-ui/src/lib.rs#L3198), [`ui/dr-ui/src/library.rs:1585`](../ui/dr-ui/src/library.rs#L1585), [`ui/dr-ui/src/library.rs:1608`](../ui/dr-ui/src/library.rs#L1608), [`ui/dr-ui/src/library.rs:1883`](../ui/dr-ui/src/library.rs#L1883), [`ui/dr-ui/src/library_ui.rs:1384`](../ui/dr-ui/src/library_ui.rs#L1384), [`ui/dr-ui/src/library_ui.rs:1457`](../ui/dr-ui/src/library_ui.rs#L1457), [`ui/dr-ui/src/library_ui.rs:1558`](../ui/dr-ui/src/library_ui.rs#L1558), [`ui/dr-ui/src/library_ui.rs:1613`](../ui/dr-ui/src/library_ui.rs#L1613), [`ui/dr-ui/src/library_ui.rs:1748`](../ui/dr-ui/src/library_ui.rs#L1748), [`ui/dr-ui/src/library_ui.rs:1866`](../ui/dr-ui/src/library_ui.rs#L1866), [`ui/dr-ui/src/library_ui.rs:2535`](../ui/dr-ui/src/library_ui.rs#L2535), [`ui/dr-ui/src/library_ui.rs:338`](../ui/dr-ui/src/library_ui.rs#L338), [`ui/dr-ui/src/library_ui.rs:349`](../ui/dr-ui/src/library_ui.rs#L349), [`ui/dr-ui/src/library_ui.rs:357`](../ui/dr-ui/src/library_ui.rs#L357), [`ui/dr-ui/src/library_ui.rs:369`](../ui/dr-ui/src/library_ui.rs#L369), [`ui/dr-ui/src/library_ui.rs:378`](../ui/dr-ui/src/library_ui.rs#L378), [`ui/dr-ui/src/library_ui.rs:478`](../ui/dr-ui/src/library_ui.rs#L478), [`ui/dr-ui/src/library_ui.rs:488`](../ui/dr-ui/src/library_ui.rs#L488), [`ui/dr-ui/src/library_ui.rs:535`](../ui/dr-ui/src/library_ui.rs#L535), [`ui/dr-ui/src/library_ui.rs:598`](../ui/dr-ui/src/library_ui.rs#L598), [`ui/dr-ui/src/library_ui.rs:629`](../ui/dr-ui/src/library_ui.rs#L629), [`ui/dr-ui/src/library_ui.rs:6413`](../ui/dr-ui/src/library_ui.rs#L6413), [`ui/dr-ui/src/library_ui.rs:641`](../ui/dr-ui/src/library_ui.rs#L641), [`ui/dr-ui/src/library_ui.rs:6431`](../ui/dr-ui/src/library_ui.rs#L6431), [`ui/dr-ui/src/library_ui.rs:6443`](../ui/dr-ui/src/library_ui.rs#L6443), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1507`](../ui/dr-ui/ui/app.slint#L1507), [`ui/dr-ui/ui/app.slint:2726`](../ui/dr-ui/ui/app.slint#L2726), [`ui/dr-ui/ui/app.slint:560`](../ui/dr-ui/ui/app.slint#L560), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:360`](../ui/dr-ui/ui/collections.slint#L360), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:372`](../ui/dr-ui/ui/collections.slint#L372), [`ui/dr-ui/ui/collections.slint:395`](../ui/dr-ui/ui/collections.slint#L395), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:596`](../ui/dr-ui/ui/collections.slint#L596), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | +| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1613`](../ui/dr-ui/src/library_ui.rs#L1613), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/derived_sync.rs:627`](../ui/dr-ui/src/derived_sync.rs#L627), [`ui/dr-ui/src/library.rs:1705`](../ui/dr-ui/src/library.rs#L1705), [`ui/dr-ui/src/library.rs:1795`](../ui/dr-ui/src/library.rs#L1795), [`ui/dr-ui/src/library.rs:3262`](../ui/dr-ui/src/library.rs#L3262), [`ui/dr-ui/src/library.rs:3700`](../ui/dr-ui/src/library.rs#L3700), [`ui/dr-ui/src/library.rs:981`](../ui/dr-ui/src/library.rs#L981), [`ui/dr-ui/src/library_ui.rs:1384`](../ui/dr-ui/src/library_ui.rs#L1384), [`ui/dr-ui/src/library_ui.rs:1457`](../ui/dr-ui/src/library_ui.rs#L1457), [`ui/dr-ui/src/library_ui.rs:1656`](../ui/dr-ui/src/library_ui.rs#L1656), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | | FR-NC-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | -| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:4221`](../ui/dr-ui/src/library_ui.rs#L4221), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | +| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:4112`](../ui/dr-ui/src/library_ui.rs#L4112), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:206`](../core/dr-types/src/settings.rs#L206), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1261`](../ui/dr-ui/src/lib.rs#L1261), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1261`](../ui/dr-ui/src/lib.rs#L1261) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`ui/dr-ui/src/lib.rs:2045`](../ui/dr-ui/src/lib.rs#L2045), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:2091`](../core/dr-pipeline/src/sidecar.rs#L2091), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:451`](../core/dr-pipeline/src/sidecar.rs#L451), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/derived_sync.rs:585`](../ui/dr-ui/src/derived_sync.rs#L585), [`ui/dr-ui/src/library.rs:1031`](../ui/dr-ui/src/library.rs#L1031), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877) | -| FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-sync/src/scan.rs:490`](../core/dr-sync/src/scan.rs#L490), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`ui/dr-ui/src/library.rs:1242`](../ui/dr-ui/src/library.rs#L1242), [`ui/dr-ui/src/library.rs:1295`](../ui/dr-ui/src/library.rs#L1295), [`ui/dr-ui/src/library.rs:1326`](../ui/dr-ui/src/library.rs#L1326), [`ui/dr-ui/src/library.rs:1431`](../ui/dr-ui/src/library.rs#L1431), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1318`](../ui/dr-ui/src/library_ui.rs#L1318), [`ui/dr-ui/src/library_ui.rs:1917`](../ui/dr-ui/src/library_ui.rs#L1917), [`ui/dr-ui/src/library_ui.rs:325`](../ui/dr-ui/src/library_ui.rs#L325), [`ui/dr-ui/src/library_ui.rs:523`](../ui/dr-ui/src/library_ui.rs#L523) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`ui/dr-ui/src/lib.rs:2045`](../ui/dr-ui/src/lib.rs#L2045), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:550`](../ui/dr-ui/src/library_ui.rs#L550) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:2091`](../core/dr-pipeline/src/sidecar.rs#L2091), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:451`](../core/dr-pipeline/src/sidecar.rs#L451), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/library.rs:1031`](../ui/dr-ui/src/library.rs#L1031), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877) | +| FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-sync/src/scan.rs:490`](../core/dr-sync/src/scan.rs#L490), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`ui/dr-ui/src/library.rs:1242`](../ui/dr-ui/src/library.rs#L1242), [`ui/dr-ui/src/library.rs:1295`](../ui/dr-ui/src/library.rs#L1295), [`ui/dr-ui/src/library.rs:1326`](../ui/dr-ui/src/library.rs#L1326), [`ui/dr-ui/src/library.rs:1431`](../ui/dr-ui/src/library.rs#L1431), [`ui/dr-ui/src/library_ui.rs:1229`](../ui/dr-ui/src/library_ui.rs#L1229), [`ui/dr-ui/src/library_ui.rs:1308`](../ui/dr-ui/src/library_ui.rs#L1308), [`ui/dr-ui/src/library_ui.rs:1907`](../ui/dr-ui/src/library_ui.rs#L1907), [`ui/dr-ui/src/library_ui.rs:322`](../ui/dr-ui/src/library_ui.rs#L322), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3303`](../ui/dr-ui/src/develop.rs#L3303), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:1080`](../ui/dr-ui/src/lib.rs#L1080), [`ui/dr-ui/src/lib.rs:1529`](../ui/dr-ui/src/lib.rs#L1529), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | @@ -120,13 +120,13 @@ _None._ | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | | FR-UI-1 | [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/lib.rs:108`](../ui/dr-ui/src/lib.rs#L108), [`ui/dr-ui/src/lib.rs:3280`](../ui/dr-ui/src/lib.rs#L3280), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/ui/app.slint:2265`](../ui/dr-ui/ui/app.slint#L2265), [`ui/dr-ui/ui/identity.slint:188`](../ui/dr-ui/ui/identity.slint#L188), [`ui/dr-ui/ui/library.slint:1071`](../ui/dr-ui/ui/library.slint#L1071), [`ui/dr-ui/ui/toolrail.slint:88`](../ui/dr-ui/ui/toolrail.slint#L88) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/library_ui.rs:360`](../ui/dr-ui/src/library_ui.rs#L360), [`ui/dr-ui/src/library_ui.rs:6060`](../ui/dr-ui/src/library_ui.rs#L6060), [`ui/dr-ui/src/library_ui.rs:6205`](../ui/dr-ui/src/library_ui.rs#L6205), [`ui/dr-ui/src/library_ui.rs:7420`](../ui/dr-ui/src/library_ui.rs#L7420), [`ui/dr-ui/ui/app.slint:590`](../ui/dr-ui/ui/app.slint#L590), [`ui/dr-ui/ui/app.slint:604`](../ui/dr-ui/ui/app.slint#L604), [`ui/dr-ui/ui/app.slint:60`](../ui/dr-ui/ui/app.slint#L60), [`ui/dr-ui/ui/app.slint:955`](../ui/dr-ui/ui/app.slint#L955), [`ui/dr-ui/ui/controls.slint:593`](../ui/dr-ui/ui/controls.slint#L593), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2291`](../ui/dr-ui/src/develop.rs#L2291), [`ui/dr-ui/src/develop.rs:2400`](../ui/dr-ui/src/develop.rs#L2400), [`ui/dr-ui/src/library_ui.rs:5139`](../ui/dr-ui/src/library_ui.rs#L5139), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:976`](../ui/dr-ui/src/masks_ui.rs#L976), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2046`](../ui/dr-ui/ui/app.slint#L2046), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | -| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1964`](../ui/dr-ui/src/collections_ui.rs#L1964), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:589`](../ui/dr-ui/src/collections_ui.rs#L589), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:5139`](../ui/dr-ui/src/library_ui.rs#L5139), [`ui/dr-ui/src/library_ui.rs:5151`](../ui/dr-ui/src/library_ui.rs#L5151), [`ui/dr-ui/src/library_ui.rs:5216`](../ui/dr-ui/src/library_ui.rs#L5216), [`ui/dr-ui/src/library_ui.rs:5325`](../ui/dr-ui/src/library_ui.rs#L5325), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`ui/dr-ui/src/library_ui.rs:6193`](../ui/dr-ui/src/library_ui.rs#L6193), [`ui/dr-ui/src/library_ui.rs:6205`](../ui/dr-ui/src/library_ui.rs#L6205), [`ui/dr-ui/ui/app.slint:1886`](../ui/dr-ui/ui/app.slint#L1886), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/app.slint:604`](../ui/dr-ui/ui/app.slint#L604), [`ui/dr-ui/ui/app.slint:689`](../ui/dr-ui/ui/app.slint#L689), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1048`](../ui/dr-ui/ui/library.slint#L1048), [`ui/dr-ui/ui/library.slint:1252`](../ui/dr-ui/ui/library.slint#L1252), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:987`](../ui/dr-ui/ui/library.slint#L987), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/library_ui.rs:357`](../ui/dr-ui/src/library_ui.rs#L357), [`ui/dr-ui/src/library_ui.rs:6298`](../ui/dr-ui/src/library_ui.rs#L6298), [`ui/dr-ui/src/library_ui.rs:6443`](../ui/dr-ui/src/library_ui.rs#L6443), [`ui/dr-ui/src/library_ui.rs:7658`](../ui/dr-ui/src/library_ui.rs#L7658), [`ui/dr-ui/ui/app.slint:590`](../ui/dr-ui/ui/app.slint#L590), [`ui/dr-ui/ui/app.slint:604`](../ui/dr-ui/ui/app.slint#L604), [`ui/dr-ui/ui/app.slint:60`](../ui/dr-ui/ui/app.slint#L60), [`ui/dr-ui/ui/app.slint:955`](../ui/dr-ui/ui/app.slint#L955), [`ui/dr-ui/ui/controls.slint:593`](../ui/dr-ui/ui/controls.slint#L593), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2291`](../ui/dr-ui/src/develop.rs#L2291), [`ui/dr-ui/src/develop.rs:2400`](../ui/dr-ui/src/develop.rs#L2400), [`ui/dr-ui/src/library_ui.rs:5362`](../ui/dr-ui/src/library_ui.rs#L5362), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:976`](../ui/dr-ui/src/masks_ui.rs#L976), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2046`](../ui/dr-ui/ui/app.slint#L2046), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | +| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1964`](../ui/dr-ui/src/collections_ui.rs#L1964), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:589`](../ui/dr-ui/src/collections_ui.rs#L589), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:5362`](../ui/dr-ui/src/library_ui.rs#L5362), [`ui/dr-ui/src/library_ui.rs:5374`](../ui/dr-ui/src/library_ui.rs#L5374), [`ui/dr-ui/src/library_ui.rs:5444`](../ui/dr-ui/src/library_ui.rs#L5444), [`ui/dr-ui/src/library_ui.rs:5554`](../ui/dr-ui/src/library_ui.rs#L5554), [`ui/dr-ui/src/library_ui.rs:5582`](../ui/dr-ui/src/library_ui.rs#L5582), [`ui/dr-ui/src/library_ui.rs:6431`](../ui/dr-ui/src/library_ui.rs#L6431), [`ui/dr-ui/src/library_ui.rs:6443`](../ui/dr-ui/src/library_ui.rs#L6443), [`ui/dr-ui/ui/app.slint:1886`](../ui/dr-ui/ui/app.slint#L1886), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/app.slint:604`](../ui/dr-ui/ui/app.slint#L604), [`ui/dr-ui/ui/app.slint:689`](../ui/dr-ui/ui/app.slint#L689), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1048`](../ui/dr-ui/ui/library.slint#L1048), [`ui/dr-ui/ui/library.slint:1252`](../ui/dr-ui/ui/library.slint#L1252), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:987`](../ui/dr-ui/ui/library.slint#L987), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | | FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2708`](../ui/dr-ui/src/lib.rs#L2708), [`ui/dr-ui/src/lib.rs:3319`](../ui/dr-ui/src/lib.rs#L3319), [`ui/dr-ui/src/lib.rs:3504`](../ui/dr-ui/src/lib.rs#L3504), [`ui/dr-ui/src/masks_ui.rs:931`](../ui/dr-ui/src/masks_ui.rs#L931), [`ui/dr-ui/ui/app.slint:121`](../ui/dr-ui/ui/app.slint#L121), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:395`](../core/dr-pipeline/src/framing.rs#L395) | -| FR-UI-8 | [`core/dr-catalog/src/collections.rs:555`](../core/dr-catalog/src/collections.rs#L555), [`core/dr-catalog/src/collections.rs:576`](../core/dr-catalog/src/collections.rs#L576), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:188`](../ui/dr-ui/src/derived_sync.rs#L188), [`ui/dr-ui/src/derived_sync.rs:657`](../ui/dr-ui/src/derived_sync.rs#L657), [`ui/dr-ui/src/derived_sync.rs:667`](../ui/dr-ui/src/derived_sync.rs#L667), [`ui/dr-ui/src/derived_sync.rs:749`](../ui/dr-ui/src/derived_sync.rs#L749), [`ui/dr-ui/src/derived_sync.rs:74`](../ui/dr-ui/src/derived_sync.rs#L74), [`ui/dr-ui/src/library.rs:1072`](../ui/dr-ui/src/library.rs#L1072), [`ui/dr-ui/src/library.rs:4253`](../ui/dr-ui/src/library.rs#L4253), [`ui/dr-ui/src/library.rs:4971`](../ui/dr-ui/src/library.rs#L4971), [`ui/dr-ui/src/library.rs:5037`](../ui/dr-ui/src/library.rs#L5037), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2104`](../ui/dr-ui/src/library_ui.rs#L2104), [`ui/dr-ui/src/library_ui.rs:2293`](../ui/dr-ui/src/library_ui.rs#L2293), [`ui/dr-ui/src/library_ui.rs:2310`](../ui/dr-ui/src/library_ui.rs#L2310), [`ui/dr-ui/src/library_ui.rs:2408`](../ui/dr-ui/src/library_ui.rs#L2408), [`ui/dr-ui/src/library_ui.rs:5172`](../ui/dr-ui/src/library_ui.rs#L5172), [`ui/dr-ui/src/library_ui.rs:5236`](../ui/dr-ui/src/library_ui.rs#L5236), [`ui/dr-ui/src/library_ui.rs:6434`](../ui/dr-ui/src/library_ui.rs#L6434), [`ui/dr-ui/src/library_ui.rs:859`](../ui/dr-ui/src/library_ui.rs#L859), [`ui/dr-ui/src/library_ui.rs:892`](../ui/dr-ui/src/library_ui.rs#L892), [`ui/dr-ui/src/library_ui.rs:938`](../ui/dr-ui/src/library_ui.rs#L938), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | +| FR-UI-8 | [`core/dr-catalog/src/collections.rs:555`](../core/dr-catalog/src/collections.rs#L555), [`core/dr-catalog/src/collections.rs:576`](../core/dr-catalog/src/collections.rs#L576), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1072`](../ui/dr-ui/src/library.rs#L1072), [`ui/dr-ui/src/library.rs:4253`](../ui/dr-ui/src/library.rs#L4253), [`ui/dr-ui/src/library.rs:4971`](../ui/dr-ui/src/library.rs#L4971), [`ui/dr-ui/src/library.rs:5037`](../ui/dr-ui/src/library.rs#L5037), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2094`](../ui/dr-ui/src/library_ui.rs#L2094), [`ui/dr-ui/src/library_ui.rs:2283`](../ui/dr-ui/src/library_ui.rs#L2283), [`ui/dr-ui/src/library_ui.rs:2300`](../ui/dr-ui/src/library_ui.rs#L2300), [`ui/dr-ui/src/library_ui.rs:5395`](../ui/dr-ui/src/library_ui.rs#L5395), [`ui/dr-ui/src/library_ui.rs:5435`](../ui/dr-ui/src/library_ui.rs#L5435), [`ui/dr-ui/src/library_ui.rs:5464`](../ui/dr-ui/src/library_ui.rs#L5464), [`ui/dr-ui/src/library_ui.rs:5727`](../ui/dr-ui/src/library_ui.rs#L5727), [`ui/dr-ui/src/library_ui.rs:5950`](../ui/dr-ui/src/library_ui.rs#L5950), [`ui/dr-ui/src/library_ui.rs:6672`](../ui/dr-ui/src/library_ui.rs#L6672), [`ui/dr-ui/src/library_ui.rs:855`](../ui/dr-ui/src/library_ui.rs#L855), [`ui/dr-ui/src/library_ui.rs:888`](../ui/dr-ui/src/library_ui.rs#L888), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | | NFR-A11Y-2 | [`ui/dr-ui/tests/ui_controls_are_accessible.rs:1`](../ui/dr-ui/tests/ui_controls_are_accessible.rs#L1) | | NFR-A11Y-3 | [`ui/dr-ui/src/histogram.rs:356`](../ui/dr-ui/src/histogram.rs#L356), [`ui/dr-ui/ui/histogram.slint:137`](../ui/dr-ui/ui/histogram.slint#L137), [`ui/dr-ui/ui/library.slint:756`](../ui/dr-ui/ui/library.slint#L756), [`ui/dr-ui/ui/library.slint:904`](../ui/dr-ui/ui/library.slint#L904), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:3078`](../ui/dr-ui/src/library.rs#L3078) | @@ -139,7 +139,7 @@ _None._ | NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | | NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:3198`](../ui/dr-ui/src/develop.rs#L3198), [`ui/dr-ui/src/lib.rs:1771`](../ui/dr-ui/src/lib.rs#L1771) | | NFR-P3 | [`tools/bench/src/thumbnails.rs:1`](../tools/bench/src/thumbnails.rs#L1) | -| NFR-P5 | [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/library.rs:4360`](../ui/dr-ui/src/library.rs#L4360), [`ui/dr-ui/src/library.rs:5465`](../ui/dr-ui/src/library.rs#L5465), [`ui/dr-ui/src/library_ui.rs:4711`](../ui/dr-ui/src/library_ui.rs#L4711), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | +| NFR-P5 | [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/library.rs:4360`](../ui/dr-ui/src/library.rs#L4360), [`ui/dr-ui/src/library.rs:5465`](../ui/dr-ui/src/library.rs#L5465), [`ui/dr-ui/src/library_ui.rs:4602`](../ui/dr-ui/src/library_ui.rs#L4602), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | | NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) | | NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:272`](../core/dr-types/src/lib.rs#L272), [`core/dr-types/src/lib.rs:305`](../core/dr-types/src/lib.rs#L305), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) | diff --git a/docs/ui-navigation.md b/docs/ui-navigation.md index 0b1cdcb..ff68601 100644 --- a/docs/ui-navigation.md +++ b/docs/ui-navigation.md @@ -451,3 +451,84 @@ rearranges everything. tablet has neither. - **FR-UI-5.** Escape and the Android back gesture leave the innermost state first. Every mode added here joins that order. + +--- + +## 7. Place — the other half of "where am I" + +§1 asked how someone *finds* anything. This asks how they stop losing what they +already found. Both are navigation; the second is the one nobody notices until +it is wrong, and then notices constantly. + +### 7.1 Three failures, one cause + +The library grid is gated on an `if` in the markup, so **every** route away from +it destroys the subtree and rebuilds it on return. The Flickable inside passes +its viewport through zero on the way out. Three consequences, reported +separately and all the same bug: + +- *"Opening Settings and coming back puts me at the top."* The guard on the + scroll handler tested `show-library`, which stays true while Settings, Import, + People or the launch screen covers the grid. The teardown's scroll-to-zero + passed it, and the remembered position was overwritten with 0. +- *"Coming out of develop I lose the photograph I was editing."* The position + restored was where the *grid* was, not what was open — and walking the photo + roll moves the second a long way from the first. +- *"Launching puts me at the beginning."* Nothing was written down at all. + +`app.slint` now computes `library-visible` once — the same expression the `if` +is spelled from — and Rust reads that rather than `show-library`. The two cannot +drift apart, which is what let them drift in the first place. + +### 7.2 Two positions, and which one wins + +Returning from develop has two candidates: `resume_at`, where the grid was, and +the open photograph. They agree in the ordinary case and disagree after a walk +along the roll. + +The rule is not "pick one". The grid seeks to the remembered position, then +`reveal()`s the keyboard cursor, which is on the open photograph and which moves +the viewport as little as will bring it into view. A frame inside the remembered +screenful moves nothing; one outside it scrolls exactly far enough. One rule, +both behaviours — and the cursor rather than the selection, so a set of forty +photographs assembled in the grid survives having one of them opened. + +### 7.3 A place is not a scroll position + +What gets written down is the view, the scope, the filter and the photograph — +because a position without the filter that produced it names a row of a list +that no longer exists. Restoring them has an order for the same reason: scope, +then filter, then position, then the view. Each step changes what an ordinal +*means*. + +Addressed by remote path and collection UUID, never by an ordinal or a row id. +See FR-UI-8 and `dr_types::place` for why, and `library::ordinal_of_path` for the +one place the ordering is inverted — through the grid's own `ORDER BY`, taken +verbatim, rather than spelled a second time. + +### 7.4 The handover, and when to refuse it + +The record travels through `.darkroom-derived/place.json`, so a session begun on +the desktop continues on the tablet. Newest wins; there is nothing to merge. + +The interesting decision is the refusal. A place arriving from another device is +welcome on the way in and unwelcome the moment the photographer has started +working — a grid that jumped somewhere else mid-scroll because a round trip +finally landed would have lost their place to the feature meant to keep it. So +any scroll, scrub, scope change, filter or opened photograph closes the latch, +and a record that arrives after that is still written to disk and simply takes +effect at the next launch. + +### 7.5 Two smaller instruments that were saying nothing + +Both were "correct" in the sense of not being wrong, and both were useless. + +- **The capture-time marker** rested greyed at mid-track until the first scroll, + on the reasoning that anchoring it would imply a choice the user had not made. + But the sidebar's claim is to say *when* you are, and that is known from the + first frame. It is now seeded from wherever the view sits. +- **The photo roll** brought the open frame into view by the shortest move, + which put it hard against one edge with nothing on that side. It now centres + on the first reveal of a develop session and steps minimally thereafter — + a one-shot request the strip consumes, so an overlay screen rebuilding the + view does not undo a roll the user has scrolled by hand. diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 3b94b23..ee207d8 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -48,6 +48,7 @@ mod masks_ui; pub mod memory; mod net_runtime; mod peaking; +mod place; mod preset_store; mod presets; mod recovery_ui; diff --git a/ui/dr-ui/src/library.rs b/ui/dr-ui/src/library.rs index b73b002..693f95f 100644 --- a/ui/dr-ui/src/library.rs +++ b/ui/dr-ui/src/library.rs @@ -1069,6 +1069,26 @@ pub fn catalog_path(account: &Account) -> PathBuf { data_root().join(account.namespace()).join("catalog.sqlite") } +/// TRACES: FR-UI-8 +/// Where this library's last position is remembered. +/// +/// Beside the catalog, under the same account namespace, for the reason +/// `catalog_path` gives: a place belongs to one library, and two folders on one +/// disk are two libraries with two positions. +/// +/// **The name matches the file that travels.** The copy on the server is +/// `place.json` under `.darkroom-derived/`, and the exchange between them is a +/// straight newest-wins swap of the same bytes — so calling the local one +/// anything else would be one more thing to keep in step for no gain. +/// +/// In the data directory rather than the cache one. The consequence is milder +/// here than for the sidecars `data_root` was moved for — losing a place costs +/// a scroll, not a day of culling — but a file the system is free to delete is +/// one that would rarely survive long enough to be read. +pub fn place_path(account: &Account) -> PathBuf { + data_root().join(account.namespace()).join("place.json") +} + /// The directory every account's data hangs off. /// /// **Not the cache directory, and on Android that distinction is the whole @@ -4230,6 +4250,89 @@ pub fn read_ids_span( Ok(ids) } +/// TRACES: FR-UI-8 +/// Where one photograph sits in the grid, by its remote path. +/// +/// The inverse of [`read_ids_span`], and it exists for the same reason that one +/// does: an ordinal only names a photograph relative to an ordering, so it has +/// to be *computed* through the ordering the cells are drawn with rather than +/// guessed at. A restored place that landed on a row read through a different +/// `ORDER BY` would open the library at a photograph the user has never seen, +/// which looks exactly like the position having been forgotten. +/// +/// # Why a window function rather than a count +/// +/// The obvious implementation is "count the rows that sort before this one", +/// and it would need this file to spell the ordering out a second time — as an +/// inequality, with its own handling of the `captured_at IS NULL` term and its +/// own tie-break. [`grid_order_for`] already warns what a second spelling +/// costs, and a manually ordered collection makes it worse: that ordering is a +/// correlated subquery, and an inequality over it is not something anyone +/// should have to read. +/// +/// `row_number() OVER ({order})` takes the ordering *verbatim* from the same +/// function the window read uses, so the two cannot disagree by construction. +/// It is a full pass over the scope rather than an index seek, which is the +/// cost of that guarantee — and it is paid once, at launch, against a query the +/// grid runs several times per screenful of scrolling. +/// +/// `Ok(None)` means the path is not in this grid: deleted, trashed, filtered +/// out, or in a collection the place did not name. The caller falls back to +/// when the photograph was taken, which is what makes a place survive its +/// subject. +/// +/// # Binding order +/// +/// The window's parameters come first, unlike in [`read_ids_span`]. SQLite +/// binds anonymous `?` by their position **in the SQL text**, and here the +/// `OVER (...)` clause is in the select list — ahead of the `WHERE` the scope +/// narrows. Swapping the two silently looks up a collection by an image id. +pub fn ordinal_of_path( + catalog: &Catalog, + scope: Option, + filter: &RatingFilter, + trash: bool, + path: &str, +) -> Result, dr_catalog::CatalogError> { + let (inner, mut params) = if trash { + ( + format!( + "SELECT i.source_ref AS sref, + row_number() OVER ({TRASH_ORDER}) - 1 AS ord + FROM images i + WHERE {TRASHED}" + ), + Vec::new(), + ) + } else { + let (clause, scope_params) = scope_clause(catalog, scope)?; + let rated = filter.sql(); + let folded = uncollapsed("i"); + let (order, mut params) = grid_order_for(catalog, scope); + // The window first, then the scope — see the note above. + params.extend(scope_params); + ( + format!( + "SELECT i.source_ref AS sref, + row_number() OVER ({order}) - 1 AS ord + FROM images i + WHERE {VISIBLE}{rated}{folded}{clause}" + ), + params, + ) + }; + params.push(rusqlite::types::Value::Text(path.to_string())); + + let mut stmt = catalog + .connection() + .prepare(&format!("SELECT ord FROM ({inner}) WHERE sref = ?"))?; + let mut rows = stmt.query(rusqlite::params_from_iter(params.iter()))?; + match rows.next()? { + Some(r) => Ok(Some(r.get::<_, i64>(0)?.max(0) as usize)), + None => Ok(None), + } +} + /// The columns every windowed read selects, in the order [`row_to_cell`] reads /// them. /// @@ -4830,6 +4933,177 @@ mod tests { } } + /// The fixture the ordinal tests share: a mix of dated, undated, shadowed + /// and trashed rows, which is what makes the grid's ordering non-obvious. + fn a_small_library() -> Catalog { + let catalog = Catalog::in_memory().unwrap(); + let c = catalog.connection(); + c.execute( + "INSERT INTO roots(id, kind, label) VALUES (1, 'remote', 'lib')", + [], + ) + .unwrap(); + + for (id, name, captured, shadow, trashed) in [ + (1i64, "2019/a.CR2", Some(100i64), None, None), + (2, "2019/b.CR2", Some(200), None, None), + // Shadowed by its RAW sibling: never a row of the grid. + (3, "2019/b.JPG", Some(200), Some(2i64), None), + (4, "2020/c.CR2", Some(300), None, None), + // Undated sorts last, whatever its name. + (5, "2018/d.CR2", None, None, None), + // Trashed: out of the library, and the only row of the trash. + (6, "2020/e.CR2", Some(400), None, Some(9i64)), + ] { + c.execute( + "INSERT INTO images(id, root_id, source_ref, captured_at, shadowed_by, + trashed_at, added_at) + VALUES (?1, 1, ?2, ?3, ?4, ?5, 0)", + rusqlite::params![id, name, captured, shadow, trashed], + ) + .unwrap(); + } + catalog + } + + #[test] + fn every_cell_reports_the_ordinal_it_is_drawn_at() { + // TRACES: FR-UI-8 + // The invariant the whole restore rests on, stated the strongest way + // there is: walk the window the grid actually draws and ask for each + // cell's ordinal by path. Anything less — a spot check, or a hand-built + // expectation — would pass while `ordinal_of_path` and `read_cells` + // disagreed about undated rows, shadowed rows or the tie-break, which + // is precisely where an ordering drifts. + let catalog = a_small_library(); + let filter = RatingFilter::default(); + let cells = read_cells(&catalog, 0, 100).unwrap(); + assert_eq!(cells.len(), 4, "shadowed and trashed are not rows"); + + for (i, cell) in cells.iter().enumerate() { + assert_eq!( + ordinal_of_path(&catalog, None, &filter, false, &cell.remote_path).unwrap(), + Some(i), + "{} is drawn at row {i}", + cell.remote_path + ); + } + } + + #[test] + fn a_photograph_that_is_not_in_the_grid_reports_nothing() { + // Deleted, never scanned, or hidden behind its RAW. All three are the + // same answer, and the caller falls back to the capture time — which is + // only reachable if this says `None` rather than guessing. + let catalog = a_small_library(); + let filter = RatingFilter::default(); + assert_eq!( + ordinal_of_path(&catalog, None, &filter, false, "2019/gone.CR2").unwrap(), + None, + "never heard of it" + ); + assert_eq!( + ordinal_of_path(&catalog, None, &filter, false, "2019/b.JPG").unwrap(), + None, + "shadowed by its RAW" + ); + assert_eq!( + ordinal_of_path(&catalog, None, &filter, false, "2020/e.CR2").unwrap(), + None, + "trashed" + ); + } + + #[test] + fn the_trash_is_ordinalled_through_its_own_list() { + // The trash orders by deletion time and lists exactly what the library + // excludes, so an ordinal taken through the library's ordering would + // name a different photograph — or, here, nothing at all. + let catalog = a_small_library(); + let filter = RatingFilter::default(); + assert_eq!( + ordinal_of_path(&catalog, None, &filter, true, "2020/e.CR2").unwrap(), + Some(0) + ); + assert_eq!( + ordinal_of_path(&catalog, None, &filter, true, "2019/a.CR2").unwrap(), + None, + "a live photograph is not in the trash" + ); + } + + #[test] + fn a_filter_moves_the_ordinal_with_the_grid() { + // TRACES: FR-UI-8 + // A place carries the filter it was recorded under *and* the position, + // and the second is only meaningful under the first. Restoring them in + // the wrong order — position, then filter — would land on a row of a + // list that no longer exists, which is the bug this pairing exists to + // rule out. + let catalog = a_small_library(); + let c = catalog.connection(); + // Three stars on the third photograph only. + c.execute( + "INSERT INTO versions(id, image_id, uuid, name, is_default, rating) + VALUES (1, 4, 'u4', 'default', 1, 3)", + [], + ) + .unwrap(); + + let unfiltered = RatingFilter::default(); + assert_eq!( + ordinal_of_path(&catalog, None, &unfiltered, false, "2020/c.CR2").unwrap(), + Some(2) + ); + + let starred = RatingFilter { + min_rating: 3, + ..RatingFilter::default() + }; + assert_eq!( + ordinal_of_path(&catalog, None, &starred, false, "2020/c.CR2").unwrap(), + Some(0), + "the only survivor of the filter is the first row of it" + ); + assert_eq!( + ordinal_of_path(&catalog, None, &starred, false, "2019/a.CR2").unwrap(), + None, + "filtered out, so it has no position in this grid" + ); + } + + #[test] + fn a_collection_is_ordinalled_through_its_own_scope() { + // The window's parameters are bound ahead of the scope's — see the note + // on `ordinal_of_path`. Getting that order wrong looks up a collection + // by an image id, which fails silently as "not in this grid". + let catalog = a_small_library(); + let c = catalog.connection(); + let coll = + dr_catalog::collections::create(c, "Trip", None, dr_catalog::CollectionKind::Manual) + .unwrap(); + // The second and third photographs, out of order, so position matters. + dr_catalog::collections::add_images(c, coll, &[dr_types::ImageId(4), dr_types::ImageId(2)]) + .unwrap(); + + let filter = RatingFilter::default(); + let cells = read_cells_scoped(&catalog, Some(coll), &filter, 0, 100).unwrap(); + assert_eq!(cells.len(), 2); + for (i, cell) in cells.iter().enumerate() { + assert_eq!( + ordinal_of_path(&catalog, Some(coll), &filter, false, &cell.remote_path).unwrap(), + Some(i), + "{} is drawn at row {i} of the collection", + cell.remote_path + ); + } + assert_eq!( + ordinal_of_path(&catalog, Some(coll), &filter, false, "2019/a.CR2").unwrap(), + None, + "not a member" + ); + } + #[test] fn the_thumbnail_pass_asks_only_for_what_is_missing() { // The work list is the whole point of the pass being resumable and of diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index bafbe6b..3010bab 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -192,6 +192,46 @@ pub struct LibraryController { /// and read here. Shared rather than passed per call because a rescan, a /// scrub and a drop all reload the window and must all honour it. scope: RefCell>, + /// TRACES: FR-UI-8 + /// How to open a photograph in develop. + /// + /// Held so a restored place that was left in develop can put the user back + /// there. The closure is the grid's own — `wire` is handed it and stashes a + /// clone — rather than a second route into the develop view, which would be + /// a second place for "persist the outgoing edit first" to be forgotten. + open_image: RefCell>, + /// Where this library's position is written, once one is open. + /// + /// `None` before a library has been opened, and on the local-files path, + /// which has no library to have a position in. + place_store: RefCell>, + /// What this device calls itself in a place it writes. + /// + /// [`dr_thumbs::ThumbStore::client_id`], so the place and the thumbnail + /// shards a device publishes carry the same name — which is what makes + /// `.darkroom-derived/` readable by a human wondering which machine put + /// what there. Informational only: see [`crate::place`] on why the + /// timestamp, not the device, decides an exchange. + place_device: RefCell, + /// Coalesces the writes a flick would otherwise make one of per event. + place_timer: RefCell>, + /// Whether a place is being applied right now. + /// + /// A restore moves the scope, the filter and the viewport, and every one of + /// those is something [`note_place`] otherwise treats as the photographer + /// having moved. Without this a restore would close its own handover latch + /// and, worse, write the record back out under a fresh timestamp — so the + /// device that had just *adopted* a place from the tablet would claim to be + /// the newer of the two. + applying_place: std::cell::Cell, + /// Whether the photographer has moved since this library was opened. + /// + /// The latch that decides whether a place arriving from another device may + /// still be applied. A handover is only welcome before the user has started + /// working: a grid that jumped somewhere else *while being scrolled*, because + /// a network round-trip finally landed, would be worse than never handing + /// over at all. + place_untouched: std::cell::Cell, /// TRACES: FR-CAT-15 /// Whether the grid is listing the trash rather than the library. /// @@ -388,6 +428,12 @@ impl LibraryController { sync_timer: RefCell::new(None), session: RefCell::new(None), scope: RefCell::new(None), + open_image: RefCell::new(None), + place_store: RefCell::new(None), + place_device: RefCell::new(String::new()), + place_timer: RefCell::new(None), + place_untouched: std::cell::Cell::new(true), + applying_place: std::cell::Cell::new(false), viewing_trash: std::cell::Cell::new(false), coll_ctl: RefCell::new(None), timeline_zoom: RefCell::new(0), @@ -806,6 +852,11 @@ impl LibraryController { /// or a drop that altered the collection being shown. pub fn reload(window: &AppWindow, ctl: &Rc) { load_window(window, ctl); + // TRACES: FR-UI-8 + // The scope changed, or the collection under it did. Either way this is the + // entry point every such change comes through, which is why the record is + // taken here rather than at each of the callers. + note_place(window, ctl); } /// Open a library: show the grid, start a scan, then fill in thumbnails. @@ -834,6 +885,18 @@ pub fn open( let filter = account.format_filter(); *ctl.session.borrow_mut() = Some((conn.clone(), filter.clone())); + // TRACES: FR-UI-8 + // Where this library's position is kept, and what this device signs it + // with. Before the catalog opens, because `adopt_catalog` is what reads the + // record back and it is the next thing to run. + *ctl.place_store.borrow_mut() = Some(crate::place::PlaceStore::open_at(library::place_path( + &conn.account, + ))); + *ctl.place_device.borrow_mut() = device_id(&conn.account); + // A fresh library is one nobody has moved in yet, so a place arriving from + // another device may still be applied — see `place_untouched`. + ctl.place_untouched.set(true); + window.set_show_library(true); window.set_library_open(true); window.set_library_scanning(true); @@ -2028,6 +2091,14 @@ fn open_catalog_for_offline( } } +/// TRACES: FR-UI-8 +/// The grid's own route into the develop view, as something that can be held. +/// +/// Named because it is threaded through the controller as well as passed to +/// `wire`, and a `RefCell>>` written out twice is a +/// signature nobody reads. +type OpenImage = Rc; + /// How long a run of geometry changes has to stop for before the window is /// reloaded against it. /// @@ -2208,7 +2279,43 @@ fn adopt_catalog( // membership — the same order the scan's completion uses. crate::collections_ui::refresh_tree(window, coll_ctl, &cat); *ctl.catalog.borrow_mut() = Some(cat); - load_window(window, ctl); + + // TRACES: FR-UI-8 + // Where the photographer was, before the first window is read rather than + // after. The scope and the filter both change what the grid's query + // returns, so restoring them afterwards would mean reading the top of the + // whole library, drawing it, and then reading again — a visible jump on + // every launch, and on a remote library a screenful of thumbnails fetched + // for photographs nobody asked to see. + // + // The sidebar is refreshed above rather than below for the same ordering + // reason: a scope is restored by naming a row of a tree that has to exist. + let stored = ctl.place_store.borrow().as_ref().and_then(|s| s.load()); + match stored { + Some(place) => apply_place(window, ctl, coll_ctl, &place), + None => load_window(window, ctl), + } +} + +/// TRACES: FR-UI-8 +/// What this device signs a place with. +/// +/// The thumbnail store's client id, which is already this device's name among +/// the clients sharing a library — it is what qualifies a shard's remote +/// filename. Reusing it means the place and the shards a machine publishes +/// carry one name in `.darkroom-derived/`, rather than two that have to be +/// correlated by hand. +/// +/// Empty where the store will not open. The field is informational, so an +/// unsigned place is worth strictly more than no place at all. +fn device_id(account: &Account) -> String { + match dr_thumbs::ThumbStore::open(&library::thumbs_dir(account)) { + Ok(store) => store.client_id().to_string(), + Err(e) => { + log::debug!("no device id for the place record: {e}"); + String::new() + } + } } /// Drop the open catalog, so the next `show_catalog_now` opens the file @@ -4806,6 +4913,338 @@ fn report_position(window: &AppWindow, ctl: &Rc, row: usize) window.set_total(window.get_library_total()); } +// --- where the photographer was (FR-UI-8) -------------------------------- + +/// How long a movement has to settle before it is written down. +/// +/// A flick reports a scroll several times per screenful, and a place is a file +/// write — one per event would put disk I/O inside the gesture NFR-P5 governs, +/// for a record only the *last* of which is ever read. Longer than +/// [`GEOMETRY_SETTLE`] because nothing on screen waits for this: the reload a +/// geometry change schedules is visible, and this is not. +const PLACE_SETTLE: std::time::Duration = std::time::Duration::from_millis(750); + +/// The photographer moved. Write it down, once they stop. +/// +/// Replacing the timer rather than adding one is what makes this a debounce: +/// a `slint::Timer` is stopped by being dropped, so the previous pending write +/// goes with the old handle. +fn note_place(window: &AppWindow, ctl: &Rc) { + if ctl.applying_place.get() { + // A restore is not a movement — see `applying_place`. + return; + } + + // The handover latch closes on the first movement — see `place_untouched`. + ctl.place_untouched.set(false); + + if ctl.place_store.borrow().is_none() { + return; + } + + let timer = slint::Timer::default(); + let weak = window.as_weak(); + let held = ctl.clone(); + timer.start(slint::TimerMode::SingleShot, PLACE_SETTLE, move || { + if let Some(w) = weak.upgrade() { + write_place(&w, &held); + } + }); + *ctl.place_timer.borrow_mut() = Some(timer); +} + +/// Write the place out now, without waiting for a settle. +/// +/// For the moments there may be no "later": leaving the grid for develop, and +/// coming back. Both are single deliberate acts rather than runs of events, and +/// both change the one field a debounce is most likely to lose — which view the +/// user is in. +fn write_place(window: &AppWindow, ctl: &Rc) { + if ctl.applying_place.get() { + return; + } + // Opening a photograph is as much "the photographer has started" as + // scrolling is, and this is the path that records it — so the handover + // latch closes here too, not only in `note_place`. Without it a record + // arriving from another device could pull someone out of the image they had + // just opened. + ctl.place_untouched.set(false); + + let Some(place) = current_place(window, ctl) else { + return; + }; + if let Some(store) = ctl.place_store.borrow().as_ref() { + store.save(&place); + } +} + +/// Where the photographer is, as a record that can be written down. +/// +/// `None` where there is nothing worth recording: no library open, or the +/// launch screen in front of everything. A place is never written from the +/// launch screen — the user is *choosing* a library there, and recording "you +/// were at the launch screen" would make the next launch skip the position it +/// had just been asked to keep. +fn current_place(window: &AppWindow, ctl: &Rc) -> Option { + use dr_types::{Place, PlaceScope, Screen}; + + if window.get_show_launch() || !window.get_library_open() { + return None; + } + + // Which view, and — the same question in both — which photograph. + // + // In the grid it is the first one visible, which `resume_at` already + // tracks; in develop it is the open one, which `report_position` keeps + // `index` holding. Both are library ordinals against the same ordering, so + // the lookup below is one piece of code rather than two. + let in_library = window.get_show_library(); + let at = if in_library { + ctl.resume_at.get() + } else { + window.get_index().max(0) as usize + }; + + // Resolved through the loaded window rather than by a query: the model has + // the path and the capture time side by side already, and this runs on a + // settle after scrolling — the one moment a query is least welcome. + let offset = *ctl.offset.borrow(); + let row = at.checked_sub(offset); + let path = row + .and_then(|r| ctl.paths.borrow().get(r).cloned()) + .unwrap_or_default(); + let captured_at = row.and_then(|r| ctl.captured_at.borrow().get(r).copied().flatten()); + + // The scope, in the vocabulary both devices share. A collection this device + // cannot name is recorded as the whole library rather than as a name + // nothing can resolve — see `collections::uuid_of`. + let scope = if ctl.viewing_trash.get() { + PlaceScope::Trash + } else { + match *ctl.scope.borrow() { + None => PlaceScope::Library, + Some(id) => { + let borrow = ctl.catalog.borrow(); + let uuid = borrow + .as_ref() + .and_then(|c| dr_catalog::collections::uuid_of(c.connection(), id).ok()) + .flatten(); + match uuid { + Some(uuid) => PlaceScope::Collection { uuid }, + None => PlaceScope::Library, + } + } + } + }; + + Some(Place { + at: library::now_secs(), + device: ctl.place_device.borrow().clone(), + screen: if in_library { + Screen::Library + } else { + Screen::Develop + }, + scope, + path, + captured_at, + filter: (&*ctl.filter.borrow()).into(), + }) +} + +/// Put the photographer back where a record says they were. +/// +/// # The order is the whole function +/// +/// Scope, then filter, then position, then — last of all — the view. Each step +/// changes what an ordinal *means*: the grid is a window over a query, and a +/// row resolved before the scope was narrowed names a photograph in a different +/// list. Resolving the path last is what makes the restored position address the +/// grid the user is actually about to see. +/// +/// # Nothing here is allowed to fail loudly +/// +/// Every step degrades to "the state we would have been in anyway". A collection +/// this device has not merged yet leaves the scope at the whole library; a +/// photograph that has been deleted falls back to when it was taken; a capture +/// time that resolves to nothing leaves the grid at the top. A place is a +/// convenience, and an application that would not open because one was stale +/// would be trading something that matters for something that does not. +pub(crate) fn apply_place( + window: &AppWindow, + ctl: &Rc, + coll_ctl: &Rc, + place: &dr_types::Place, +) { + use dr_types::{PlaceScope, Screen}; + + ctl.applying_place.set(true); + + // --- the scope -------------------------------------------------------- + // + // Through the same callback the sidebar fires, rather than by setting the + // two controllers' fields by hand. Both of them hold a scope, and so does + // the header's label and the reorderable flag; there is exactly one piece + // of code that keeps those four in step and this is not the place to write + // a second. + let selected = match &place.scope { + PlaceScope::Library => 0, + PlaceScope::Trash => -1, + PlaceScope::Collection { uuid } => { + let borrow = ctl.catalog.borrow(); + let found = borrow + .as_ref() + .and_then(|c| dr_catalog::collections::id_for_uuid(c.connection(), uuid).ok()) + .flatten(); + drop(borrow); + match found { + Some(id) => id.0 as i32, + // Recorded on another device, and this one has not merged the + // collection yet. The whole library is the honest fallback: an + // empty grid under a name nothing can resolve reads as data + // loss. + None => { + log::info!("the stored place names a collection this device does not have yet"); + 0 + } + } + } + }; + window.invoke_collection_select(selected); + + // --- the filter ------------------------------------------------------- + *ctl.filter.borrow_mut() = (&place.filter).into(); + push_filter(window, ctl); + + // --- the position ----------------------------------------------------- + let resolved = resolve_place(window, ctl, place); + + // The filter and the scope have both moved the window to the top; this is + // what puts it back, and it has to run after them for that reason. + let total = window.get_library_total().max(0) as usize; + let resolved = resolved.filter(|(at, _)| *at < total); + match resolved { + Some((at, _)) => { + ctl.resume_at.set(at); + // Moves the capture-time marker with it — see `resume_position`. + resume_position(window, ctl, coll_ctl, None); + } + None => refilter(window, ctl), + } + + // --- and the view ----------------------------------------------------- + // + // Only where the photograph itself was found. A capture time that merely + // landed in the right week is a good answer for the grid and no answer at + // all for a canvas: develop would open on a path that no longer resolves + // and show a filename over an empty frame. + if place.screen == Screen::Develop { + if let Some((at, Found::Photograph)) = resolved { + if let Some(open) = ctl.open_image.borrow().clone() { + window.set_show_library(false); + window.set_library_roll_centre(true); + let offset = *ctl.offset.borrow(); + if let Some(row) = at.checked_sub(offset) { + window.set_library_roll_current(row as i32); + } + open(place.path.clone()); + // After the open, which resets the readout to "1 of 1" on its + // way in — the same ordering `report_position` documents. + window.set_index(at as i32); + window.set_total(window.get_library_total()); + } + } + } + + // Last, so nothing between here and the top is mistaken for the + // photographer having moved. + ctl.applying_place.set(false); +} + +/// The library ordinal a record names, or `None` if it names nothing here. +/// +/// Two ways, and the second is why a place survives its subject. The path is +/// exact and is tried first. Failing that — the photograph was deleted, moved, +/// or filtered out by the very filter this place restored — the capture time +/// puts the grid in the right week, which is close enough that the user +/// recognises where they are. +fn resolve_place( + window: &AppWindow, + ctl: &Rc, + place: &dr_types::Place, +) -> Option<(usize, Found)> { + let borrow = ctl.catalog.borrow(); + let catalog = borrow.as_ref()?; + + if !place.path.is_empty() { + match library::ordinal_of_path( + catalog, + *ctl.scope.borrow(), + &ctl.filter.borrow(), + ctl.viewing_trash.get(), + &place.path, + ) { + Ok(Some(at)) => return Some((at, Found::Photograph)), + Ok(None) => {} + Err(e) => log::debug!("resolving the stored place: {e}"), + } + } + + let when = place.captured_at?; + drop(borrow); + // Through the scrubber's own route, so "an instant, as an ordinal" has one + // spelling. It sets the viewport and the loaded window together, which is + // exactly what a restore needs, and it anchors the timeline marker on the + // way. + scrub_to(window, ctl, when); + // Where it landed, read back rather than recomputed: `scrub_to` sets the + // offset to the ordinal it resolved and `load_window` then clamps it against + // the end of the library, so the clamped value is the one the grid is + // actually showing. + Some((*ctl.offset.borrow(), Found::Thereabouts)) +} + +/// How exactly a place was resolved. +/// +/// The distinction is what stops a restore reopening develop on the wrong +/// photograph. Landing in the right week is a perfectly good answer for a grid +/// — the user recognises where they are — and no answer at all for a canvas, +/// which would open on a path that no longer resolves and show a filename over +/// an empty frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Found { + /// The photograph itself is still in this grid, at this ordinal. + Photograph, + /// It is not, so this is where its capture time falls instead. + Thereabouts, +} + +/// Put the whole rating filter on screen. +/// +/// The filter bar is drawn from half a dozen properties, each of which is set +/// where its own control is handled — which is right while the user is moving +/// them one at a time, and leaves nothing that can restore all of them at once. +/// A restored place that narrowed the grid without moving the stars would show +/// a filtered library under a bar claiming it was not filtered, and the user's +/// first act would be to try to clear a filter the bar says is off. +fn push_filter(window: &AppWindow, ctl: &Rc) { + let filter = ctl.filter.borrow().clone(); + window.set_library_filter_min_rating(filter.min_rating as i32); + window.set_library_filter_unjudged(filter.unjudged); + // The bar's own numbering, from `on_library_filter_flag_changed`. It has no + // code for `Unflagged` because it cannot produce one — "nothing has judged + // this" is the `unjudged` chip, over both stars and flags — so a place can + // never carry one either, and it falls in with "no flag constraint". + window.set_library_filter_flag(match filter.flag { + Some(dr_types::FlagState::Pick) => 1, + Some(dr_types::FlagState::Reject) => 2, + _ => 0, + }); + window.set_library_local_only(filter.local_only); + ctl.local_only.set(filter.local_only); + push_people_chips(window, ctl); +} + /// Bring the grid back to where the photographer left it, and — coming out of /// develop — put the keyboard cursor on the photograph they were editing. /// @@ -4952,7 +5391,12 @@ pub fn wire( // Shared rather than moved: a click and `Return` both open an image, and // they are two callbacks. - let on_open_image = Rc::new(on_open_image); + let on_open_image: OpenImage = Rc::new(on_open_image); + // TRACES: FR-UI-8 + // And a third caller, which is not a callback at all: a place recorded in + // develop reopens it at launch. Through the same closure, so the edit-saving + // and identity bookkeeping it does are not something a restore can skip. + *ctl.open_image.borrow_mut() = Some(on_open_image.clone()); { let weak = window.as_weak(); @@ -4988,6 +5432,11 @@ pub fn wire( w.set_library_roll_centre(true); on_open_image(path); report_position(&w, &ctl, i as usize); + // TRACES: FR-UI-8 + // Written now rather than on a settle: which view you are in is + // the field a debounce is most likely to lose, and quitting + // straight from develop is exactly the case worth getting right. + write_place(&w, &ctl); } }); } @@ -5012,6 +5461,10 @@ pub fn wire( w.set_library_roll_current(i); on_open_image(path); report_position(&w, &ctl, i as usize); + // TRACES: FR-UI-8 + // Debounced, unlike the two entries into develop: the roll is walked + // frame by frame and every step is a different photograph. + note_place(&w, &ctl); }); } @@ -5056,6 +5509,7 @@ pub fn wire( w.set_library_roll_centre(true); on_open(path); report_position(&w, &ctl, row.unwrap_or(0)); + write_place(&w, &ctl); } }); } @@ -5270,6 +5724,11 @@ pub fn wire( // is only in the Flickable, which is about to be destroyed. ctl.resume_at.set(first_visible); + // TRACES: FR-UI-8 + // And where the *next launch* will look for it. Debounced: a flick + // reports several of these per screenful. + note_place(&w, &ctl); + // The same position where a rebuilt grid will look for it. // // `scroll-to` is read by `seek()`, which runs on a `scroll-token` @@ -5488,6 +5947,10 @@ pub fn wire( resume_position(&w, &ctl, &coll_ctl, Some(open)); w.set_show_library(true); + // TRACES: FR-UI-8 + // After the flag, not before: `current_place` reads it to say which + // view the record is of, and this is the moment it becomes the grid. + write_place(&w, &ctl); }); } @@ -6206,6 +6669,11 @@ fn refilter(window: &AppWindow, ctl: &Rc) { *ctl.offset.borrow_mut() = 0; ctl.requested.borrow_mut().clear(); load_window(window, ctl); + // TRACES: FR-UI-8 + // A narrowed grid is part of where you are: coming back to five-star frames + // of one person and finding the whole library is the same loss of place as + // coming back to the top of it. + note_place(window, ctl); } fn stop(slot: &RefCell>) { diff --git a/ui/dr-ui/src/place.rs b/ui/dr-ui/src/place.rs new file mode 100644 index 0000000..e6ce7cd --- /dev/null +++ b/ui/dr-ui/src/place.rs @@ -0,0 +1,300 @@ +//! TRACES: FR-UI-8 +//! Reading and writing the place — where the photographer was. +//! +//! The record itself is [`dr_types::Place`], which is where the reasoning about +//! what a place *is* lives: why it is not a setting, why it names photographs by +//! remote path and collections by UUID rather than by any local integer, and why +//! the newer of two records simply wins. This module is the half that touches a +//! disk, split off for the reason `dr-types`' manifest gives about +//! `settings.json`: a JSON serialiser in `core/` would be paid for by every +//! crate there. +//! +//! A near-twin of [`crate::settings_store`] in shape, and deliberately not an +//! extension of it. Two files, two lifetimes — signing out forgets a session and +//! must not discard a cache budget; resetting preferences must not forget where +//! you were. + +use std::path::PathBuf; + +use dr_types::{Place, StoredFilter}; + +use crate::library::{PeopleMode, RatingFilter}; + +impl From<&RatingFilter> for StoredFilter { + fn from(f: &RatingFilter) -> Self { + Self { + min_rating: f.min_rating, + unjudged: f.unjudged, + flag: f.flag, + local_only: f.local_only, + captured_from: f.captured_from, + captured_to: f.captured_to, + people: f.people.clone(), + people_all: matches!(f.people_mode, PeopleMode::All), + } + } +} + +impl From<&StoredFilter> for RatingFilter { + fn from(s: &StoredFilter) -> Self { + Self { + min_rating: s.min_rating, + unjudged: s.unjudged, + flag: s.flag, + local_only: s.local_only, + captured_from: s.captured_from, + captured_to: s.captured_to, + people: s.people.clone(), + people_mode: if s.people_all { + PeopleMode::All + } else { + PeopleMode::Any + }, + } + } +} + +/// Parse a record as it travels — the body of the file, local or remote. +/// +/// `None` rather than an error for anything unreadable. A place is advisory: +/// the only thing lost by discarding one is that the grid opens at the top, and +/// an application that refused to start because a position file was truncated +/// would be trading a certainty for an inconvenience. The failure is logged so +/// it is not silent. +pub fn from_bytes(bytes: &[u8], whence: &str) -> Option { + match serde_json::from_slice::(bytes) { + Ok(p) => Some(p), + Err(e) => { + log::warn!("{whence} is not a readable place ({e}); ignoring it"); + None + } + } +} + +/// The bytes that travel, local file and server copy alike. +pub fn to_bytes(place: &Place) -> Result, serde_json::Error> { + serde_json::to_vec_pretty(place) +} + +/// Loads and saves the place, beside the catalog it belongs to. +/// +/// One file per library rather than one file holding every library's place, +/// because that is how everything else per-account is stored — the catalog, the +/// thumbnail shards, the un-uploaded sidecars all hang off +/// `Account::namespace()` — and because the file that travels to the server has +/// to be exactly one library's anyway. A map would need splitting on the way +/// out and merging on the way in, for no gain. +/// +/// In the *data* directory, not the cache one. `library::place_path` explains +/// why that distinction is load-bearing on Android; here it matters less — +/// losing a place costs a scroll — but a file that sat in a directory the +/// system deletes at will would be one that never survived to be useful. +pub struct PlaceStore { + path: PathBuf, +} + +impl PlaceStore { + pub fn open_at(path: PathBuf) -> Self { + Self { path } + } + + /// The stored place, or `None` if there is not one. + /// + /// A missing file is a first run, not a failure, and neither is an + /// unparseable one — see [`from_bytes`]. + pub fn load(&self) -> Option { + match std::fs::read(&self.path) { + Ok(bytes) => from_bytes(&bytes, &self.path.display().to_string()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => { + log::warn!("reading {}: {e}", self.path.display()); + None + } + } + } + + /// Write it, creating the directory if the account is new. + /// + /// Errors are logged and swallowed. This is called from a scroll settling, + /// and a photographer cannot act on "the place could not be written" — the + /// next scroll writes it again, and if the disk is genuinely gone there are + /// louder failures already on screen. + pub fn save(&self, place: &Place) { + if let Some(dir) = self.path.parent() { + if let Err(e) = std::fs::create_dir_all(dir) { + log::warn!("creating {}: {e}", dir.display()); + return; + } + } + let bytes = match to_bytes(place) { + Ok(b) => b, + Err(e) => { + log::warn!("serialising the place: {e}"); + return; + } + }; + if let Err(e) = std::fs::write(&self.path, bytes) { + log::warn!("writing {}: {e}", self.path.display()); + } + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use dr_types::{PlaceScope, Screen}; + + fn dir(name: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("darkroom-place-test-{name}")); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + fn a_place(at: i64) -> Place { + Place { + at, + device: "desktop".into(), + screen: Screen::Develop, + scope: PlaceScope::Collection { + uuid: "0d1f-…-9c".into(), + }, + path: "2019/2019-04-03/DSC_1234.NEF".into(), + captured_at: Some(1_554_300_000), + filter: StoredFilter { + min_rating: 3, + unjudged: false, + flag: Some(dr_types::FlagState::Pick), + local_only: true, + captured_from: Some(1), + captured_to: Some(2), + people: vec![7, 9], + people_all: true, + }, + } + } + + #[test] + fn round_trips_through_the_file() { + let store = PlaceStore::open_at(dir("round-trip").join("place.json")); + let place = a_place(1000); + store.save(&place); + assert_eq!(store.load(), Some(place)); + } + + #[test] + fn a_missing_file_is_a_first_run() { + let store = PlaceStore::open_at(dir("missing").join("place.json")); + assert_eq!(store.load(), None); + } + + #[test] + fn the_directory_is_created_on_the_way_out() { + // A first launch against a new account has no data directory yet, and + // the place is written before anything else in it has been. + let store = PlaceStore::open_at(dir("mkdir").join("deep").join("er").join("place.json")); + store.save(&a_place(1)); + assert!(store.load().is_some()); + } + + #[test] + fn a_torn_file_is_ignored_rather_than_fatal() { + let path = dir("torn").join("place.json"); + std::fs::write(&path, b"{\"at\": 12, \"screen\":").unwrap(); + let store = PlaceStore::open_at(path); + assert_eq!(store.load(), None, "and the app opens at the top instead"); + } + + #[test] + fn a_file_from_a_newer_build_keeps_what_it_can() { + // The additive-change contract: a field this build does not know must + // not cost it the fields it does. + let path = dir("newer").join("place.json"); + std::fs::write( + &path, + br#"{"at": 42, "path": "a/b.NEF", "zoom_level": 3, "screen": "develop"}"#, + ) + .unwrap(); + let got = PlaceStore::open_at(path).load().expect("still readable"); + assert_eq!(got.at, 42); + assert_eq!(got.path, "a/b.NEF"); + assert_eq!(got.screen, Screen::Develop); + } + + #[test] + fn a_file_from_an_older_build_defaults_what_is_missing() { + let path = dir("older").join("place.json"); + std::fs::write(&path, br#"{"at": 42, "path": "a/b.NEF"}"#).unwrap(); + let got = PlaceStore::open_at(path).load().expect("still readable"); + assert_eq!(got.screen, Screen::Library, "the safe view to arrive in"); + assert_eq!(got.scope, PlaceScope::Library); + assert_eq!(got.filter, StoredFilter::default()); + } + + #[test] + fn the_newer_record_wins_in_both_directions() { + let older = a_place(100); + let newer = a_place(200); + assert!(newer.supersedes(Some(&older))); + assert!(!older.supersedes(Some(&newer))); + assert!(older.supersedes(None), "anything beats nothing"); + } + + #[test] + fn an_equal_timestamp_settles_rather_than_ping_pongs() { + // Two idle devices re-reading the same record must not each decide they + // have something to say. + let place = a_place(100); + assert!(!place.supersedes(Some(&place.clone()))); + } + + #[test] + fn the_filter_survives_the_projection() { + // The record and the query must narrow the grid by the same thing, or a + // restored place shows a different set than the one that was left. + let original = RatingFilter { + min_rating: 4, + unjudged: true, + flag: Some(dr_types::FlagState::Reject), + local_only: true, + captured_from: Some(10), + captured_to: Some(20), + people: vec![3, 5, 8], + people_mode: PeopleMode::All, + }; + let back: RatingFilter = (&StoredFilter::from(&original)).into(); + assert_eq!(back, original); + + let empty = RatingFilter::default(); + let back: RatingFilter = (&StoredFilter::from(&empty)).into(); + assert_eq!(back, empty); + assert!(back.is_unfiltered()); + } + + #[test] + fn a_scope_travels_as_a_uuid() { + // The guard against someone reaching for `collections.id`, which the + // schema calls local and colliding across devices. + let json = serde_json::to_string(&PlaceScope::Collection { + uuid: "abc-123".into(), + }) + .unwrap(); + assert!(json.contains("abc-123"), "{json}"); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + PlaceScope::Collection { + uuid: "abc-123".into() + } + ); + } + + #[test] + fn the_trash_is_its_own_scope() { + let json = serde_json::to_string(&PlaceScope::Trash).unwrap(); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + PlaceScope::Trash + ); + } +}