From d04087af83609c91d1995fa14051794016806cc5 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 22 Aug 2026 21:09:26 +0200 Subject: [PATCH] Import into the library, which is on the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was a local destination, defaulting to ~/Pictures, and an "upload" switch that could be turned off. That was wrong twice over. DarkRoom's library *is* a folder on a Nextcloud server (FR-NC-6) — there is no local library — so a user-chosen local destination built a second pile of photographs that no view in the application ever lists, and made "where did my import go" a question with two answers. An import now has exactly one destination and the page asks nothing about it. With no account there is nowhere to go at all, so Import is refused rather than quietly filling a folder. What lands on the device is a staging copy in a directory the app owns, the same shape `export` uses for its outbox and for the reason its module docs give: staging first is the only path, not a fallback for being offline. The bytes have to reach disk before the network — streaming a card straight to the server would let a move-import erase a card against an in-flight upload, and would make importing impossible with no connection (FR-NC-10). A staged file is removed once the server confirms it; one that is not confirmed stays queued, and the next import drains it. And the rule that was stated but never enforced: `retirable` was reported and `dr_ingest::retire` was never called by anything, so a move-import silently behaved as a copy. The card is now emptied by the worker, of exactly those photographs the *server* has confirmed — not those merely written here, because the staging copy is removed moments later and anything unconfirmed would then exist nowhere at all. FR-NC-7b said "copied locally first ... then queued for upload", which is a staging area; it has been rewritten to say so in terms that do not also permit what was built. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-types/src/settings.rs | 26 ++-- docs/requirements.md | 29 ++++- docs/traceability.md | 44 +++---- ui/dr-ui/src/import.rs | 224 ++++++++++++++++++++++++++++++++-- ui/dr-ui/src/import_ui.rs | 75 +++++++----- ui/dr-ui/src/lib.rs | 1 + ui/dr-ui/ui/app.slint | 2 - ui/dr-ui/ui/import.slint | 39 +++--- 8 files changed, 336 insertions(+), 104 deletions(-) diff --git a/core/dr-types/src/settings.rs b/core/dr-types/src/settings.rs index b5aaeea..f2baf62 100644 --- a/core/dr-types/src/settings.rs +++ b/core/dr-types/src/settings.rs @@ -62,9 +62,14 @@ pub struct Settings { /// TRACES: FR-CAT-10 | FR-NC-7a /// What an import defaults to, remembered between cards. /// -/// A photographer imports the same way every time — same folders, same -/// destination, same answer about the card — and retyping a template on every -/// card is the kind of friction that makes people stop using the importer. +/// A photographer imports the same way every time — same folders, same answer +/// about the card — and retyping a template on every card is the kind of +/// friction that makes people stop using the importer. +/// +/// **There is no destination here.** The library is the folder on the server +/// (FR-NC-6), so an import has exactly one place to go and nothing to ask. +/// What lands on this machine is a staging copy the app owns and removes once +/// the server confirms it (FR-NC-7b), which is not a setting either. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct ImportSettings { @@ -76,15 +81,13 @@ pub struct ImportSettings { /// defined — so it owns the default too, and this records only a /// deliberate departure from it. pub folder_template: String, - /// Where originals are copied to on this machine. Empty until chosen. - /// - /// A local folder even on a library that lives on a server: FR-NC-7b has - /// the bytes land and be verified here first, and the upload follow. - pub destination: String, /// The optional second copy (FR-CAT-10). Empty means none. + /// + /// The only local path an import has, and deliberately so: the library + /// lives on the server, so there is no *first* destination for the user to + /// choose. This is an extra copy kept somewhere of their choosing — an + /// external drive, another disk — and nothing reads it back. pub backup: String, - /// Whether to send imported originals on to the server (FR-NC-7b). - pub upload: bool, /// Whether the card is to be emptied once everything is confirmed. /// /// **Defaults to false and is the one setting worth not remembering @@ -109,9 +112,7 @@ impl Default for ImportSettings { fn default() -> Self { Self { folder_template: String::new(), - destination: String::new(), backup: String::new(), - upload: true, // The one irreversible thing this application does starts off. move_from_card: false, verify: true, @@ -1136,7 +1137,6 @@ mod import_settings_tests { let s: Settings = serde_json::from_str(stored).unwrap(); assert!(s.import.verify); assert!(s.import.skip_duplicates); - assert!(s.import.upload); // And the irreversible one stays off. assert!(!s.import.move_from_card); } diff --git a/docs/requirements.md b/docs/requirements.md index 950d262..62fdf1a 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -654,14 +654,31 @@ Two consequences the mechanics do not give for free: Sidecars follow their image, not the template: they are named from `oc:fileid` (FR-NC-8) and live beside the file they describe. -**FR-NC-7b — Ingest to remote.** Import (FR-CAT-10) and upload (FR-NC-7) shall compose: a card -import may target a Nextcloud library, in which case files are copied locally first, verified by -checksum, and only then queued for upload. The local copy is not deleted on a move-import until -the upload is confirmed — a card erased against an in-flight upload is unrecoverable, which is the -one failure in this app with no undo. +**FR-NC-7b — Ingest to remote.** Import (FR-CAT-10) and upload (FR-NC-7) compose, and the library +an import targets is **always** the remote one. There is no local library for a card to land in: +FR-NC-6 puts the library on the server, so an import has exactly one destination and the interface +shall not offer a choice of another. + +What lands on the device is a **staging copy**, in a location the app owns, and it is not a +library: no view lists it, and it is removed once the server confirms the file. A staged file the +server did not take remains queued, and a later import drains the queue. An import with no +reachable server therefore succeeds and defers, rather than failing (FR-NC-10); an import with no +*account* has nowhere to go at all and shall be refused. + +The bytes shall reach the device before they reach the network. Streaming a card straight to the +server would make a move-import erase a card against an in-flight upload, and would make importing +impossible offline. + +**On erasing the card.** A move-import shall delete from the card only those photographs the +**server has confirmed** — not those merely written to the staging copy, since that copy is removed +as soon as the upload succeeds and would otherwise be the only remaining copy. Anything that does +not upload keeps its card copy. A card erased against an unfinished upload is unrecoverable, which +is the one failure in this app with no undo. Duplicate detection (FR-CAT-11) runs against the catalog before upload, so re-inserting a card -that was already imported transfers nothing. +that was already imported transfers nothing. Where the catalog cannot answer — a file already in +the destination folder, locally or on the server — the folder's own listing shall answer instead, +so a re-import neither duplicates nor renames what is already held. **FR-NC-8 — Edit metadata sync.** Edit graphs sync bidirectionally as sidecars, one per image, named deterministically from `oc:fileid`. Each sidecar carries a monotonic revision counter, a diff --git a/docs/traceability.md b/docs/traceability.md index e5e516f..e9008bb 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 | 180 | -| TRACES tags found | 516 | +| TRACES tags found | 519 | | Requirements defined | 177 | | Requirements covered | 89 | | **Coverage** | **50.3%** (89/177) | @@ -34,25 +34,25 @@ _None._ | ID | Tagged in | |---|---| | FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`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-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:198`](../core/dr-types/src/lib.rs#L198), [`core/dr-types/src/lib.rs:267`](../core/dr-types/src/lib.rs#L267), [`core/dr-types/src/lib.rs:300`](../core/dr-types/src/lib.rs#L300), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) | -| 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:62`](../core/dr-types/src/settings.rs#L62), [`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:916`](../ui/dr-ui/src/lib.rs#L916), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:692`](../ui/dr-ui/ui/library.slint#L692), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:990`](../ui/dr-ui/ui/library.slint#L990) | +| 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:62`](../core/dr-types/src/settings.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:916`](../ui/dr-ui/src/lib.rs#L916), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:692`](../ui/dr-ui/ui/library.slint#L692), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:990`](../ui/dr-ui/ui/library.slint#L990) | | 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:916`](../ui/dr-ui/src/lib.rs#L916), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2052`](../ui/dr-ui/src/library.rs#L2052), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111) | | FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) | | FR-CAT-15 | [`core/dr-catalog/src/schema.rs:365`](../core/dr-catalog/src/schema.rs#L365), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`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), [`ui/dr-ui/src/collections_ui.rs:1171`](../ui/dr-ui/src/collections_ui.rs#L1171), [`ui/dr-ui/src/collections_ui.rs:1846`](../ui/dr-ui/src/collections_ui.rs#L1846), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:2933`](../ui/dr-ui/src/library.rs#L2933), [`ui/dr-ui/src/library.rs:2965`](../ui/dr-ui/src/library.rs#L2965), [`ui/dr-ui/src/library_ui.rs:128`](../ui/dr-ui/src/library_ui.rs#L128), [`ui/dr-ui/src/library_ui.rs:643`](../ui/dr-ui/src/library_ui.rs#L643), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:51`](../core/dr-types/src/lib.rs#L51) | | 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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | -| 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:373`](../ui/dr-ui/src/import.rs#L373), [`ui/dr-ui/src/import.rs:398`](../ui/dr-ui/src/import.rs#L398), [`ui/dr-ui/src/library.rs:2528`](../ui/dr-ui/src/library.rs#L2528), [`ui/dr-ui/src/library.rs:2554`](../ui/dr-ui/src/library.rs#L2554), [`ui/dr-ui/src/library_ui.rs:115`](../ui/dr-ui/src/library_ui.rs#L115), [`ui/dr-ui/src/library_ui.rs:3339`](../ui/dr-ui/src/library_ui.rs#L3339), [`ui/dr-ui/src/library_ui.rs:4113`](../ui/dr-ui/src/library_ui.rs#L4113), [`ui/dr-ui/ui/app.slint:528`](../ui/dr-ui/ui/app.slint#L528), [`ui/dr-ui/ui/settings.slint:316`](../ui/dr-ui/ui/settings.slint#L316), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | +| 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:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2528`](../ui/dr-ui/src/library.rs#L2528), [`ui/dr-ui/src/library.rs:2554`](../ui/dr-ui/src/library.rs#L2554), [`ui/dr-ui/src/library_ui.rs:115`](../ui/dr-ui/src/library_ui.rs#L115), [`ui/dr-ui/src/library_ui.rs:3339`](../ui/dr-ui/src/library_ui.rs#L3339), [`ui/dr-ui/src/library_ui.rs:4113`](../ui/dr-ui/src/library_ui.rs#L4113), [`ui/dr-ui/ui/app.slint:528`](../ui/dr-ui/ui/app.slint#L528), [`ui/dr-ui/ui/settings.slint:316`](../ui/dr-ui/ui/settings.slint#L316), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | 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), [`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) | | 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:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:401`](../core/dr-decode/src/lib.rs#L401), [`core/dr-pipeline/src/sidecar.rs:128`](../core/dr-pipeline/src/sidecar.rs#L128), [`ui/dr-ui/src/library_ui.rs:5391`](../ui/dr-ui/src/library_ui.rs#L5391), [`ui/dr-ui/src/library_ui.rs:5402`](../ui/dr-ui/src/library_ui.rs#L5402), [`ui/dr-ui/src/library_ui.rs:5415`](../ui/dr-ui/src/library_ui.rs#L5415), [`ui/dr-ui/src/library_ui.rs:5430`](../ui/dr-ui/src/library_ui.rs#L5430), [`ui/dr-ui/src/library_ui.rs:5439`](../ui/dr-ui/src/library_ui.rs#L5439), [`ui/dr-ui/ui/app.slint:600`](../ui/dr-ui/ui/app.slint#L600), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:685`](../ui/dr-ui/ui/library.slint#L685), [`ui/dr-ui/ui/library.slint:736`](../ui/dr-ui/ui/library.slint#L736) | | 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:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/ui/app.slint:600`](../ui/dr-ui/ui/app.slint#L600), [`ui/dr-ui/ui/library.slint:736`](../ui/dr-ui/ui/library.slint#L736) | | 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:1544`](../ui/dr-ui/src/collections_ui.rs#L1544), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`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:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/library.slint:726`](../ui/dr-ui/ui/library.slint#L726) | -| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:141`](../core/dr-pipeline/src/ops/curve.rs#L141), [`core/dr-pipeline/src/ops/curve.rs:657`](../core/dr-pipeline/src/ops/curve.rs#L657), [`core/dr-pipeline/src/sidecar.rs:1276`](../core/dr-pipeline/src/sidecar.rs#L1276), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/tests/tone_curve.rs:32`](../core/dr-pipeline/tests/tone_curve.rs#L32), [`ui/dr-ui/src/develop.rs:2237`](../ui/dr-ui/src/develop.rs#L2237), [`ui/dr-ui/src/export.rs:749`](../ui/dr-ui/src/export.rs#L749), [`ui/dr-ui/src/lib.rs:1140`](../ui/dr-ui/src/lib.rs#L1140), [`ui/dr-ui/src/lib.rs:1480`](../ui/dr-ui/src/lib.rs#L1480), [`ui/dr-ui/src/lib.rs:1585`](../ui/dr-ui/src/lib.rs#L1585), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:768`](../ui/dr-ui/src/lib.rs#L768), [`ui/dr-ui/src/library.rs:1475`](../ui/dr-ui/src/library.rs#L1475), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:398`](../ui/dr-ui/src/library.rs#L398), [`ui/dr-ui/src/library.rs:626`](../ui/dr-ui/src/library.rs#L626), [`ui/dr-ui/src/library_ui.rs:4402`](../ui/dr-ui/src/library_ui.rs#L4402), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:141`](../core/dr-pipeline/src/ops/curve.rs#L141), [`core/dr-pipeline/src/ops/curve.rs:657`](../core/dr-pipeline/src/ops/curve.rs#L657), [`core/dr-pipeline/src/sidecar.rs:1276`](../core/dr-pipeline/src/sidecar.rs#L1276), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/tests/tone_curve.rs:32`](../core/dr-pipeline/tests/tone_curve.rs#L32), [`ui/dr-ui/src/develop.rs:2237`](../ui/dr-ui/src/develop.rs#L2237), [`ui/dr-ui/src/export.rs:749`](../ui/dr-ui/src/export.rs#L749), [`ui/dr-ui/src/lib.rs:1141`](../ui/dr-ui/src/lib.rs#L1141), [`ui/dr-ui/src/lib.rs:1481`](../ui/dr-ui/src/lib.rs#L1481), [`ui/dr-ui/src/lib.rs:1586`](../ui/dr-ui/src/lib.rs#L1586), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:768`](../ui/dr-ui/src/lib.rs#L768), [`ui/dr-ui/src/library.rs:1475`](../ui/dr-ui/src/library.rs#L1475), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:398`](../ui/dr-ui/src/library.rs#L398), [`ui/dr-ui/src/library.rs:626`](../ui/dr-ui/src/library.rs#L626), [`ui/dr-ui/src/library_ui.rs:4402`](../ui/dr-ui/src/library_ui.rs#L4402), [`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:337`](../core/dr-catalog/src/schema.rs#L337), [`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:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:117`](../core/dr-types/src/lib.rs#L117), [`ui/dr-ui/src/develop.rs:1945`](../ui/dr-ui/src/develop.rs#L1945), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1435`](../ui/dr-ui/src/library.rs#L1435), [`ui/dr-ui/src/library.rs:1512`](../ui/dr-ui/src/library.rs#L1512), [`ui/dr-ui/src/library.rs:196`](../ui/dr-ui/src/library.rs#L196), [`ui/dr-ui/src/library.rs:3148`](../ui/dr-ui/src/library.rs#L3148), [`ui/dr-ui/src/library.rs:398`](../ui/dr-ui/src/library.rs#L398), [`ui/dr-ui/src/library.rs:610`](../ui/dr-ui/src/library.rs#L610), [`ui/dr-ui/src/library.rs:626`](../ui/dr-ui/src/library.rs#L626), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library_ui.rs:1450`](../ui/dr-ui/src/library_ui.rs#L1450), [`ui/dr-ui/src/library_ui.rs:1476`](../ui/dr-ui/src/library_ui.rs#L1476), [`ui/dr-ui/src/library_ui.rs:1492`](../ui/dr-ui/src/library_ui.rs#L1492), [`ui/dr-ui/src/library_ui.rs:1586`](../ui/dr-ui/src/library_ui.rs#L1586), [`ui/dr-ui/src/library_ui.rs:170`](../ui/dr-ui/src/library_ui.rs#L170), [`ui/dr-ui/src/library_ui.rs:203`](../ui/dr-ui/src/library_ui.rs#L203), [`ui/dr-ui/src/library_ui.rs:2119`](../ui/dr-ui/src/library_ui.rs#L2119), [`ui/dr-ui/src/library_ui.rs:2548`](../ui/dr-ui/src/library_ui.rs#L2548), [`ui/dr-ui/src/library_ui.rs:2730`](../ui/dr-ui/src/library_ui.rs#L2730), [`ui/dr-ui/src/library_ui.rs:3011`](../ui/dr-ui/src/library_ui.rs#L3011), [`ui/dr-ui/src/library_ui.rs:3083`](../ui/dr-ui/src/library_ui.rs#L3083), [`ui/dr-ui/src/library_ui.rs:3248`](../ui/dr-ui/src/library_ui.rs#L3248), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:358`](../ui/dr-ui/src/library_ui.rs#L358), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/library_ui.rs:4632`](../ui/dr-ui/src/library_ui.rs#L4632), [`ui/dr-ui/src/library_ui.rs:4747`](../ui/dr-ui/src/library_ui.rs#L4747), [`ui/dr-ui/src/presets.rs:284`](../ui/dr-ui/src/presets.rs#L284), [`ui/dr-ui/src/presets.rs:296`](../ui/dr-ui/src/presets.rs#L296), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) | -| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:373`](../ui/dr-ui/src/import.rs#L373) | +| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:128`](../core/dr-pipeline/src/sidecar.rs#L128), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:318`](../core/dr-pipeline/src/operation.rs#L318) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:395`](../core/dr-gpu/src/adjust.rs#L395), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:121`](../core/dr-pipeline/src/graph.rs#L121), [`core/dr-pipeline/src/graph.rs:394`](../core/dr-pipeline/src/graph.rs#L394), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:439`](../core/dr-pipeline/src/operation.rs#L439), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:223`](../core/dr-pipeline/src/ops/curve.rs#L223), [`core/dr-pipeline/src/ops/curve.rs:636`](../core/dr-pipeline/src/ops/curve.rs#L636), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1276`](../core/dr-pipeline/src/sidecar.rs#L1276), [`core/dr-pipeline/src/sidecar.rs:1328`](../core/dr-pipeline/src/sidecar.rs#L1328), [`core/dr-pipeline/src/sidecar.rs:149`](../core/dr-pipeline/src/sidecar.rs#L149), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1298`](../ui/dr-ui/src/develop.rs#L1298), [`ui/dr-ui/src/develop.rs:1313`](../ui/dr-ui/src/develop.rs#L1313), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1418`](../ui/dr-ui/src/develop.rs#L1418), [`ui/dr-ui/src/develop.rs:1492`](../ui/dr-ui/src/develop.rs#L1492), [`ui/dr-ui/src/develop.rs:243`](../ui/dr-ui/src/develop.rs#L243), [`ui/dr-ui/src/develop.rs:2595`](../ui/dr-ui/src/develop.rs#L2595), [`ui/dr-ui/src/develop.rs:2649`](../ui/dr-ui/src/develop.rs#L2649), [`ui/dr-ui/src/develop.rs:276`](../ui/dr-ui/src/develop.rs#L276), [`ui/dr-ui/src/develop.rs:830`](../ui/dr-ui/src/develop.rs#L830), [`ui/dr-ui/src/lib.rs:1176`](../ui/dr-ui/src/lib.rs#L1176), [`ui/dr-ui/src/lib.rs:1828`](../ui/dr-ui/src/lib.rs#L1828), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/masks_ui.rs:217`](../ui/dr-ui/src/masks_ui.rs#L217), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:700`](../ui/dr-ui/src/masks_ui.rs#L700), [`ui/dr-ui/src/masks_ui.rs:814`](../ui/dr-ui/src/masks_ui.rs#L814), [`ui/dr-ui/ui/app.slint:1884`](../ui/dr-ui/ui/app.slint#L1884) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:395`](../core/dr-gpu/src/adjust.rs#L395), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:121`](../core/dr-pipeline/src/graph.rs#L121), [`core/dr-pipeline/src/graph.rs:394`](../core/dr-pipeline/src/graph.rs#L394), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:439`](../core/dr-pipeline/src/operation.rs#L439), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:223`](../core/dr-pipeline/src/ops/curve.rs#L223), [`core/dr-pipeline/src/ops/curve.rs:636`](../core/dr-pipeline/src/ops/curve.rs#L636), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1276`](../core/dr-pipeline/src/sidecar.rs#L1276), [`core/dr-pipeline/src/sidecar.rs:1328`](../core/dr-pipeline/src/sidecar.rs#L1328), [`core/dr-pipeline/src/sidecar.rs:149`](../core/dr-pipeline/src/sidecar.rs#L149), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1298`](../ui/dr-ui/src/develop.rs#L1298), [`ui/dr-ui/src/develop.rs:1313`](../ui/dr-ui/src/develop.rs#L1313), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1418`](../ui/dr-ui/src/develop.rs#L1418), [`ui/dr-ui/src/develop.rs:1492`](../ui/dr-ui/src/develop.rs#L1492), [`ui/dr-ui/src/develop.rs:243`](../ui/dr-ui/src/develop.rs#L243), [`ui/dr-ui/src/develop.rs:2595`](../ui/dr-ui/src/develop.rs#L2595), [`ui/dr-ui/src/develop.rs:2649`](../ui/dr-ui/src/develop.rs#L2649), [`ui/dr-ui/src/develop.rs:276`](../ui/dr-ui/src/develop.rs#L276), [`ui/dr-ui/src/develop.rs:830`](../ui/dr-ui/src/develop.rs#L830), [`ui/dr-ui/src/lib.rs:1177`](../ui/dr-ui/src/lib.rs#L1177), [`ui/dr-ui/src/lib.rs:1829`](../ui/dr-ui/src/lib.rs#L1829), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/masks_ui.rs:217`](../ui/dr-ui/src/masks_ui.rs#L217), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:700`](../ui/dr-ui/src/masks_ui.rs#L700), [`ui/dr-ui/src/masks_ui.rs:814`](../ui/dr-ui/src/masks_ui.rs#L814), [`ui/dr-ui/ui/app.slint:1882`](../ui/dr-ui/ui/app.slint#L1882) | | FR-DEV-3a | [`core/dr-pipeline/build.rs:1804`](../core/dr-pipeline/build.rs#L1804), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:185`](../core/dr-pipeline/src/graph.rs#L185), [`core/dr-pipeline/src/graph.rs:19`](../core/dr-pipeline/src/graph.rs#L19), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/mask.rs:950`](../core/dr-pipeline/src/mask.rs#L950), [`core/dr-pipeline/src/operation.rs:294`](../core/dr-pipeline/src/operation.rs#L294), [`core/dr-pipeline/src/ops/curve.rs:323`](../core/dr-pipeline/src/ops/curve.rs#L323), [`ui/dr-ui/src/develop.rs:727`](../ui/dr-ui/src/develop.rs#L727), [`ui/dr-ui/src/lib.rs:526`](../ui/dr-ui/src/lib.rs#L526) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/operation.rs:294`](../core/dr-pipeline/src/operation.rs#L294) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:1804`](../core/dr-pipeline/build.rs#L1804), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:185`](../core/dr-pipeline/src/graph.rs#L185), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/mask.rs:950`](../core/dr-pipeline/src/mask.rs#L950), [`ui/dr-ui/src/develop.rs:3228`](../ui/dr-ui/src/develop.rs#L3228) | @@ -60,20 +60,20 @@ _None._ | FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:702`](../core/dr-decode/src/lib.rs#L702), [`core/dr-decode/src/lib.rs:742`](../core/dr-decode/src/lib.rs#L742), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:690`](../core/dr-gpu/src/adjust.rs#L690), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1344`](../core/dr-pipeline/src/operation.rs#L1344), [`core/dr-pipeline/src/operation.rs:1369`](../core/dr-pipeline/src/operation.rs#L1369), [`core/dr-pipeline/src/operation.rs:1384`](../core/dr-pipeline/src/operation.rs#L1384), [`core/dr-pipeline/src/operation.rs:1405`](../core/dr-pipeline/src/operation.rs#L1405), [`core/dr-pipeline/src/operation.rs:369`](../core/dr-pipeline/src/operation.rs#L369), [`core/dr-pipeline/src/operation.rs:379`](../core/dr-pipeline/src/operation.rs#L379), [`core/dr-pipeline/src/operation.rs:513`](../core/dr-pipeline/src/operation.rs#L513) | | FR-DEV-3h | [`core/dr-decode/src/lib.rs:401`](../core/dr-decode/src/lib.rs#L401), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:334`](../core/dr-types/src/lib.rs#L334) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | -| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2253`](../ui/dr-ui/src/develop.rs#L2253), [`ui/dr-ui/src/develop.rs:225`](../ui/dr-ui/src/develop.rs#L225), [`ui/dr-ui/src/develop.rs:2263`](../ui/dr-ui/src/develop.rs#L2263), [`ui/dr-ui/src/lib.rs:1168`](../ui/dr-ui/src/lib.rs#L1168) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:127`](../core/dr-types/src/settings.rs#L127), [`ui/dr-ui/src/develop.rs:2214`](../ui/dr-ui/src/develop.rs#L2214), [`ui/dr-ui/src/develop.rs:2224`](../ui/dr-ui/src/develop.rs#L2224), [`ui/dr-ui/src/lib.rs:1140`](../ui/dr-ui/src/lib.rs#L1140), [`ui/dr-ui/src/library.rs:1475`](../ui/dr-ui/src/library.rs#L1475), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:368`](../ui/dr-ui/src/library.rs#L368), [`ui/dr-ui/src/library_ui.rs:2379`](../ui/dr-ui/src/library_ui.rs#L2379), [`ui/dr-ui/src/library_ui.rs:2730`](../ui/dr-ui/src/library_ui.rs#L2730), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1105`](../ui/dr-ui/ui/library.slint#L1105), [`ui/dr-ui/ui/library.slint:666`](../ui/dr-ui/ui/library.slint#L666), [`ui/dr-ui/ui/library.slint:747`](../ui/dr-ui/ui/library.slint#L747), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) | +| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2253`](../ui/dr-ui/src/develop.rs#L2253), [`ui/dr-ui/src/develop.rs:225`](../ui/dr-ui/src/develop.rs#L225), [`ui/dr-ui/src/develop.rs:2263`](../ui/dr-ui/src/develop.rs#L2263), [`ui/dr-ui/src/lib.rs:1169`](../ui/dr-ui/src/lib.rs#L1169) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:128`](../core/dr-types/src/settings.rs#L128), [`ui/dr-ui/src/develop.rs:2214`](../ui/dr-ui/src/develop.rs#L2214), [`ui/dr-ui/src/develop.rs:2224`](../ui/dr-ui/src/develop.rs#L2224), [`ui/dr-ui/src/lib.rs:1141`](../ui/dr-ui/src/lib.rs#L1141), [`ui/dr-ui/src/library.rs:1475`](../ui/dr-ui/src/library.rs#L1475), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:368`](../ui/dr-ui/src/library.rs#L368), [`ui/dr-ui/src/library_ui.rs:2379`](../ui/dr-ui/src/library_ui.rs#L2379), [`ui/dr-ui/src/library_ui.rs:2730`](../ui/dr-ui/src/library_ui.rs#L2730), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1105`](../ui/dr-ui/ui/library.slint#L1105), [`ui/dr-ui/ui/library.slint:666`](../ui/dr-ui/ui/library.slint#L666), [`ui/dr-ui/ui/library.slint:747`](../ui/dr-ui/ui/library.slint#L747), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) | | FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265) | | FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:1783`](../core/dr-gpu/src/adjust.rs#L1783), [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:1945`](../core/dr-gpu/src/adjust.rs#L1945), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:204`](../core/dr-gpu/tests/capture_sharpen.rs#L204), [`core/dr-gpu/tests/detail_stage.rs:322`](../core/dr-gpu/tests/detail_stage.rs#L322), [`core/dr-gpu/tests/local_contrast.rs:254`](../core/dr-gpu/tests/local_contrast.rs#L254), [`core/dr-gpu/tests/noise_reduction.rs:365`](../core/dr-gpu/tests/noise_reduction.rs#L365), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:368`](../core/dr-pipeline/src/graph.rs#L368), [`core/dr-pipeline/src/graph.rs:394`](../core/dr-pipeline/src/graph.rs#L394), [`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:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`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:655`](../core/dr-pipeline/src/ops/local_contrast.rs#L655), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:1781`](../ui/dr-ui/src/develop.rs#L1781), [`ui/dr-ui/src/develop.rs:2401`](../ui/dr-ui/src/develop.rs#L2401), [`ui/dr-ui/src/develop.rs:2773`](../ui/dr-ui/src/develop.rs#L2773), [`ui/dr-ui/src/develop.rs:2807`](../ui/dr-ui/src/develop.rs#L2807), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:667`](../ui/dr-ui/src/lib.rs#L667) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) | -| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:1826`](../ui/dr-ui/src/develop.rs#L1826), [`ui/dr-ui/src/develop.rs:236`](../ui/dr-ui/src/develop.rs#L236), [`ui/dr-ui/src/develop.rs:3875`](../ui/dr-ui/src/develop.rs#L3875), [`ui/dr-ui/src/develop.rs:3907`](../ui/dr-ui/src/develop.rs#L3907), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1227`](../ui/dr-ui/src/lib.rs#L1227), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:305`](../ui/dr-ui/ui/app.slint#L305), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | +| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:1826`](../ui/dr-ui/src/develop.rs#L1826), [`ui/dr-ui/src/develop.rs:236`](../ui/dr-ui/src/develop.rs#L236), [`ui/dr-ui/src/develop.rs:3875`](../ui/dr-ui/src/develop.rs#L3875), [`ui/dr-ui/src/develop.rs:3907`](../ui/dr-ui/src/develop.rs#L3907), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1228`](../ui/dr-ui/src/lib.rs#L1228), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:305`](../ui/dr-ui/ui/app.slint#L305), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2093`](../core/dr-gpu/src/adjust.rs#L2093), [`core/dr-pipeline/src/graph.rs:358`](../core/dr-pipeline/src/graph.rs#L358), [`core/dr-pipeline/src/graph.rs:404`](../core/dr-pipeline/src/graph.rs#L404), [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:540`](../core/dr-types/src/settings.rs#L540), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2093`](../core/dr-gpu/src/adjust.rs#L2093), [`core/dr-pipeline/src/graph.rs:358`](../core/dr-pipeline/src/graph.rs#L358), [`core/dr-pipeline/src/graph.rs:404`](../core/dr-pipeline/src/graph.rs#L404), [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:541`](../core/dr-types/src/settings.rs#L541), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | 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:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:561`](../ui/dr-ui/src/settings_ui.rs#L561) | -| 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:941`](../ui/dr-ui/src/export.rs#L941), [`ui/dr-ui/src/lib.rs:1751`](../ui/dr-ui/src/lib.rs#L1751), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library_ui.rs:3100`](../ui/dr-ui/src/library_ui.rs#L3100), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:5373`](../ui/dr-ui/src/library_ui.rs#L5373), [`ui/dr-ui/src/library_ui.rs:5450`](../ui/dr-ui/src/library_ui.rs#L5450), [`ui/dr-ui/src/library_ui.rs:5462`](../ui/dr-ui/src/library_ui.rs#L5462), [`ui/dr-ui/src/library_ui.rs:546`](../ui/dr-ui/src/library_ui.rs#L546), [`ui/dr-ui/src/library_ui.rs:603`](../ui/dr-ui/src/library_ui.rs#L603), [`ui/dr-ui/ui/app.slint:1353`](../ui/dr-ui/ui/app.slint#L1353), [`ui/dr-ui/ui/app.slint:992`](../ui/dr-ui/ui/app.slint#L992), [`ui/dr-ui/ui/library.slint:1113`](../ui/dr-ui/ui/library.slint#L1113), [`ui/dr-ui/ui/library.slint:670`](../ui/dr-ui/ui/library.slint#L670), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) | -| 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:1163`](../core/dr-decode/src/locate.rs#L1163), [`core/dr-decode/src/locate.rs:1221`](../core/dr-decode/src/locate.rs#L1221), [`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:119`](../core/dr-export/src/encode.rs#L119), [`core/dr-export/src/encode.rs:163`](../core/dr-export/src/encode.rs#L163), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:208`](../core/dr-export/src/encode.rs#L208), [`core/dr-export/src/encode.rs:237`](../core/dr-export/src/encode.rs#L237), [`core/dr-export/src/encode.rs:310`](../core/dr-export/src/encode.rs#L310), [`core/dr-export/src/encode.rs:324`](../core/dr-export/src/encode.rs#L324), [`core/dr-export/src/encode.rs:404`](../core/dr-export/src/encode.rs#L404), [`core/dr-export/src/encode.rs:452`](../core/dr-export/src/encode.rs#L452), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:791`](../core/dr-export/src/encode.rs#L791), [`core/dr-export/src/encode.rs:805`](../core/dr-export/src/encode.rs#L805), [`core/dr-export/src/encode.rs:846`](../core/dr-export/src/encode.rs#L846), [`core/dr-export/src/encode.rs:894`](../core/dr-export/src/encode.rs#L894), [`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:442`](../core/dr-types/src/lib.rs#L442), [`core/dr-types/src/settings.rs:258`](../core/dr-types/src/settings.rs#L258), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:776`](../ui/dr-ui/src/export.rs#L776), [`ui/dr-ui/src/export.rs:793`](../ui/dr-ui/src/export.rs#L793), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| 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:941`](../ui/dr-ui/src/export.rs#L941), [`ui/dr-ui/src/lib.rs:1752`](../ui/dr-ui/src/lib.rs#L1752), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library_ui.rs:3100`](../ui/dr-ui/src/library_ui.rs#L3100), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:5373`](../ui/dr-ui/src/library_ui.rs#L5373), [`ui/dr-ui/src/library_ui.rs:5450`](../ui/dr-ui/src/library_ui.rs#L5450), [`ui/dr-ui/src/library_ui.rs:5462`](../ui/dr-ui/src/library_ui.rs#L5462), [`ui/dr-ui/src/library_ui.rs:546`](../ui/dr-ui/src/library_ui.rs#L546), [`ui/dr-ui/src/library_ui.rs:603`](../ui/dr-ui/src/library_ui.rs#L603), [`ui/dr-ui/ui/app.slint:1351`](../ui/dr-ui/ui/app.slint#L1351), [`ui/dr-ui/ui/app.slint:991`](../ui/dr-ui/ui/app.slint#L991), [`ui/dr-ui/ui/library.slint:1113`](../ui/dr-ui/ui/library.slint#L1113), [`ui/dr-ui/ui/library.slint:670`](../ui/dr-ui/ui/library.slint#L670), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) | +| 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:1163`](../core/dr-decode/src/locate.rs#L1163), [`core/dr-decode/src/locate.rs:1221`](../core/dr-decode/src/locate.rs#L1221), [`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:119`](../core/dr-export/src/encode.rs#L119), [`core/dr-export/src/encode.rs:163`](../core/dr-export/src/encode.rs#L163), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:208`](../core/dr-export/src/encode.rs#L208), [`core/dr-export/src/encode.rs:237`](../core/dr-export/src/encode.rs#L237), [`core/dr-export/src/encode.rs:310`](../core/dr-export/src/encode.rs#L310), [`core/dr-export/src/encode.rs:324`](../core/dr-export/src/encode.rs#L324), [`core/dr-export/src/encode.rs:404`](../core/dr-export/src/encode.rs#L404), [`core/dr-export/src/encode.rs:452`](../core/dr-export/src/encode.rs#L452), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:791`](../core/dr-export/src/encode.rs#L791), [`core/dr-export/src/encode.rs:805`](../core/dr-export/src/encode.rs#L805), [`core/dr-export/src/encode.rs:846`](../core/dr-export/src/encode.rs#L846), [`core/dr-export/src/encode.rs:894`](../core/dr-export/src/encode.rs#L894), [`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:442`](../core/dr-types/src/lib.rs#L442), [`core/dr-types/src/settings.rs:259`](../core/dr-types/src/settings.rs#L259), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:776`](../ui/dr-ui/src/export.rs#L776), [`ui/dr-ui/src/export.rs:793`](../ui/dr-ui/src/export.rs#L793), [`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:503`](../core/dr-decode/src/lib.rs#L503), [`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:791`](../core/dr-gpu/src/adjust.rs#L791), [`ui/dr-ui/src/develop.rs:1904`](../ui/dr-ui/src/develop.rs#L1904), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328) | | 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/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) | | FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library.rs:1512`](../ui/dr-ui/src/library.rs#L1512), [`ui/dr-ui/src/library.rs:398`](../ui/dr-ui/src/library.rs#L398), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1492`](../ui/dr-ui/src/library_ui.rs#L1492), [`ui/dr-ui/src/library_ui.rs:3100`](../ui/dr-ui/src/library_ui.rs#L3100), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | @@ -81,15 +81,15 @@ _None._ | FR-NC-2 | [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`core/dr-sync-nextcloud/src/session.rs:34`](../core/dr-sync-nextcloud/src/session.rs#L34) | | FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2528`](../ui/dr-ui/src/library.rs#L2528), [`ui/dr-ui/src/library.rs:2554`](../ui/dr-ui/src/library.rs#L2554), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3339`](../ui/dr-ui/src/library_ui.rs#L3339), [`ui/dr-ui/src/library_ui.rs:4113`](../ui/dr-ui/src/library_ui.rs#L4113), [`ui/dr-ui/ui/app.slint:528`](../ui/dr-ui/ui/app.slint#L528), [`ui/dr-ui/ui/settings.slint:316`](../ui/dr-ui/ui/settings.slint#L316), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`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:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) | -| 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:398`](../ui/dr-ui/src/import.rs#L398) | +| 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:489`](../ui/dr-ui/src/import.rs#L489) | | 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/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`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:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:574`](../ui/dr-ui/src/collections_ui.rs#L574), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/lib.rs:1511`](../ui/dr-ui/src/lib.rs#L1511), [`ui/dr-ui/src/lib.rs:2251`](../ui/dr-ui/src/lib.rs#L2251), [`ui/dr-ui/src/library.rs:1254`](../ui/dr-ui/src/library.rs#L1254), [`ui/dr-ui/src/library.rs:1277`](../ui/dr-ui/src/library.rs#L1277), [`ui/dr-ui/src/library.rs:1435`](../ui/dr-ui/src/library.rs#L1435), [`ui/dr-ui/src/library_ui.rs:1024`](../ui/dr-ui/src/library_ui.rs#L1024), [`ui/dr-ui/src/library_ui.rs:1125`](../ui/dr-ui/src/library_ui.rs#L1125), [`ui/dr-ui/src/library_ui.rs:1180`](../ui/dr-ui/src/library_ui.rs#L1180), [`ui/dr-ui/src/library_ui.rs:1298`](../ui/dr-ui/src/library_ui.rs#L1298), [`ui/dr-ui/src/library_ui.rs:1417`](../ui/dr-ui/src/library_ui.rs#L1417), [`ui/dr-ui/src/library_ui.rs:1895`](../ui/dr-ui/src/library_ui.rs#L1895), [`ui/dr-ui/src/library_ui.rs:211`](../ui/dr-ui/src/library_ui.rs#L211), [`ui/dr-ui/src/library_ui.rs:222`](../ui/dr-ui/src/library_ui.rs#L222), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:242`](../ui/dr-ui/src/library_ui.rs#L242), [`ui/dr-ui/src/library_ui.rs:251`](../ui/dr-ui/src/library_ui.rs#L251), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:327`](../ui/dr-ui/src/library_ui.rs#L327), [`ui/dr-ui/src/library_ui.rs:369`](../ui/dr-ui/src/library_ui.rs#L369), [`ui/dr-ui/src/library_ui.rs:432`](../ui/dr-ui/src/library_ui.rs#L432), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:4649`](../ui/dr-ui/src/library_ui.rs#L4649), [`ui/dr-ui/src/library_ui.rs:4667`](../ui/dr-ui/src/library_ui.rs#L4667), [`ui/dr-ui/src/library_ui.rs:4679`](../ui/dr-ui/src/library_ui.rs#L4679), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:951`](../ui/dr-ui/src/library_ui.rs#L951), [`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:2359`](../ui/dr-ui/ui/app.slint#L2359), [`ui/dr-ui/ui/app.slint:589`](../ui/dr-ui/ui/app.slint#L589), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:808`](../ui/dr-ui/ui/library.slint#L808) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`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:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:574`](../ui/dr-ui/src/collections_ui.rs#L574), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/lib.rs:1512`](../ui/dr-ui/src/lib.rs#L1512), [`ui/dr-ui/src/lib.rs:2252`](../ui/dr-ui/src/lib.rs#L2252), [`ui/dr-ui/src/library.rs:1254`](../ui/dr-ui/src/library.rs#L1254), [`ui/dr-ui/src/library.rs:1277`](../ui/dr-ui/src/library.rs#L1277), [`ui/dr-ui/src/library.rs:1435`](../ui/dr-ui/src/library.rs#L1435), [`ui/dr-ui/src/library_ui.rs:1024`](../ui/dr-ui/src/library_ui.rs#L1024), [`ui/dr-ui/src/library_ui.rs:1125`](../ui/dr-ui/src/library_ui.rs#L1125), [`ui/dr-ui/src/library_ui.rs:1180`](../ui/dr-ui/src/library_ui.rs#L1180), [`ui/dr-ui/src/library_ui.rs:1298`](../ui/dr-ui/src/library_ui.rs#L1298), [`ui/dr-ui/src/library_ui.rs:1417`](../ui/dr-ui/src/library_ui.rs#L1417), [`ui/dr-ui/src/library_ui.rs:1895`](../ui/dr-ui/src/library_ui.rs#L1895), [`ui/dr-ui/src/library_ui.rs:211`](../ui/dr-ui/src/library_ui.rs#L211), [`ui/dr-ui/src/library_ui.rs:222`](../ui/dr-ui/src/library_ui.rs#L222), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:242`](../ui/dr-ui/src/library_ui.rs#L242), [`ui/dr-ui/src/library_ui.rs:251`](../ui/dr-ui/src/library_ui.rs#L251), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:327`](../ui/dr-ui/src/library_ui.rs#L327), [`ui/dr-ui/src/library_ui.rs:369`](../ui/dr-ui/src/library_ui.rs#L369), [`ui/dr-ui/src/library_ui.rs:432`](../ui/dr-ui/src/library_ui.rs#L432), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:4649`](../ui/dr-ui/src/library_ui.rs#L4649), [`ui/dr-ui/src/library_ui.rs:4667`](../ui/dr-ui/src/library_ui.rs#L4667), [`ui/dr-ui/src/library_ui.rs:4679`](../ui/dr-ui/src/library_ui.rs#L4679), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:951`](../ui/dr-ui/src/library_ui.rs#L951), [`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:2357`](../ui/dr-ui/ui/app.slint#L2357), [`ui/dr-ui/ui/app.slint:589`](../ui/dr-ui/ui/app.slint#L589), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:808`](../ui/dr-ui/ui/library.slint#L808) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1180`](../ui/dr-ui/src/library_ui.rs#L1180) | | FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:117`](../core/dr-types/src/lib.rs#L117), [`core/dr-types/src/lib.rs:199`](../core/dr-types/src/lib.rs#L199), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.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:574`](../ui/dr-ui/src/collections_ui.rs#L574), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/library_ui.rs:1024`](../ui/dr-ui/src/library_ui.rs#L1024), [`ui/dr-ui/src/library_ui.rs:951`](../ui/dr-ui/src/library_ui.rs#L951), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) | | FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2554`](../ui/dr-ui/src/library.rs#L2554), [`ui/dr-ui/src/library_ui.rs:3339`](../ui/dr-ui/src/library_ui.rs#L3339), [`ui/dr-ui/ui/settings.slint:316`](../ui/dr-ui/ui/settings.slint#L316) | -| 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:62`](../core/dr-types/src/settings.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:75`](../ui/dr-ui/src/import.rs#L75), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:916`](../ui/dr-ui/src/lib.rs#L916), [`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:75`](../ui/dr-ui/src/import.rs#L75), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:916`](../ui/dr-ui/src/lib.rs#L916) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1480`](../ui/dr-ui/src/lib.rs#L1480), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384) | +| 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:62`](../core/dr-types/src/settings.rs#L62), [`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:916`](../ui/dr-ui/src/lib.rs#L916), [`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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:916`](../ui/dr-ui/src/lib.rs#L916) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1481`](../ui/dr-ui/src/lib.rs#L1481), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384) | | FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:149`](../core/dr-pipeline/src/sidecar.rs#L149), [`core/dr-pipeline/src/sidecar.rs:303`](../core/dr-pipeline/src/sidecar.rs#L303), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:810`](../ui/dr-ui/src/library.rs#L810) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:51`](../core/dr-types/src/lib.rs#L51) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | @@ -98,14 +98,14 @@ _None._ | FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:503`](../core/dr-decode/src/lib.rs#L503), [`core/dr-decode/src/locate.rs:1364`](../core/dr-decode/src/locate.rs#L1364) | | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175) | | 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:2333`](../ui/dr-ui/src/lib.rs#L2333), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:700`](../ui/dr-ui/src/masks_ui.rs#L700), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866) | +| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2334`](../ui/dr-ui/src/lib.rs#L2334), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:700`](../ui/dr-ui/src/masks_ui.rs#L700), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866) | | FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:1448`](../ui/dr-ui/src/collections_ui.rs#L1448), [`ui/dr-ui/src/collections_ui.rs:1462`](../ui/dr-ui/src/collections_ui.rs#L1462), [`ui/dr-ui/src/collections_ui.rs:1507`](../ui/dr-ui/src/collections_ui.rs#L1507), [`ui/dr-ui/src/collections_ui.rs:1517`](../ui/dr-ui/src/collections_ui.rs#L1517), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:941`](../ui/dr-ui/src/collections_ui.rs#L941), [`ui/dr-ui/src/collections_ui.rs:951`](../ui/dr-ui/src/collections_ui.rs#L951), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:4679`](../ui/dr-ui/src/library_ui.rs#L4679), [`ui/dr-ui/ui/app.slint:619`](../ui/dr-ui/ui/app.slint#L619), [`ui/dr-ui/ui/app.slint:626`](../ui/dr-ui/ui/app.slint#L626), [`ui/dr-ui/ui/library.slint:1015`](../ui/dr-ui/ui/library.slint#L1015), [`ui/dr-ui/ui/library.slint:1022`](../ui/dr-ui/ui/library.slint#L1022), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:1900`](../ui/dr-ui/ui/library.slint#L1900), [`ui/dr-ui/ui/library.slint:658`](../ui/dr-ui/ui/library.slint#L658), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1492`](../ui/dr-ui/src/develop.rs#L1492), [`ui/dr-ui/src/library_ui.rs:3908`](../ui/dr-ui/src/library_ui.rs#L3908), [`ui/dr-ui/src/masks_ui.rs:217`](../ui/dr-ui/src/masks_ui.rs#L217), [`ui/dr-ui/src/masks_ui.rs:792`](../ui/dr-ui/src/masks_ui.rs#L792), [`ui/dr-ui/src/masks_ui.rs:814`](../ui/dr-ui/src/masks_ui.rs#L814), [`ui/dr-ui/ui/app.slint:1884`](../ui/dr-ui/ui/app.slint#L1884), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | -| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:124`](../ui/dr-ui/src/collections_ui.rs#L124), [`ui/dr-ui/src/collections_ui.rs:1448`](../ui/dr-ui/src/collections_ui.rs#L1448), [`ui/dr-ui/src/collections_ui.rs:1462`](../ui/dr-ui/src/collections_ui.rs#L1462), [`ui/dr-ui/src/collections_ui.rs:1507`](../ui/dr-ui/src/collections_ui.rs#L1507), [`ui/dr-ui/src/collections_ui.rs:1517`](../ui/dr-ui/src/collections_ui.rs#L1517), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1544`](../ui/dr-ui/src/collections_ui.rs#L1544), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:537`](../ui/dr-ui/src/collections_ui.rs#L537), [`ui/dr-ui/src/collections_ui.rs:941`](../ui/dr-ui/src/collections_ui.rs#L941), [`ui/dr-ui/src/collections_ui.rs:951`](../ui/dr-ui/src/collections_ui.rs#L951), [`ui/dr-ui/src/library_ui.rs:3908`](../ui/dr-ui/src/library_ui.rs#L3908), [`ui/dr-ui/src/library_ui.rs:3953`](../ui/dr-ui/src/library_ui.rs#L3953), [`ui/dr-ui/src/library_ui.rs:4056`](../ui/dr-ui/src/library_ui.rs#L4056), [`ui/dr-ui/src/library_ui.rs:4084`](../ui/dr-ui/src/library_ui.rs#L4084), [`ui/dr-ui/src/library_ui.rs:4667`](../ui/dr-ui/src/library_ui.rs#L4667), [`ui/dr-ui/src/library_ui.rs:4679`](../ui/dr-ui/src/library_ui.rs#L4679), [`ui/dr-ui/ui/app.slint:1580`](../ui/dr-ui/ui/app.slint#L1580), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/app.slint:626`](../ui/dr-ui/ui/app.slint#L626), [`ui/dr-ui/ui/library.slint:1015`](../ui/dr-ui/ui/library.slint#L1015), [`ui/dr-ui/ui/library.slint:1022`](../ui/dr-ui/ui/library.slint#L1022), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:1900`](../ui/dr-ui/ui/library.slint#L1900), [`ui/dr-ui/ui/library.slint:658`](../ui/dr-ui/ui/library.slint#L658), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/library.slint:726`](../ui/dr-ui/ui/library.slint#L726) | -| 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:1978`](../ui/dr-ui/src/lib.rs#L1978), [`ui/dr-ui/src/lib.rs:2367`](../ui/dr-ui/src/lib.rs#L2367), [`ui/dr-ui/src/lib.rs:2552`](../ui/dr-ui/src/lib.rs#L2552), [`ui/dr-ui/src/masks_ui.rs:747`](../ui/dr-ui/src/masks_ui.rs#L747), [`ui/dr-ui/ui/app.slint:326`](../ui/dr-ui/ui/app.slint#L326), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1492`](../ui/dr-ui/src/develop.rs#L1492), [`ui/dr-ui/src/library_ui.rs:3908`](../ui/dr-ui/src/library_ui.rs#L3908), [`ui/dr-ui/src/masks_ui.rs:217`](../ui/dr-ui/src/masks_ui.rs#L217), [`ui/dr-ui/src/masks_ui.rs:792`](../ui/dr-ui/src/masks_ui.rs#L792), [`ui/dr-ui/src/masks_ui.rs:814`](../ui/dr-ui/src/masks_ui.rs#L814), [`ui/dr-ui/ui/app.slint:1882`](../ui/dr-ui/ui/app.slint#L1882), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | +| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:124`](../ui/dr-ui/src/collections_ui.rs#L124), [`ui/dr-ui/src/collections_ui.rs:1448`](../ui/dr-ui/src/collections_ui.rs#L1448), [`ui/dr-ui/src/collections_ui.rs:1462`](../ui/dr-ui/src/collections_ui.rs#L1462), [`ui/dr-ui/src/collections_ui.rs:1507`](../ui/dr-ui/src/collections_ui.rs#L1507), [`ui/dr-ui/src/collections_ui.rs:1517`](../ui/dr-ui/src/collections_ui.rs#L1517), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1544`](../ui/dr-ui/src/collections_ui.rs#L1544), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:537`](../ui/dr-ui/src/collections_ui.rs#L537), [`ui/dr-ui/src/collections_ui.rs:941`](../ui/dr-ui/src/collections_ui.rs#L941), [`ui/dr-ui/src/collections_ui.rs:951`](../ui/dr-ui/src/collections_ui.rs#L951), [`ui/dr-ui/src/library_ui.rs:3908`](../ui/dr-ui/src/library_ui.rs#L3908), [`ui/dr-ui/src/library_ui.rs:3953`](../ui/dr-ui/src/library_ui.rs#L3953), [`ui/dr-ui/src/library_ui.rs:4056`](../ui/dr-ui/src/library_ui.rs#L4056), [`ui/dr-ui/src/library_ui.rs:4084`](../ui/dr-ui/src/library_ui.rs#L4084), [`ui/dr-ui/src/library_ui.rs:4667`](../ui/dr-ui/src/library_ui.rs#L4667), [`ui/dr-ui/src/library_ui.rs:4679`](../ui/dr-ui/src/library_ui.rs#L4679), [`ui/dr-ui/ui/app.slint:1578`](../ui/dr-ui/ui/app.slint#L1578), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/app.slint:626`](../ui/dr-ui/ui/app.slint#L626), [`ui/dr-ui/ui/library.slint:1015`](../ui/dr-ui/ui/library.slint#L1015), [`ui/dr-ui/ui/library.slint:1022`](../ui/dr-ui/ui/library.slint#L1022), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:1900`](../ui/dr-ui/ui/library.slint#L1900), [`ui/dr-ui/ui/library.slint:658`](../ui/dr-ui/ui/library.slint#L658), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/library.slint:726`](../ui/dr-ui/ui/library.slint#L726) | +| 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:1979`](../ui/dr-ui/src/lib.rs#L1979), [`ui/dr-ui/src/lib.rs:2368`](../ui/dr-ui/src/lib.rs#L2368), [`ui/dr-ui/src/lib.rs:2553`](../ui/dr-ui/src/lib.rs#L2553), [`ui/dr-ui/src/masks_ui.rs:747`](../ui/dr-ui/src/masks_ui.rs#L747), [`ui/dr-ui/ui/app.slint:326`](../ui/dr-ui/ui/app.slint#L326), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1552`](../ui/dr-ui/src/export.rs#L1552), [`ui/dr-ui/src/export.rs:1578`](../ui/dr-ui/src/export.rs#L1578), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1785`](../ui/dr-ui/src/lib.rs#L1785), [`ui/dr-ui/ui/app.slint:1003`](../ui/dr-ui/ui/app.slint#L1003), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1552`](../ui/dr-ui/src/export.rs#L1552), [`ui/dr-ui/src/export.rs:1578`](../ui/dr-ui/src/export.rs#L1578), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1786`](../ui/dr-ui/src/lib.rs#L1786), [`ui/dr-ui/ui/app.slint:1002`](../ui/dr-ui/ui/app.slint#L1002), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) | | NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) | | NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | diff --git a/ui/dr-ui/src/import.rs b/ui/dr-ui/src/import.rs index 676683a..ad88330 100644 --- a/ui/dr-ui/src/import.rs +++ b/ui/dr-ui/src/import.rs @@ -19,6 +19,26 @@ //! user sees "1,847 photographs, 61.2 GB" and then a progress bar, rather than //! a frozen window and then a progress bar. //! +//! # There is no local library, so there is no destination to choose +//! +//! DarkRoom's library is a folder on a Nextcloud server (FR-NC-6). An import +//! therefore has exactly one place to go, and the interface has nothing to ask +//! about it. +//! +//! What lands on this machine is a **staging copy**, in a directory the app +//! owns beside the catalog — the same shape `export` uses for its outbox, and +//! for the same reason its module docs give: staging first is the only path, +//! not a fallback for the offline case. +//! +//! The bytes have to touch disk before they touch the network. Streaming a +//! card straight to the server would mean a move-import erasing a card against +//! an in-flight upload, which is the one operation here with no undo, and it +//! would make importing impossible with no connection, which FR-NC-10 forbids. +//! +//! A staged file is removed once the server confirms it. One that is not +//! confirmed stays where it is, and the next import drains it — so an import +//! made on a train finishes when the train arrives somewhere. +//! //! # The import does not catalogue what it wrote //! //! It writes files into the library and then asks for a scan, rather than @@ -33,7 +53,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, Sender}; use std::sync::Arc; -use dr_ingest::{Candidate, DupKey, Imported, Ingest, Options, Report, Shot}; +use dr_ingest::{Candidate, DupKey, Imported, Ingest, Options, Report, Shot, TransferMode}; use dr_plat::{DirRef, LocalStorage, Storage, WritableStorage}; use dr_sync::{RemoteBackend, RemotePath}; use dr_sync_nextcloud::{AppCredentials, NextcloudBackend}; @@ -56,8 +76,9 @@ pub type Cancel = Arc; pub struct Request { /// Where the card is mounted. pub card: PathBuf, - /// The library root the template's folders are created under. - pub library: PathBuf, + /// Where the copies are staged before they go up. Owned by the app, not + /// chosen by the user — see the module docs. + pub staging: PathBuf, /// The optional second destination (FR-CAT-10). pub backup: Option, /// The catalog, opened by the worker for its duplicate queries. @@ -68,8 +89,9 @@ pub struct Request { /// Which file types to take off the card. pub filter: FormatFilter, pub options: Options, - /// Where to send the originals afterwards, if anywhere (FR-NC-7b). - pub upload: Option, + /// Where the photographs are going. An import without one has nowhere to + /// put anything, and the page refuses to start. + pub upload: Upload, } /// TRACES: FR-NC-7a | FR-NC-7b @@ -89,6 +111,22 @@ pub struct Upload { /// Where the thumbnail shards live, so a thumbnail made during the import /// can be filed under the server's `oc:fileid` once the upload assigns one. pub thumbs: PathBuf, + /// Where copies wait between disk and the server. + /// + /// Per account, beside the catalog, like the export outbox: a file queued + /// for one server is meaningless to another, and a staging directory + /// shared between accounts would upload a card to whichever was signed in + /// when the connection came back. + pub staging: PathBuf, +} + +/// TRACES: FR-NC-7b +/// Where an account's imports wait between disk and the server. +pub fn staging_dir(server: &str, user_id: &str) -> PathBuf { + crate::library::catalog_path(server, user_id) + .parent() + .map(|p| p.join("staging")) + .unwrap_or_else(|| std::env::temp_dir().join("darkroom-import-staging")) } impl std::fmt::Debug for Upload { @@ -98,6 +136,7 @@ impl std::fmt::Debug for Upload { .field("user_id", &self.user_id) .field("library", &self.library) .field("thumbs", &self.thumbs) + .field("staging", &self.staging) .finish_non_exhaustive() } } @@ -166,6 +205,18 @@ pub struct Outcome { /// and the card has not been touched. Reported so the user knows the /// server does not have them yet. pub upload_failed: usize, + /// Card files actually deleted, for a move-import. + /// + /// Never more than `retirable`, and zero for a copy-import. Reported + /// because "the card is now empty" is a claim the user needs to be able to + /// check against what they expected. + pub retired: usize, + /// Staged copies still waiting to go up. + /// + /// The same files as `upload_failed`, named for what happens next rather + /// than for what went wrong: they are queued, and the next import sends + /// them. An import made with no connection is this and nothing else. + pub staged: usize, } /// Start an import. @@ -188,7 +239,16 @@ pub fn spawn(request: Request, cancel: Cancel) -> Receiver { fn run(request: Request, cancel: &Cancel, tx: &Sender) -> Result<(), String> { let card = LocalStorage::with_root(CARD, request.card.clone()); - let library = LocalStorage::with_root(LIBRARY, request.library.clone()); + // Created rather than assumed: the first import on a machine finds no + // staging directory, and a failure here is worth reporting as itself + // rather than as every file failing to be written. + if let Err(e) = std::fs::create_dir_all(&request.staging) { + return Err(format!( + "could not prepare {}: {e}", + request.staging.display() + )); + } + let library = LocalStorage::with_root(LIBRARY, request.staging.clone()); let backup = request .backup .as_ref() @@ -242,7 +302,8 @@ fn run(request: Request, cancel: &Cancel, tx: &Sender) -> Result<(), St // FR-NC-7b. Everything above has already finished: every file is on disk // and its digest checked, so nothing from here on can cost the user a // photograph — only a transfer. - if let Some(upload) = &request.upload { + { + let upload = &request.upload; let (sent, thumbnails) = upload_all(upload, &library, &report.imported, cancel, tx); outcome.thumbnails = thumbnails; outcome.uploaded = sent.iter().filter(|s| s.transferred).count(); @@ -264,6 +325,36 @@ fn run(request: Request, cancel: &Cancel, tx: &Sender) -> Result<(), St // now. The metadata tier covers the interval, which is why this is not // worth waiting for a sync to do properly. record_digests(catalog.connection(), &upload.library, &sent); + + // The staging copy has done its job the moment the server confirms it: + // the photograph is in the library, and keeping a second copy in a + // directory no view ever shows would be a slow disk leak. What is *not* + // confirmed stays exactly where it is — that is the queue FR-NC-7b + // describes, and the next import drains it. + outcome.staged = report.imported.len() - sent.len(); + clear_staged(&library, &report.imported, &sent); + + // TRACES: FR-CAT-10 | FR-NC-7b + // The card, last of all, and only for what the server has. + // + // The rule this enforces: a copy-import never touches the card, and a + // move-import empties it only of photographs that are confirmed on the + // server — not merely written to disk here, because the staging copy + // above has just been removed and this machine is no longer holding + // them either. Anything the upload did not take keeps its card copy, + // which is then the only copy that exists. + // + // Done here rather than handed to the caller: `dr_ingest::retire` is + // deliberately awkward to reach, and a caller that forgot to call it + // was exactly the bug this replaces — Move behaved as Copy, silently. + if request.options.mode == TransferMode::Move { + let confirmed = confirmed_sources(&report.imported, &sent); + outcome.retirable = confirmed.len(); + let failures = dr_ingest::retire(&card, &confirmed); + outcome.retired = confirmed.len() - failures.len(); + } else { + outcome.retirable = 0; + } } let _ = tx.send(Message::Finished(outcome)); @@ -478,6 +569,43 @@ async fn file_thumbnails( filed } +/// The card sources whose photographs the server now holds. +/// +/// Paired positionally with `imported`, which is how `upload_all` builds +/// `sent`: one entry per original it got an answer about, in order. A file the +/// upload skipped or failed on has no entry, and so is never returned here. +fn confirmed_sources(imported: &[Imported], sent: &[Sent]) -> Vec { + imported + .iter() + .zip(sent.iter()) + .map(|(image, _)| image.source.clone()) + .collect() +} + +/// TRACES: FR-NC-7b +/// Remove the staging copies the server has confirmed. +/// +/// Keyed on what came back from the upload rather than on what was imported: +/// a file the server did not take must survive, because the staging directory +/// is the only place it exists once the card is put away. +/// +/// Failures are logged, not propagated. A staged file that cannot be deleted +/// costs disk; refusing to finish an import over it would cost the user the +/// report telling them their photographs are safe. +fn clear_staged(library: &LocalStorage, imported: &[Imported], sent: &[Sent]) { + let confirmed: std::collections::HashSet<&str> = + sent.iter().map(|s| s.remote_path.as_str()).collect(); + + for (image, item) in imported.iter().zip(sent.iter()) { + if !confirmed.contains(item.remote_path.as_str()) { + continue; + } + if let Err(e) = library.remove_file(&image.written) { + log::warn!("could not clear the staged copy of {}: {e}", image.name); + } + } +} + /// Read an imported file back out of the library. fn read_all(library: &LocalStorage, image: &Imported) -> Result, String> { use std::io::Read; @@ -640,6 +768,8 @@ pub fn summarise(report: &Report) -> Outcome { uploaded: 0, already_on_server: 0, upload_failed: 0, + staged: 0, + retired: 0, thumbnails: 0, } } @@ -698,12 +828,18 @@ pub fn describe(outcome: &Outcome) -> String { } if outcome.upload_failed > 0 { out.push_str(&format!( - ", {} still only on this computer", + ", {} waiting to go up — the next import sends them", outcome.upload_failed )); } } + // The card. Said plainly, because emptying one is irreversible and the + // user should not have to infer it from a count of uploads. + if outcome.retired > 0 { + out.push_str(&format!(". {} removed from the card", outcome.retired)); + } + // Worth saying: it is why the grid fills in immediately after an import // rather than fetching a preview out of every original that just went up. if outcome.thumbnails > 0 { @@ -938,7 +1074,77 @@ mod tests { }; let text = describe(&o); assert!(text.contains("2 uploaded"), "{text}"); - assert!(text.contains("1 still only on this computer"), "{text}"); + // Named for what happens next rather than for what went wrong: the + // file is queued, not lost, and the next import sends it. + assert!(text.contains("1 waiting to go up"), "{text}"); + } + + #[test] + fn emptying_the_card_is_stated_plainly() { + // Irreversible, so the user should not have to infer it from a count + // of uploads. + let o = Outcome { + uploaded: 3, + retirable: 3, + retired: 3, + ..outcome() + }; + assert!( + describe(&o).contains("3 removed from the card"), + "{}", + describe(&o) + ); + } + + #[test] + fn a_copy_import_never_mentions_the_card() { + let o = Outcome { + uploaded: 3, + ..outcome() + }; + assert!(!describe(&o).contains("card"), "{}", describe(&o)); + } + + #[test] + fn only_confirmed_photographs_are_taken_off_the_card() { + // The rule: a card is emptied of what the *server* has, never of what + // merely reached this disk — the staging copy is removed straight + // after, so anything not uploaded would otherwise exist nowhere. + let imported: Vec = (0..3) + .map(|i| Imported { + source: dr_types::SourceRef::Local { + root: CARD, + relative: format!("IMG_{i}.CR3"), + }, + written: dr_types::SourceRef::Local { + root: LIBRARY, + relative: format!("2026/2026-08-22/IMG_{i}.CR3"), + }, + folders: vec!["2026".into(), "2026-08-22".into()], + name: format!("IMG_{i}.CR3"), + digest: "x".into(), + size: 10, + dated_from: DateSource::Capture, + backup: None, + }) + .collect(); + + // The server took the first two; the third never went up. + let sent: Vec = (0..2) + .map(|i| Sent { + remote_path: format!("PhotosRaw/2026/2026-08-22/IMG_{i}.CR3"), + digest: "x".into(), + transferred: true, + thumbnail: None, + }) + .collect(); + + let confirmed = confirmed_sources(&imported, &sent); + assert_eq!(confirmed.len(), 2); + assert_eq!(confirmed[0].key(), "IMG_0.CR3"); + assert_eq!(confirmed[1].key(), "IMG_1.CR3"); + // IMG_2 keeps its card copy, which is now the only copy of it. + assert!(!confirmed.iter().any(|s| s.key() == "IMG_2.CR3")); } #[test] diff --git a/ui/dr-ui/src/import_ui.rs b/ui/dr-ui/src/import_ui.rs index 35dae3c..28052ef 100644 --- a/ui/dr-ui/src/import_ui.rs +++ b/ui/dr-ui/src/import_ui.rs @@ -188,7 +188,9 @@ impl ImportController { fn can_start(&self) -> bool { !self.running.get() && !self.card.borrow().is_empty() - && !self.options().destination.is_empty() + // An account, because the library is on the server and an import + // with none has nowhere to go at all. + && !self.upload_target.borrow().is_empty() && self.survey.borrow().map(|(n, _)| n > 0).unwrap_or(false) } } @@ -252,17 +254,9 @@ pub fn render(window: &AppWindow, ctl: &Rc) { .into(), ); - window.set_import_destination(stored.destination.as_str().into()); - // Empty where no account is signed in, or where the user turned the upload - // off — the page then shows one destination because there is one. - window.set_import_upload_target( - if stored.upload { - ctl.upload_target.borrow().clone() - } else { - String::new() - } - .into(), - ); + // The only destination there is. Empty means no account, which is what + // keeps Import disabled — see `can_start`. + window.set_import_upload_target(ctl.upload_target.borrow().as_str().into()); window.set_import_folder_template(stored.folder_template.as_str().into()); window.set_import_template_preview(preview(&stored, crate::library::now_secs()).into()); @@ -333,13 +327,6 @@ where // does: another instance may have written it since. *ctl.settings.settings.borrow_mut() = ctl.settings.store.load(); - // A destination the user has never chosen defaults to somewhere - // that exists, rather than to an empty field they must decode. - if ctl.options().destination.is_empty() { - if let Some(home) = default_destination() { - ctl.edit(|s| s.destination = home.display().to_string()); - } - } *ctl.upload_target.borrow_mut() = context().map(|c| c.library_label).unwrap_or_default(); ctl.refresh_volumes(); @@ -505,16 +492,6 @@ where } } -/// Somewhere sensible for a first import to go. -fn default_destination() -> Option { - let home = std::env::var_os("HOME").map(PathBuf::from)?; - let pictures = home.join("Pictures"); - // Only if it exists: inventing a folder the user has not asked for, in a - // field they may not read, is how photographs end up somewhere nobody - // looks. An empty field keeps Import disabled until they choose. - pictures.exists().then_some(pictures) -} - /// Count what is on the card, without copying anything. /// /// Runs on the UI thread, which is defensible only because it is a directory @@ -570,14 +547,21 @@ fn start( Some(PathBuf::from(stored.backup.trim())) }; + let Some(upload) = context.upload else { + *ctl.error.borrow_mut() = + "Sign in first — the library this imports into is on the server.".into(); + render(window, ctl); + return; + }; + let request = Request { card: PathBuf::from(ctl.card.borrow().clone()), - library: PathBuf::from(&stored.destination), + staging: upload.staging.clone(), backup, catalog: context.catalog.clone(), filter: context.filter.clone(), options: ctl.ingest_options(), - upload: stored.upload.then_some(context.upload).flatten(), + upload, }; let cancel: import::Cancel = Default::default(); @@ -745,6 +729,35 @@ mod tests { assert_eq!(preview(&s, AUG_22), "2026/EOS R5"); } + #[test] + fn an_import_needs_an_account_before_it_can_start() { + // The library is on the server, so "where does this go" has no local + // answer to fall back on. Import stays disabled rather than quietly + // filling a folder nothing ever shows. + let ctl = ImportController::new( + crate::settings_ui::SettingsController::new(), + crate::activity::ActivityLog::new(), + ); + *ctl.card.borrow_mut() = "/run/media/duncan/EOS DIGITAL".into(); + *ctl.survey.borrow_mut() = Some((12, 1234)); + assert!(!ctl.can_start(), "no account, yet Import was offered"); + + *ctl.upload_target.borrow_mut() = "PhotosRaw".into(); + assert!(ctl.can_start()); + } + + #[test] + fn an_empty_card_cannot_be_imported_either() { + let ctl = ImportController::new( + crate::settings_ui::SettingsController::new(), + crate::activity::ActivityLog::new(), + ); + *ctl.card.borrow_mut() = "/run/media/duncan/EOS DIGITAL".into(); + *ctl.upload_target.borrow_mut() = "PhotosRaw".into(); + *ctl.survey.borrow_mut() = Some((0, 0)); + assert!(!ctl.can_start()); + } + #[test] fn a_volume_says_why_it_is_being_offered() { // "EOS DIGITAL" and "archive" look equally plausible in a list, and diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 846fcaf..4463f42 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -953,6 +953,7 @@ pub fn run(paths: Vec) -> Result<()> { // pushes, so a thumbnail made during an import is the // one every other client gets. thumbs: library::thumbs_dir(&session.server, &session.user_id), + staging: import::staging_dir(&session.server, &session.user_id), }), }) }, diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 63c9727..b03fc6b 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -781,7 +781,6 @@ export component AppWindow inherits Window { in property import-card-looks-right: false; in property import-surveying: false; in property import-survey-summary: ""; - in property import-destination: ""; in property import-upload-target: ""; in property import-folder-template: ""; in property import-template-preview: ""; @@ -1099,7 +1098,6 @@ in property panel-visible: true; card-looks-right: root.import-card-looks-right; surveying: root.import-surveying; survey-summary: root.import-survey-summary; - destination: root.import-destination; upload-target: root.import-upload-target; folder-template: root.import-folder-template; template-preview: root.import-template-preview; diff --git a/ui/dr-ui/ui/import.slint b/ui/dr-ui/ui/import.slint index 3b6b009..0b7eebc 100644 --- a/ui/dr-ui/ui/import.slint +++ b/ui/dr-ui/ui/import.slint @@ -85,11 +85,11 @@ export component ImportPage inherits Rectangle { in property survey-summary; // --- where they are going -------------------------------------------- - /// The library root. Shown, not editable — see the header comment. - in property destination; - /// The folder on the server the originals go on to, once they are safely - /// on disk (FR-NC-7b). Empty when no account is signed in, which hides the - /// line rather than showing an empty one. + /// The library folder on the server — the only destination there is. + /// + /// Empty means no account is signed in, and Import stays disabled: the + /// library lives on the server, so an import without one has nowhere to + /// go at all. in property upload-target; in-out property folder-template; /// The template expanded against a real date, so the answer to "what will @@ -272,27 +272,24 @@ export component ImportPage inherits Rectangle { spacing: Theme.gap; Label { text: "Library"; } Value { - text: root.destination; + text: root.upload-target != "" ? root.upload-target + : "not signed in"; overflow: elide; horizontal-stretch: 1; } } - if root.upload-target != "": HorizontalLayout { - spacing: Theme.gap; - Label { text: "Server"; } - Value { - text: root.upload-target; - overflow: elide; - horizontal-stretch: 1; - } - } - - // Said once, here, rather than beside the Import button: - // the order is the promise, and a user watching a slow - // upload needs to already know the photographs are on disk. + // The order is the promise, and a user watching a slow + // upload needs to already know the photographs are safe. + // Said here, beside the destination, rather than beside the + // Import button where it would read as a warning. if root.upload-target != "": Caption { - text: "Copied here and verified first, then uploaded."; + text: "Copied to this computer and verified first, then uploaded. Anything that does not go up stays queued for the next import."; + wrap: word-wrap; + } + + if root.upload-target == "": Caption { + text: "Sign in first — the library this imports into is on the server."; wrap: word-wrap; } @@ -333,7 +330,7 @@ export component ImportPage inherits Rectangle { // The one operation in this application with no undo, // and the only honest place to say so is beside the // control that arms it (FR-NC-7b). - text: "The card is emptied only after every file is verified in the library — and, on a library that syncs, after it has reached the server."; + text: "The card is emptied only of photographs the server has confirmed. Anything that does not upload keeps its card copy — which is then the only copy there is."; wrap: word-wrap; }