diff --git a/docs/traceability.md b/docs/traceability.md index 43f23ea..00929da 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 238 | -| TRACES tags found | 624 | +| Source files scanned | 241 | +| TRACES tags found | 627 | | Requirements defined | 177 | | Requirements covered | 97 | | **Coverage** | **54.8%** (97/177) | @@ -34,84 +34,84 @@ _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:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`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:116`](../core/dr-types/src/settings.rs#L116), [`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:1003`](../ui/dr-ui/src/lib.rs#L1003), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1007`](../ui/dr-ui/ui/library.slint#L1007), [`ui/dr-ui/ui/library.slint:1161`](../ui/dr-ui/ui/library.slint#L1161), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) | -| 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:1003`](../ui/dr-ui/src/lib.rs#L1003), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | +| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1028`](../ui/dr-ui/src/lib.rs#L1028), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1008`](../ui/dr-ui/ui/library.slint#L1008), [`ui/dr-ui/ui/library.slint:1170`](../ui/dr-ui/ui/library.slint#L1170), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) | +| 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:1028`](../ui/dr-ui/src/lib.rs#L1028), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120) | | 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:536`](../core/dr-catalog/src/schema.rs#L536), [`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:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`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:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:3007`](../ui/dr-ui/src/library.rs#L3007), [`ui/dr-ui/src/library.rs:3041`](../ui/dr-ui/src/library.rs#L3041), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`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:53`](../core/dr-types/src/lib.rs#L53) | | 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: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:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:513`](../ui/dr-ui/ui/app.slint#L513), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`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:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:514`](../ui/dr-ui/ui/app.slint#L514), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`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), [`core/dr-catalog/src/schema.rs:298`](../core/dr-catalog/src/schema.rs#L298), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`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:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-catalog/src/schema.rs:954`](../core/dr-catalog/src/schema.rs#L954), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | -| 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:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/app.slint:826`](../ui/dr-ui/ui/app.slint#L826), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1278`](../ui/dr-ui/ui/library.slint#L1278), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) | -| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`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/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | -| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:2790`](../ui/dr-ui/src/develop.rs#L2790), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1240`](../ui/dr-ui/src/lib.rs#L1240), [`ui/dr-ui/src/lib.rs:1580`](../ui/dr-ui/src/lib.rs#L1580), [`ui/dr-ui/src/lib.rs:1685`](../ui/dr-ui/src/lib.rs#L1685), [`ui/dr-ui/src/lib.rs:465`](../ui/dr-ui/src/lib.rs#L465), [`ui/dr-ui/src/lib.rs:850`](../ui/dr-ui/src/lib.rs#L850), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.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:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-catalog/src/schema.rs:954`](../core/dr-catalog/src/schema.rs#L954), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:459`](../ui/dr-ui/ui/app.slint#L459), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902) | +| 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:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:462`](../ui/dr-ui/ui/app.slint#L462), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/app.slint:857`](../ui/dr-ui/ui/app.slint#L857), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) | +| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`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/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | +| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:2790`](../ui/dr-ui/src/develop.rs#L2790), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1265`](../ui/dr-ui/src/lib.rs#L1265), [`ui/dr-ui/src/lib.rs:1605`](../ui/dr-ui/src/lib.rs#L1605), [`ui/dr-ui/src/lib.rs:1710`](../ui/dr-ui/src/lib.rs#L1710), [`ui/dr-ui/src/lib.rs:467`](../ui/dr-ui/src/lib.rs#L467), [`ui/dr-ui/src/lib.rs:852`](../ui/dr-ui/src/lib.rs#L852), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`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:508`](../core/dr-catalog/src/schema.rs#L508), [`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:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2325`](../ui/dr-ui/src/develop.rs#L2325), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3408`](../ui/dr-ui/src/library.rs#L3408), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`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-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | -| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337) | -| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337) | +| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | +| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | +| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | 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:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) | | FR-CULL-8 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | -| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | +| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`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:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`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:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`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:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1276`](../ui/dr-ui/src/lib.rs#L1276), [`ui/dr-ui/src/lib.rs:1935`](../ui/dr-ui/src/lib.rs#L1935), [`ui/dr-ui/src/lib.rs:291`](../ui/dr-ui/src/lib.rs#L291), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) | -| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`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:18`](../core/dr-pipeline/src/graph.rs#L18), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:580`](../ui/dr-ui/src/lib.rs#L580) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`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:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`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:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`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:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1301`](../ui/dr-ui/src/lib.rs#L1301), [`ui/dr-ui/src/lib.rs:1960`](../ui/dr-ui/src/lib.rs#L1960), [`ui/dr-ui/src/lib.rs:293`](../ui/dr-ui/src/lib.rs#L293), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1975`](../ui/dr-ui/ui/app.slint#L1975), [`ui/dr-ui/ui/app.slint:944`](../ui/dr-ui/ui/app.slint#L944) | +| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`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:18`](../core/dr-pipeline/src/graph.rs#L18), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:582`](../ui/dr-ui/src/lib.rs#L582) | | 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:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3921`](../ui/dr-ui/src/develop.rs#L3921) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:433`](../core/dr-gpu/tests/capture_sharpen.rs#L433), [`core/dr-gpu/tests/detail_stage.rs:241`](../core/dr-gpu/tests/detail_stage.rs#L241), [`core/dr-gpu/tests/local_contrast.rs:475`](../core/dr-gpu/tests/local_contrast.rs#L475), [`core/dr-gpu/tests/noise_reduction.rs:555`](../core/dr-gpu/tests/noise_reduction.rs#L555), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:486`](../core/dr-pipeline/src/graph.rs#L486), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) | | 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:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`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:967`](../core/dr-gpu/src/adjust.rs#L967), [`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:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:1491`](../core/dr-pipeline/src/operation.rs#L1491), [`core/dr-pipeline/src/operation.rs:1516`](../core/dr-pipeline/src/operation.rs#L1516), [`core/dr-pipeline/src/operation.rs:1531`](../core/dr-pipeline/src/operation.rs#L1531), [`core/dr-pipeline/src/operation.rs:1555`](../core/dr-pipeline/src/operation.rs#L1555), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:549`](../core/dr-pipeline/src/operation.rs#L549) | -| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2799`](../ui/dr-ui/src/develop.rs#L2799), [`ui/dr-ui/src/lib.rs:1909`](../ui/dr-ui/src/lib.rs#L1909), [`ui/dr-ui/src/lib.rs:513`](../ui/dr-ui/src/lib.rs#L513), [`ui/dr-ui/src/lib.rs:571`](../ui/dr-ui/src/lib.rs#L571), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707) | +| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2799`](../ui/dr-ui/src/develop.rs#L2799), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:515`](../ui/dr-ui/src/lib.rs#L515), [`ui/dr-ui/src/lib.rs:573`](../ui/dr-ui/src/lib.rs#L573), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2430`](../ui/dr-ui/ui/app.slint#L2430), [`ui/dr-ui/ui/app.slint:738`](../ui/dr-ui/ui/app.slint#L738) | | FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`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:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1268`](../ui/dr-ui/src/lib.rs#L1268) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1240`](../ui/dr-ui/src/lib.rs#L1240), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`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:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1293`](../ui/dr-ui/src/lib.rs#L1293) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1265`](../ui/dr-ui/src/lib.rs#L1265), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`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:299`](../core/dr-pipeline/src/operation.rs#L299) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs: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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`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:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`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:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:60`](../ui/dr-ui/src/lib.rs#L60), [`ui/dr-ui/src/lib.rs:721`](../ui/dr-ui/src/lib.rs#L721) | +| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs: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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`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:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`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:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62), [`ui/dr-ui/src/lib.rs:723`](../ui/dr-ui/src/lib.rs#L723) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`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:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1327`](../ui/dr-ui/src/lib.rs#L1327), [`ui/dr-ui/src/lib.rs:286`](../ui/dr-ui/src/lib.rs#L286), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`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:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1352`](../ui/dr-ui/src/lib.rs#L1352), [`ui/dr-ui/src/lib.rs:288`](../ui/dr-ui/src/lib.rs#L288), [`ui/dr-ui/ui/app.slint:277`](../ui/dr-ui/ui/app.slint#L277), [`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:2400`](../core/dr-gpu/src/adjust.rs#L2400), [`core/dr-pipeline/src/graph.rs:417`](../core/dr-pipeline/src/graph.rs#L417), [`core/dr-pipeline/src/graph.rs:470`](../core/dr-pipeline/src/graph.rs#L470), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`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:595`](../core/dr-types/src/settings.rs#L595), [`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:329`](../ui/dr-ui/src/lib.rs#L329), [`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:590`](../ui/dr-ui/src/settings_ui.rs#L590) | -| 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:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:176`](../ui/dr-ui/src/lib.rs#L176), [`ui/dr-ui/src/lib.rs:1851`](../ui/dr-ui/src/lib.rs#L1851), [`ui/dr-ui/src/lib.rs:329`](../ui/dr-ui/src/lib.rs#L329), [`ui/dr-ui/src/lib.rs:364`](../ui/dr-ui/src/lib.rs#L364), [`ui/dr-ui/src/lib.rs:391`](../ui/dr-ui/src/lib.rs#L391), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1004`](../ui/dr-ui/ui/app.slint#L1004), [`ui/dr-ui/ui/app.slint:1384`](../ui/dr-ui/ui/app.slint#L1384), [`ui/dr-ui/ui/library.slint:1313`](../ui/dr-ui/ui/library.slint#L1313), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | +| 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:331`](../ui/dr-ui/src/lib.rs#L331), [`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:590`](../ui/dr-ui/src/settings_ui.rs#L590) | +| 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:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178), [`ui/dr-ui/src/lib.rs:1876`](../ui/dr-ui/src/lib.rs#L1876), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331), [`ui/dr-ui/src/lib.rs:366`](../ui/dr-ui/src/lib.rs#L366), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1035`](../ui/dr-ui/ui/app.slint#L1035), [`ui/dr-ui/ui/app.slint:1443`](../ui/dr-ui/ui/app.slint#L1443), [`ui/dr-ui/ui/library.slint:1323`](../ui/dr-ui/ui/library.slint#L1323), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`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:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2284`](../ui/dr-ui/src/develop.rs#L2284), [`ui/dr-ui/src/lib.rs:329`](../ui/dr-ui/src/lib.rs#L329) | +| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2284`](../ui/dr-ui/src/develop.rs#L2284), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331) | | 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:391`](../ui/dr-ui/src/lib.rs#L391), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1) | | 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:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:513`](../ui/dr-ui/ui/app.slint#L513), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | +| 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:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:514`](../ui/dr-ui/ui/app.slint#L514), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`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: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:508`](../core/dr-catalog/src/schema.rs#L508), [`core/dr-catalog/src/schema.rs:909`](../core/dr-catalog/src/schema.rs#L909), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1611`](../ui/dr-ui/src/lib.rs#L1611), [`ui/dr-ui/src/lib.rs:2408`](../ui/dr-ui/src/lib.rs#L2408), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2420`](../ui/dr-ui/ui/app.slint#L2420), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`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:973`](../ui/dr-ui/ui/library.slint#L973) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:508`](../core/dr-catalog/src/schema.rs#L508), [`core/dr-catalog/src/schema.rs:909`](../core/dr-catalog/src/schema.rs#L909), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1636`](../ui/dr-ui/src/lib.rs#L1636), [`ui/dr-ui/src/lib.rs:2433`](../ui/dr-ui/src/lib.rs#L2433), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2480`](../ui/dr-ui/ui/app.slint#L2480), [`ui/dr-ui/ui/app.slint:607`](../ui/dr-ui/ui/app.slint#L607), [`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:974`](../ui/dr-ui/ui/library.slint#L974) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) | | 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:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`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:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340) | -| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1003`](../ui/dr-ui/src/lib.rs#L1003), [`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:1003`](../ui/dr-ui/src/lib.rs#L1003) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1580`](../ui/dr-ui/src/lib.rs#L1580), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | +| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1028`](../ui/dr-ui/src/lib.rs#L1028), [`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:1028`](../ui/dr-ui/src/lib.rs#L1028) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1605`](../ui/dr-ui/src/lib.rs#L1605), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | | FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:332`](../core/dr-pipeline/src/sidecar.rs#L332), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | -| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:790`](../ui/dr-ui/src/lib.rs#L790), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | +| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:792`](../ui/dr-ui/src/lib.rs#L792), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) | | FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) | -| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:176`](../ui/dr-ui/src/lib.rs#L176) | +| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178) | | 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:2490`](../ui/dr-ui/src/lib.rs#L2490), [`ui/dr-ui/src/lib.rs:68`](../ui/dr-ui/src/lib.rs#L68), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:68`](../ui/dr-ui/src/lib.rs#L68), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2142`](../ui/dr-ui/ui/app.slint#L2142), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:606`](../ui/dr-ui/ui/app.slint#L606), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`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:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1611`](../ui/dr-ui/ui/app.slint#L1611), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | -| 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:2135`](../ui/dr-ui/src/lib.rs#L2135), [`ui/dr-ui/src/lib.rs:2524`](../ui/dr-ui/src/lib.rs#L2524), [`ui/dr-ui/src/lib.rs:2709`](../ui/dr-ui/src/lib.rs#L2709), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:297`](../ui/dr-ui/ui/app.slint#L297), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2515`](../ui/dr-ui/src/lib.rs#L2515), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1040`](../ui/dr-ui/ui/library.slint#L1040) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2202`](../ui/dr-ui/ui/app.slint#L2202), [`ui/dr-ui/ui/app.slint:263`](../ui/dr-ui/ui/app.slint#L263), [`ui/dr-ui/ui/app.slint:637`](../ui/dr-ui/ui/app.slint#L637), [`ui/dr-ui/ui/app.slint:644`](../ui/dr-ui/ui/app.slint#L644), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1975`](../ui/dr-ui/ui/app.slint#L1975), [`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:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/app.slint:644`](../ui/dr-ui/ui/app.slint#L644), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | +| 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:2160`](../ui/dr-ui/src/lib.rs#L2160), [`ui/dr-ui/src/lib.rs:2549`](../ui/dr-ui/src/lib.rs#L2549), [`ui/dr-ui/src/lib.rs:2734`](../ui/dr-ui/src/lib.rs#L2734), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:298`](../ui/dr-ui/ui/app.slint#L298), [`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), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`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:1885`](../ui/dr-ui/src/lib.rs#L1885), [`ui/dr-ui/ui/app.slint:1015`](../ui/dr-ui/ui/app.slint#L1015), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`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:1910`](../ui/dr-ui/src/lib.rs#L1910), [`ui/dr-ui/ui/app.slint:1046`](../ui/dr-ui/ui/app.slint#L1046), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | 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) | @@ -124,10 +124,10 @@ _None._ | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | | NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | -| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:60`](../ui/dr-ui/src/lib.rs#L60) | +| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62) | | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:508`](../core/dr-catalog/src/schema.rs#L508), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | -| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | +| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) | | R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | diff --git a/ui/dr-ui/src/faces.rs b/ui/dr-ui/src/faces.rs index 34e6524..77bbbda 100644 --- a/ui/dr-ui/src/faces.rs +++ b/ui/dr-ui/src/faces.rs @@ -446,6 +446,82 @@ fn group_probability( } } +/// A face cut out of its photograph, ready to draw. +#[derive(Debug, Clone, PartialEq)] +pub struct FaceCrop { + pub width: u32, + pub height: u32, + /// Tightly packed RGBA. + pub rgba: Vec, +} + +/// How much of the surrounding frame a face crop keeps, per side. +/// +/// A face cut exactly to its detection box reads as a mugshot: no hair, no +/// chin, no context, and a row of them is genuinely hard to tell apart — which +/// matters, because telling them apart is the entire task the People screen +/// asks of the user. A third on each side gives back the head. +const CROP_MARGIN: f32 = 0.35; + +/// Cut one face out of its proxy. +/// +/// The box is normalised to the long edge (the catalog's convention), so this +/// works whatever size the proxy happens to be now — the property that made +/// normalising worth the trouble. The thumbnail cache is entitled to evict a +/// proxy and regenerate it at another resolution, and a face stored in pixels +/// would then point at the wrong part of the picture. +pub fn crop_face( + rgba: &[u8], + width: u32, + height: u32, + face: &faces::Face, + out_edge: u32, +) -> Option { + if width == 0 || height == 0 || out_edge == 0 { + return None; + } + let long_edge = width.max(height) as f32; + + // Square, centred on the face: the grid draws square cells, and cropping to + // a square here rather than letterboxing there means the face fills the + // cell instead of floating in it. + let cx = (face.x + face.w * 0.5) * long_edge; + let cy = (face.y + face.h * 0.5) * long_edge; + let half = (face.w.max(face.h) * long_edge * 0.5) * (1.0 + CROP_MARGIN); + if !(half.is_finite() && half > 0.5) { + return None; + } + + let mut out = vec![0u8; (out_edge * out_edge * 4) as usize]; + let step = (half * 2.0) / out_edge as f32; + + for oy in 0..out_edge { + let sy = cy - half + (oy as f32 + 0.5) * step; + for ox in 0..out_edge { + let sx = cx - half + (ox as f32 + 0.5) * step; + let o = ((oy * out_edge + ox) * 4) as usize; + // Nearest neighbour: this is a downscale of an already-small proxy + // shown at ~96 px, and a bilinear tap would cost four reads per + // pixel for a difference nobody can see at that size. Outside the + // frame stays transparent, so a face at the very edge of the + // picture is drawn short rather than smeared. + if sx < 0.0 || sy < 0.0 || sx >= width as f32 || sy >= height as f32 { + continue; + } + let i = ((sy as u32 * width + sx as u32) * 4) as usize; + if i + 4 <= rgba.len() { + out[o..o + 4].copy_from_slice(&rgba[i..i + 4]); + } + } + } + + Some(FaceCrop { + width: out_edge, + height: out_edge, + rgba: out, + }) +} + /// `dr-thumbs` decodes to RGBA; `dr-face` reads packed `f32` RGB. fn rgba_to_rgb_f32(rgba: &[u8]) -> Vec { let mut out = Vec::with_capacity(rgba.len() / 4 * 3); @@ -470,6 +546,87 @@ mod tests { assert!((n[2].0 - 1.0).abs() < 1e-6); } + fn gradient(width: u32, height: u32) -> Vec { + let mut v = vec![0u8; (width * height * 4) as usize]; + for (i, px) in v.chunks_exact_mut(4).enumerate() { + // A gradient, so a mis-placed crop shows up as the wrong value + // rather than as more of the same colour. + px[0] = (i % 251) as u8; + px[1] = 40; + px[2] = 90; + px[3] = 255; + } + v + } + + fn stored_face(x: f32, y: f32, w: f32, h: f32) -> faces::Face { + faces::Face { + id: faces::FaceId(1), + image_id: ImageId(1), + x, + y, + w, + h, + landmarks: [(0.0, 0.0); 5], + confidence: 0.9, + crop_px: 120.0, + model_id: "w600k_mbf".into(), + person: None, + probability: 0.0, + confirmed: false, + } + } + + #[test] + fn a_face_crop_is_square_and_the_size_asked_for() { + let px = gradient(1024, 683); + let c = crop_face(&px, 1024, 683, &stored_face(0.3, 0.2, 0.1, 0.15), 96).unwrap(); + assert_eq!((c.width, c.height), (96, 96)); + assert_eq!(c.rgba.len(), 96 * 96 * 4); + } + + #[test] + fn a_degenerate_box_yields_no_crop_rather_than_a_panic() { + let px = gradient(64, 64); + assert!(crop_face(&px, 64, 64, &stored_face(0.5, 0.5, 0.0, 0.0), 96).is_none()); + assert!(crop_face(&px, 0, 0, &stored_face(0.1, 0.1, 0.2, 0.2), 96).is_none()); + assert!(crop_face(&px, 64, 64, &stored_face(0.1, 0.1, 0.2, 0.2), 0).is_none()); + } + + /// A face at the very edge of the frame is drawn short, not smeared: the + /// out-of-frame margin stays transparent. + #[test] + fn a_face_at_the_edge_keeps_a_transparent_margin() { + let px = gradient(200, 200); + let c = crop_face(&px, 200, 200, &stored_face(0.0, 0.0, 0.1, 0.1), 32).unwrap(); + assert_eq!(c.rgba[3], 0, "outside the frame should be transparent"); + let centre = ((16 * 32 + 16) * 4 + 3) as usize; + assert_eq!(c.rgba[centre], 255, "the face itself should be opaque"); + } + + /// The normalised box means a proxy regenerated at another resolution still + /// crops the same part of the picture — what the catalog's normalisation is + /// for. + #[test] + fn the_same_face_crops_the_same_region_at_two_proxy_sizes() { + let face = stored_face(0.25, 0.25, 0.2, 0.2); + let small = crop_face(&gradient(400, 400), 400, 400, &face, 16).unwrap(); + let large = crop_face(&gradient(800, 800), 800, 800, &face, 16).unwrap(); + assert!(small.rgba.chunks_exact(4).all(|p| p[3] == 255)); + assert!(large.rgba.chunks_exact(4).all(|p| p[3] == 255)); + } + + #[test] + fn the_crop_keeps_margin_around_the_detection_box() { + // A 0.1-wide face in a 1000px frame is 100px; with the margin the crop + // spans 100 * 1.35 = 135px of source. + let face = stored_face(0.4, 0.4, 0.1, 0.1); + let c = crop_face(&gradient(1000, 1000), 1000, 1000, &face, 135).unwrap(); + assert_eq!(c.width, 135); + // Fully inside the frame, so nothing is transparent. + assert!(c.rgba.chunks_exact(4).all(|p| p[3] == 255)); + } + #[test] fn rgba_becomes_packed_rgb_dropping_alpha() { let rgba = [255u8, 128, 0, 255, 0, 0, 0, 128]; diff --git a/ui/dr-ui/src/identity.rs b/ui/dr-ui/src/identity.rs new file mode 100644 index 0000000..749e036 --- /dev/null +++ b/ui/dr-ui/src/identity.rs @@ -0,0 +1,574 @@ +//! TRACES: FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5 +//! The Identity screen's model: who the library knows, and how the user fixes it. +//! +//! A third top-level screen beside the library and develop, because it is a +//! *place you go to work*, not a panel you glance at: naming a cluster, pulling +//! a stranger out of it, and merging two halves of the same person are tasks +//! with their own rhythm, and they need the whole window. +//! +//! # The screen exists because the clustering is wrong +//! +//! That is not pessimism, it is FR-CULL-10: clustering will over-merge on +//! siblings, on parents and children, and on the same person a decade apart. A +//! tool that could only merge would make its own errors permanent, so +//! **splitting is as prominent as merging here**, and every suggestion is +//! visibly a suggestion until the user says otherwise. +//! +//! # What this module is, and is not +//! +//! It builds a view model from the catalog and applies the user's decisions +//! back to it. It holds no Slint types, so the whole of it is testable against +//! an in-memory catalog with no window open — which matters because the +//! operations it performs are the ones that touch user data. + +use dr_catalog::faces::{self, FaceId, PersonId}; +use dr_catalog::Catalog; +use dr_thumbs::ThumbStore; +use dr_types::ImageId; + +use crate::faces::{crop_face, FaceCrop, FACE_TIER}; + +/// Edge of a face crop in the grid, in pixels. +/// +/// Generous for a thumbnail because the judgement being asked for — *is this +/// the same person* — is one the user cannot make from a postage stamp. +pub const FACE_CROP_EDGE: u32 = 128; + +/// A row in the people rail. +#[derive(Debug, Clone, PartialEq)] +pub struct PersonRow { + pub id: PersonId, + /// Empty for a cluster the system found and the user has not named. + pub name: String, + pub confirmed_faces: u64, + pub suggested_faces: u64, + /// The face to show as this person's portrait, if any is loaded. + pub cover: Option, +} + +impl PersonRow { + /// What to draw when the person has no name yet. + /// + /// Deliberately not a guessed name. FR-CULL-10 has the *user* name a group, + /// and a system-chosen name would be an inference wearing a fact's clothes + /// — the exact conflation the confirmed/suggested split exists to prevent. + pub fn display_name(&self) -> String { + if self.name.is_empty() { + format!("Unnamed ({} faces)", self.confirmed_faces + self.suggested_faces) + } else { + self.name.clone() + } + } + + /// Whether this group is still entirely the system's opinion. + pub fn is_unconfirmed(&self) -> bool { + self.confirmed_faces == 0 + } +} + +/// One face in the grid. +#[derive(Debug, Clone, PartialEq)] +pub struct FaceCell { + pub face: FaceId, + pub image: ImageId, + /// `None` while the crop is still being cut. + pub crop: Option, + /// The user asserted this, as against the system guessing it. + pub confirmed: bool, + /// Calibrated P(this face is this person). + pub probability: f32, + /// Whether that probability means anything — false when the library's + /// calibration has not been fitted (FR-CULL-9). + pub probability_known: bool, + /// Source pixels across the aligned crop. Small faces embed worse, and the + /// user deserves to know which of a bad suggestion's causes is in play. + pub crop_px: f32, +} + +impl FaceCell { + /// Confidence as text, or why there is none. + /// + /// The FR-CULL-9 rule made concrete: where the calibration is not fitted, + /// this says so rather than printing an untuned number that looks measured. + pub fn confidence_label(&self) -> String { + if self.confirmed { + "Confirmed".into() + } else if !self.probability_known { + "Confidence unavailable".into() + } else { + format!("{:.0}% likely", self.probability * 100.0) + } + } +} + +/// Everything the Identity screen draws. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct IdentityView { + pub people: Vec, + pub selected: Option, + pub faces: Vec, + /// Faces belonging to nobody — the pool the user can pull a new person out + /// of, and the honest answer to "why is this photo not under anyone". + pub unassigned: usize, + /// Whether the library's similarity calibration has been fitted. + pub calibrated: bool, +} + +/// Read the people rail. +pub fn load_people(catalog: &Catalog, model_id: &str) -> Result { + let conn = catalog.connection(); + let people = faces::people(conn)? + .into_iter() + .map(|p| PersonRow { + id: p.id, + name: p.name, + confirmed_faces: p.confirmed_faces, + suggested_faces: p.suggested_faces, + cover: None, + }) + .collect(); + + Ok(IdentityView { + people, + selected: None, + faces: Vec::new(), + unassigned: faces::unassigned(conn, model_id)?.len(), + calibrated: faces::calibration(conn, model_id)?.is_some_and(|(c, _)| c.valid), + }) +} + +/// Read one person's faces, cutting a crop for each. +/// +/// Suggestions are included and marked, never hidden: the whole purpose of this +/// screen is ruling on them, and a screen that showed only confirmed faces +/// would give the user nothing to do. +/// +/// Crops come from the proxy the grid already built. An image whose proxy has +/// been evicted yields a cell with no crop rather than being dropped — the face +/// is still real, still counted, and still confirmable from its filename. +pub fn load_faces( + catalog: &Catalog, + store: &ThumbStore, + person: PersonId, + calibrated: bool, +) -> Result, dr_catalog::CatalogError> { + let conn = catalog.connection(); + let rows = faces::for_person(conn, person, true)?; + + // One decode per *image*, not per face: a group photograph holding six + // faces of one family is one JPEG, and decoding it six times is the kind of + // waste that only shows up on a slow machine. + let mut decoded: std::collections::HashMap)>> = + std::collections::HashMap::new(); + + let mut out = Vec::with_capacity(rows.len()); + for f in rows { + let entry = decoded + .entry(f.image_id) + .or_insert_with(|| decode_proxy(catalog, store, f.image_id)); + + let crop = entry + .as_ref() + .and_then(|(w, h, rgba)| crop_face(rgba, *w, *h, &f, FACE_CROP_EDGE)); + + out.push(FaceCell { + face: f.id, + image: f.image_id, + crop, + confirmed: f.confirmed, + probability: f.probability, + probability_known: calibrated, + crop_px: f.crop_px, + }); + } + + Ok(out) +} + +fn decode_proxy( + catalog: &Catalog, + store: &ThumbStore, + image: ImageId, +) -> Option<(u32, u32, Vec)> { + let file_id: i64 = catalog + .connection() + .query_row( + "SELECT file_id FROM remote WHERE image_id = ?1", + [image.0 as i64], + |r| r.get(0), + ) + .ok()?; + let thumb = store.get(file_id as u64, FACE_TIER).ok()??; + dr_thumbs::codec::decode_rgba(&thumb.bytes).ok() +} + +// ── the user's decisions ────────────────────────────────────────────────── + +/// Name a group, or rename a person. +/// +/// The identity is untouched: FR-CULL-12 makes a *name* the user data that +/// travels to the sidecar, while the person's UUID is what a cross-device merge +/// keys on, and renaming must not create a second person on the other device. +pub fn rename( + catalog: &Catalog, + person: PersonId, + name: &str, +) -> Result<(), dr_catalog::CatalogError> { + faces::rename_person(catalog.connection(), person, name.trim()) +} + +/// Confirm every suggestion in a group at once. +/// +/// The operation the screen exists to make cheap. A cluster that is simply +/// right — the common case for a well-photographed person — should cost one +/// click, not forty. +pub fn confirm_all( + catalog: &Catalog, + person: PersonId, +) -> Result { + let conn = catalog.connection(); + let mut n = 0; + for f in faces::for_person(conn, person, true)? { + if !f.confirmed { + faces::confirm(conn, f.id, person)?; + n += 1; + } + } + Ok(n) +} + +/// The user says this face is this person. +pub fn confirm( + catalog: &Catalog, + face: FaceId, + person: PersonId, +) -> Result<(), dr_catalog::CatalogError> { + faces::confirm(catalog.connection(), face, person) +} + +/// The user says this face is not this person. +/// +/// Remembered, so the next clustering pass does not put it straight back — +/// which is the difference between a correction and a recurring argument. +pub fn reject( + catalog: &Catalog, + face: FaceId, + person: PersonId, +) -> Result<(), dr_catalog::CatalogError> { + faces::reject(catalog.connection(), face, person) +} + +/// Fold one person into another. +pub fn merge( + catalog: &Catalog, + target: PersonId, + source: PersonId, +) -> Result { + faces::merge_people(catalog.connection(), target, source) +} + +/// What a split would produce, without performing it. +/// +/// Offered as a preview because a split is the destructive-feeling half of +/// FR-CULL-10 and the user should see the groups before agreeing to them. It +/// re-agglomerates this person's faces at a stricter threshold; a person who is +/// genuinely one person comes back as one group and the UI can say so instead +/// of splitting nothing. +pub fn preview_split( + catalog: &Catalog, + store: &ThumbStore, + person: PersonId, + model_id: &str, + strictness: f32, +) -> Result>, dr_catalog::CatalogError> { + let conn = catalog.connection(); + let cal = faces::calibration(conn, model_id)? + .map(|(c, _)| c) + .unwrap_or_default(); + let calibrated = cal.valid; + + let cells = load_faces(catalog, store, person, calibrated)?; + if cells.len() < 2 { + return Ok(vec![cells]); + } + + let model = dr_face::ModelId::new(model_id.to_string()); + let stored: std::collections::HashMap<_, _> = faces::embeddings(conn, model_id)? + .into_iter() + .map(|(id, image, blob, crop_px)| (id, (image, blob, crop_px))) + .collect(); + + let mut candidates = Vec::with_capacity(cells.len()); + let mut order = Vec::with_capacity(cells.len()); + for c in &cells { + let Some((image, blob, crop_px)) = stored.get(&c.face) else { + continue; + }; + let Some(emb) = dr_face::Embedding::from_f16_bytes(model.clone(), blob) else { + continue; + }; + candidates.push(dr_face::Candidate { + face: c.face.0, + image: image.0, + embedding: emb.v.to_vec(), + crop_px: *crop_px, + confirmed_person: None, + }); + order.push(c.clone()); + } + + let groups = dr_face::split(&candidates, &cal, strictness); + Ok(groups + .into_iter() + .map(|g| g.members.into_iter().map(|m| order[m].clone()).collect()) + .collect()) +} + +/// Pull a set of faces out of a person and onto a new one. +/// +/// The commit half of [`preview_split`], and also what "these three are +/// actually someone else" does. The faces are **confirmed** onto the new +/// person, because the user has just asserted they belong together — leaving +/// them as suggestions would invite the next clustering pass to undo the +/// correction. +pub fn split_off( + catalog: &Catalog, + from: PersonId, + members: &[FaceId], + name: &str, +) -> Result { + let conn = catalog.connection(); + let new_person = faces::create_person(conn, name.trim())?; + for &face in members { + // Rejecting first is what stops the split being undone: without it the + // next pass sees a face that looks like `from` and suggests it straight + // back, and the user's correction becomes an argument they keep having. + faces::reject(conn, face, from)?; + faces::confirm(conn, face, new_person)?; + } + Ok(new_person) +} + +/// Detach a face from everyone, asserting nothing. +/// +/// Distinct from [`reject`]: this is "I do not know", and the face returns to +/// the pool for the next clustering pass to place. Needed for the case where +/// the user can see a crop is a lamp, not a person — rejecting it from *this* +/// person would leave it free to be suggested for the next one. +pub fn unassign(catalog: &Catalog, face: FaceId) -> Result<(), dr_catalog::CatalogError> { + faces::unassign(catalog.connection(), face) +} + +/// Delete every face, person and embedding in the library (NFR-SEC-5). +/// +/// Surfaced from this screen because this is where the user's face data +/// visibly lives, and a control to remove it that is hidden in a settings page +/// three levels down is a control that does not really exist. +pub fn delete_all(catalog: &Catalog) -> Result { + faces::delete_all_face_data(catalog.connection()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dr_catalog::faces::DetectedFace; + + fn catalog() -> Catalog { + let dir = std::env::temp_dir().join(format!( + "dr-identity-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("catalog.db"); + let _ = std::fs::remove_file(&path); + Catalog::open(&path).unwrap() + } + + fn image(c: &Catalog, n: i64) -> ImageId { + let conn = c.connection(); + conn.execute( + "INSERT OR IGNORE INTO roots(id, kind, label) VALUES (1, 'local', 'lib')", + [], + ) + .unwrap(); + conn.execute( + "INSERT INTO images(id, root_id, source_ref, added_at) VALUES (?1, 1, ?2, 0)", + rusqlite::params![n, format!("IMG_{n}.CR3")], + ) + .unwrap(); + ImageId(n as u64) + } + + fn face(seed: u8) -> DetectedFace { + DetectedFace { + x: 0.1, + y: 0.1, + w: 0.2, + h: 0.3, + landmarks: [(0.1, 0.1); 5], + confidence: 0.9, + embedding: vec![seed; 1024], + crop_px: 180.0, + model_id: "w600k_mbf".into(), + } + } + + #[test] + fn an_unnamed_group_says_how_many_faces_it_holds_rather_than_guessing_a_name() { + let row = PersonRow { + id: PersonId(1), + name: String::new(), + confirmed_faces: 0, + suggested_faces: 12, + cover: None, + }; + assert_eq!(row.display_name(), "Unnamed (12 faces)"); + assert!(row.is_unconfirmed()); + } + + #[test] + fn a_named_person_shows_their_name() { + let row = PersonRow { + id: PersonId(1), + name: "Anna".into(), + confirmed_faces: 4, + suggested_faces: 2, + cover: None, + }; + assert_eq!(row.display_name(), "Anna"); + assert!(!row.is_unconfirmed()); + } + + /// FR-CULL-9's rule at the point it becomes visible: an unfitted + /// calibration must not print a number that looks measured. + #[test] + fn an_uncalibrated_library_says_so_instead_of_showing_a_percentage() { + let cell = FaceCell { + face: FaceId(1), + image: ImageId(1), + crop: None, + confirmed: false, + probability: 0.87, + probability_known: false, + crop_px: 120.0, + }; + assert_eq!(cell.confidence_label(), "Confidence unavailable"); + + let calibrated = FaceCell { + probability_known: true, + ..cell.clone() + }; + assert_eq!(calibrated.confidence_label(), "87% likely"); + + let confirmed = FaceCell { + confirmed: true, + probability_known: false, + ..cell + }; + assert_eq!(confirmed.confidence_label(), "Confirmed"); + } + + #[test] + fn the_people_rail_reports_the_unassigned_pool() { + let c = catalog(); + let i1 = image(&c, 1); + let i2 = image(&c, 2); + let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); + faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); + + let anna = faces::create_person(c.connection(), "Anna").unwrap(); + faces::confirm(c.connection(), a[0], anna).unwrap(); + + let view = load_people(&c, "w600k_mbf").unwrap(); + assert_eq!(view.people.len(), 1); + assert_eq!(view.people[0].name, "Anna"); + assert_eq!(view.unassigned, 1); + assert!(!view.calibrated, "a fresh library has no fitted calibration"); + } + + #[test] + fn confirm_all_promotes_every_suggestion_and_nothing_else() { + let c = catalog(); + let i1 = image(&c, 1); + let i2 = image(&c, 2); + let i3 = image(&c, 3); + let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); + let b = faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); + let d = faces::record_detections(c.connection(), i3, &[face(3)]).unwrap(); + + let anna = faces::create_person(c.connection(), "Anna").unwrap(); + faces::confirm(c.connection(), a[0], anna).unwrap(); + faces::suggest(c.connection(), b[0], anna, 0.8).unwrap(); + faces::suggest(c.connection(), d[0], anna, 0.7).unwrap(); + + assert_eq!(confirm_all(&c, anna).unwrap(), 2); + assert_eq!(confirm_all(&c, anna).unwrap(), 0, "should be idempotent"); + assert_eq!(faces::for_person(c.connection(), anna, false).unwrap().len(), 3); + } + + /// The correction must stick. Splitting a face off and then re-running + /// clustering must not put it back where the user took it from. + #[test] + fn a_split_face_is_not_suggested_back_to_the_person_it_left() { + let c = catalog(); + let i1 = image(&c, 1); + let i2 = image(&c, 2); + let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); + let b = faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); + + let anna = faces::create_person(c.connection(), "Anna").unwrap(); + faces::confirm(c.connection(), a[0], anna).unwrap(); + faces::suggest(c.connection(), b[0], anna, 0.95).unwrap(); + + let bob = split_off(&c, anna, &[b[0]], "Bob").unwrap(); + assert_eq!(faces::for_person(c.connection(), bob, false).unwrap().len(), 1); + assert_eq!(faces::for_person(c.connection(), anna, true).unwrap().len(), 1); + + // The next clustering pass tries again and is refused. + assert!( + !faces::suggest(c.connection(), b[0], anna, 0.99).unwrap(), + "the split was undone by the next inference pass" + ); + } + + #[test] + fn renaming_trims_and_keeps_the_person() { + let c = catalog(); + let anna = faces::create_person(c.connection(), "").unwrap(); + rename(&c, anna, " Anna Smith ").unwrap(); + let people = faces::people(c.connection()).unwrap(); + assert_eq!(people[0].name, "Anna Smith"); + assert_eq!(people[0].id, anna); + } + + #[test] + fn merging_folds_one_person_into_the_other() { + let c = catalog(); + let i1 = image(&c, 1); + let i2 = image(&c, 2); + let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); + let b = faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); + let anna = faces::create_person(c.connection(), "Anna").unwrap(); + let annie = faces::create_person(c.connection(), "Annie").unwrap(); + faces::confirm(c.connection(), a[0], anna).unwrap(); + faces::confirm(c.connection(), b[0], annie).unwrap(); + + assert_eq!(merge(&c, anna, annie).unwrap(), 1); + assert_eq!(load_people(&c, "w600k_mbf").unwrap().people.len(), 1); + } + + #[test] + fn deleting_everything_empties_the_screen() { + let c = catalog(); + let i1 = image(&c, 1); + let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); + let anna = faces::create_person(c.connection(), "Anna").unwrap(); + faces::confirm(c.connection(), a[0], anna).unwrap(); + + assert_eq!(delete_all(&c).unwrap(), 1); + let view = load_people(&c, "w600k_mbf").unwrap(); + assert!(view.people.is_empty()); + assert_eq!(view.unassigned, 0); + } +} diff --git a/ui/dr-ui/src/identity_ui.rs b/ui/dr-ui/src/identity_ui.rs new file mode 100644 index 0000000..2ad724c --- /dev/null +++ b/ui/dr-ui/src/identity_ui.rs @@ -0,0 +1,400 @@ +//! TRACES: FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | NFR-SEC-5 +//! The Identity screen's wiring: catalog state onto Slint models and back. +//! +//! [`crate::identity`] holds the decisions and is testable without a window; +//! this file is the part that cannot be, and it is deliberately thin. Anything +//! here that starts making a judgement belongs over there. + +use std::cell::RefCell; +use std::rc::Rc; + +use dr_catalog::faces::{FaceId, PersonId}; +use dr_catalog::Catalog; +use dr_thumbs::ThumbStore; +use slint::{ComponentHandle, ModelRc, VecModel}; + +use crate::identity::{self, FaceCell, PersonRow}; +use crate::{AppWindow, IdentityFace, IdentityPerson}; + +/// Which model's faces the screen is looking at. +/// +/// A constant for now because exactly one model is supported at a time; it is +/// named rather than inlined because every query in this file is keyed on it, +/// and a library indexed across a model change holds faces from both. +pub const MODEL_ID: &str = "w600k_mbf"; + +/// Screen state that outlives a single callback. +#[derive(Default)] +pub struct IdentityController { + people: RefCell>, + faces: RefCell>, + selected: std::cell::Cell>, + /// Faces the user has ticked, which is what a split would carry. + picked: RefCell>, +} + +impl IdentityController { + pub fn new() -> Self { + Self::default() + } + + /// Clear the multi-select. + /// + /// Called on every person change: a pick set that survived navigating to + /// another person would make the next "Split off" act on faces the user + /// can no longer see, which is the kind of surprise that loses data. + fn clear_picks(&self) { + self.picked.borrow_mut().clear(); + } +} + +/// Push the people rail and the face grid into the window. +pub fn refresh( + window: &AppWindow, + ctl: &IdentityController, + catalog: &Rc>>, + store: Option<&ThumbStore>, +) { + let borrow = catalog.borrow(); + let Some(cat) = borrow.as_ref() else { + window.set_identity_people(ModelRc::new(VecModel::from(Vec::::new()))); + window.set_identity_faces(ModelRc::new(VecModel::from(Vec::::new()))); + return; + }; + + let view = match identity::load_people(cat, MODEL_ID) { + Ok(v) => v, + Err(e) => { + log::warn!("identity: reading people: {e}"); + return; + } + }; + + window.set_identity_unassigned(view.unassigned as i32); + window.set_identity_calibrated(view.calibrated); + + let rows: Vec = view + .people + .iter() + .map(|p| IdentityPerson { + id: p.id.0 as i32, + label: p.display_name().into(), + unconfirmed: p.is_unconfirmed(), + confirmed_faces: p.confirmed_faces as i32, + suggested_faces: p.suggested_faces as i32, + cover: slint::Image::default(), + has_cover: false, + }) + .collect(); + window.set_identity_people(ModelRc::new(VecModel::from(rows))); + *ctl.people.borrow_mut() = view.people; + + // The selected person may have just been merged away or deleted. + if let Some(sel) = ctl.selected.get() { + if !ctl.people.borrow().iter().any(|p| p.id == sel) { + ctl.selected.set(None); + ctl.clear_picks(); + } + } + + match (ctl.selected.get(), store) { + (Some(person), Some(store)) => { + let cells = identity::load_faces(cat, store, person, view.calibrated) + .unwrap_or_else(|e| { + log::warn!("identity: reading faces: {e}"); + Vec::new() + }); + push_faces(window, ctl, &cells); + *ctl.faces.borrow_mut() = cells; + + let name = ctl + .people + .borrow() + .iter() + .find(|p| p.id == person) + .map(|p| p.name.clone()) + .unwrap_or_default(); + window.set_identity_selected(person.0 as i32); + window.set_identity_selected_name(name.into()); + } + _ => { + window.set_identity_selected(-1); + window.set_identity_selected_name(Default::default()); + window.set_identity_faces(ModelRc::new(VecModel::from(Vec::::new()))); + ctl.faces.borrow_mut().clear(); + } + } + + window.set_identity_picked(ctl.picked.borrow().len() as i32); +} + +fn push_faces(window: &AppWindow, ctl: &IdentityController, cells: &[FaceCell]) { + let picked = ctl.picked.borrow(); + let rows: Vec = cells + .iter() + .map(|c| IdentityFace { + id: c.face.0 as i32, + crop: c + .crop + .as_ref() + .map(|p| to_slint_image(p.width, p.height, &p.rgba)) + .unwrap_or_default(), + has_crop: c.crop.is_some(), + confirmed: c.confirmed, + confidence: c.confidence_label().into(), + crop_px: c.crop_px as i32, + picked: picked.contains(&c.face), + }) + .collect(); + window.set_identity_faces(ModelRc::new(VecModel::from(rows))); +} + +fn to_slint_image(width: u32, height: u32, rgba: &[u8]) -> slint::Image { + let mut buf = slint::SharedPixelBuffer::::new(width, height); + buf.make_mut_bytes().copy_from_slice(rgba); + slint::Image::from_rgba8(buf) +} + +/// Attach every Identity callback. +pub fn wire( + window: &AppWindow, + ctl: Rc, + catalog: Rc>>, + store: S, +) where + S: Fn() -> Option> + 'static, +{ + let store: Rc Option>> = Rc::new(store); + + // Re-read everything and redraw. Every mutating callback ends in this + // rather than patching the model in place: the operations here have + // second-order effects — a merge empties a person, a split creates one, + // a rejection changes two counts — and a model patched by hand would + // drift from the catalog in exactly the cases that matter. + macro_rules! reload { + ($w:expr, $ctl:expr, $catalog:expr, $store:expr) => { + refresh(&$w, &$ctl, &$catalog, $store().as_deref()) + }; + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_open(move || { + let Some(w) = weak.upgrade() else { return }; + w.set_show_identity(true); + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + window.on_identity_close(move || { + let Some(w) = weak.upgrade() else { return }; + w.set_show_identity(false); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_person_picked(move |id| { + let Some(w) = weak.upgrade() else { return }; + ctl.selected.set(Some(PersonId(id as u64))); + ctl.clear_picks(); + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_rename(move |name| { + let Some(w) = weak.upgrade() else { return }; + let Some(person) = ctl.selected.get() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + if let Err(e) = identity::rename(cat, person, &name) { + log::warn!("identity: rename: {e}"); + } + } + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_confirm_face(move |id| { + let Some(w) = weak.upgrade() else { return }; + let Some(person) = ctl.selected.get() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + if let Err(e) = identity::confirm(cat, FaceId(id as u64), person) { + log::warn!("identity: confirm: {e}"); + } + } + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_reject_face(move |id| { + let Some(w) = weak.upgrade() else { return }; + let Some(person) = ctl.selected.get() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + if let Err(e) = identity::reject(cat, FaceId(id as u64), person) { + log::warn!("identity: reject: {e}"); + } + } + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + window.on_identity_toggle_pick(move |id| { + let Some(w) = weak.upgrade() else { return }; + let face = FaceId(id as u64); + { + let mut picked = ctl.picked.borrow_mut(); + match picked.iter().position(|&f| f == face) { + Some(i) => { + picked.remove(i); + } + None => picked.push(face), + } + } + // Only the pick flags changed, so this is the one case that does + // not re-read the catalog: nothing in it moved. + push_faces(&w, &ctl, &ctl.faces.borrow()); + w.set_identity_picked(ctl.picked.borrow().len() as i32); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_confirm_all(move || { + let Some(w) = weak.upgrade() else { return }; + let Some(person) = ctl.selected.get() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + match identity::confirm_all(cat, person) { + Ok(n) => log::info!("identity: confirmed {n} suggestion(s)"), + Err(e) => log::warn!("identity: confirm all: {e}"), + } + } + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_split_picked(move || { + let Some(w) = weak.upgrade() else { return }; + let Some(person) = ctl.selected.get() else { return }; + let members: Vec = ctl.picked.borrow().clone(); + if members.is_empty() { + return; + } + if let Some(cat) = catalog.borrow().as_ref() { + // Unnamed, so the user names it themselves — the same rule the + // clustering pass follows. A split that guessed a name would + // be presenting an inference as a fact (FR-CULL-10). + match identity::split_off(cat, person, &members, "") { + Ok(new) => log::info!( + "identity: split {} face(s) onto person {:?}", + members.len(), + new + ), + Err(e) => log::warn!("identity: split: {e}"), + } + } + ctl.clear_picks(); + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_recluster(move || { + let Some(w) = weak.upgrade() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + match crate::faces::recluster(cat, MODEL_ID, dr_face::DEFAULT_MERGE_PROBABILITY) { + Ok((s, c)) => log::info!("identity: {s} suggestion(s), {c} new group(s)"), + Err(e) => log::warn!("identity: recluster: {e}"), + } + } + reload!(w, ctl, catalog, store); + }); + } + + { + let weak = window.as_weak(); + let ctl = ctl.clone(); + let catalog = catalog.clone(); + let store = store.clone(); + window.on_identity_delete_all(move || { + let Some(w) = weak.upgrade() else { return }; + if let Some(cat) = catalog.borrow().as_ref() { + match identity::delete_all(cat) { + Ok(n) => log::info!("identity: deleted {n} face(s) and every person"), + Err(e) => log::warn!("identity: delete all: {e}"), + } + } + ctl.selected.set(None); + ctl.clear_picks(); + reload!(w, ctl, catalog, store); + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_pick_toggles_on_and_off() { + let ctl = IdentityController::new(); + let f = FaceId(7); + ctl.picked.borrow_mut().push(f); + assert_eq!(ctl.picked.borrow().len(), 1); + let pos = ctl.picked.borrow().iter().position(|&x| x == f); + assert_eq!(pos, Some(0)); + ctl.picked.borrow_mut().remove(0); + assert!(ctl.picked.borrow().is_empty()); + } + + /// Changing person must not carry a stale pick set: a later "Split off" + /// would otherwise act on faces the user can no longer see. + #[test] + fn changing_person_clears_the_pick_set() { + let ctl = IdentityController::new(); + ctl.picked.borrow_mut().push(FaceId(1)); + ctl.picked.borrow_mut().push(FaceId(2)); + ctl.selected.set(Some(PersonId(1))); + + ctl.selected.set(Some(PersonId(2))); + ctl.clear_picks(); + assert!(ctl.picked.borrow().is_empty()); + } +} diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 4a71751..9822eec 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -23,6 +23,8 @@ mod activity; mod collections_ui; mod derived_sync; pub mod faces; +pub mod identity; +mod identity_ui; mod develop; mod export; mod gradient; @@ -871,6 +873,9 @@ pub fn run(paths: Vec) -> Result<()> { // needs it too: a paste onto "the selection" reads the selection from // here, and that wiring happens once the develop session exists. let collections = collections_ui::CollectionsController::new(activity.clone()); + // The People screen's own state: which person is open, and which of their + // faces are ticked for a split. + let identity = std::rc::Rc::new(identity_ui::IdentityController::new()); // Launch screen: shown when there is nothing to display — no local paths // and no configured library. A user who has already signed in and chosen @@ -948,6 +953,26 @@ pub fn run(paths: Vec) -> Result<()> { // and thumbnail workers use. move || lib_session.session(), ); + + // The People screen reads the same catalog and the same thumbnail + // store the grid does — its face crops come from the proxies the + // grid already built, which is the whole reason face indexing is + // affordable (FR-CULL-8). + let lib_store = library.clone(); + identity_ui::wire( + &window, + identity.clone(), + library.catalog(), + move || { + let (_, session) = lib_store.session()?; + dr_thumbs::ThumbStore::open(&library::thumbs_dir( + &session.server, + &session.user_id, + )) + .ok() + .map(std::rc::Rc::new) + }, + ); } let weak = window.as_weak(); diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 328670a..2a1b17f 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -2,6 +2,7 @@ import { Theme } from "theme.slint"; import { AdjustPanel, GeometryPanel, ModeStrip, ParamRow, TransferPanel, ViewMode } from "adjust.slint"; import { GradientHandle, HandleRole, MaskPanel, MaskRow, SubjectRow } from "masks.slint"; import { LaunchScreen } from "launch.slint"; +import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint"; import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow } from "library.slint"; import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint"; import { CollectionsPanel, CollectionRow, OfflinePrompt } from "collections.slint"; @@ -516,6 +517,36 @@ export component AppWindow inherits Window { /// the page is reachable from the launch screen too. in property library-thumbnailing: false; in property library-open: false; + + // --- people and faces (FR-CULL-8 … FR-CULL-12) --- + // + // A third top-level screen rather than a panel: naming a cluster and + // pulling a stranger out of it are tasks with their own rhythm, and they + // need the whole window. Gated the same way the other screens are, so + // exactly one is ever up. + in property show-identity: false; + in property <[IdentityPerson]> identity-people; + in property <[IdentityFace]> identity-faces; + in property identity-selected: -1; + in property identity-selected-name; + in property identity-unassigned: 0; + in property identity-calibrated: false; + in property identity-indexing: false; + in property identity-indexing-status; + in property identity-model-missing: false; + in property identity-picked: 0; + + callback identity-open(); + callback identity-close(); + callback identity-person-picked(int); + callback identity-rename(string); + callback identity-confirm-face(int); + callback identity-reject-face(int); + callback identity-toggle-pick(int); + callback identity-confirm-all(); + callback identity-split-picked(); + callback identity-recluster(); + callback identity-delete-all(); callback library-thumbnail-all(); in-out property library-cell-size: 180px; /// Whether this session is a touch one, which decides if the rating strip @@ -1236,9 +1267,37 @@ in property panel-visible: true; reset-defaults() => { root.settings-reset(); } } + // Like the launch screen, this replaces the window rather than + // overlaying it — see `show-identity`. + if !root.show-import && !root.show-settings && root.show-identity: IdentityScreen { + width: 100%; + height: 100%; + people: root.identity-people; + faces: root.identity-faces; + selected-person: root.identity-selected; + selected-name: root.identity-selected-name; + unassigned: root.identity-unassigned; + calibrated: root.identity-calibrated; + indexing: root.identity-indexing; + indexing-status: root.identity-indexing-status; + model-missing: root.identity-model-missing; + picked-count: root.identity-picked; + + person-picked(id) => { root.identity-person-picked(id); } + rename(name) => { root.identity-rename(name); } + confirm-face(id) => { root.identity-confirm-face(id); } + reject-face(id) => { root.identity-reject-face(id); } + toggle-pick(id) => { root.identity-toggle-pick(id); } + confirm-all() => { root.identity-confirm-all(); } + split-picked() => { root.identity-split-picked(); } + recluster() => { root.identity-recluster(); } + delete-all-face-data() => { root.identity-delete-all(); } + close() => { root.identity-close(); } + } + // The launch screen replaces the whole window rather than overlaying it: // there is no library to look at until an account is configured. - if !root.show-import && !root.show-settings && root.show-launch: LaunchScreen { + if !root.show-import && !root.show-settings && !root.show-identity && root.show-launch: LaunchScreen { width: 100%; height: 100%; signed-in: root.launch-signed-in; @@ -1278,7 +1337,7 @@ in property panel-visible: true; // has been opened but no image chosen yet. The collections sidebar and the // grid are siblings here rather than the sidebar living inside the grid, // because the drag that connects them has to be owned above both. - if !root.show-import && !root.show-settings && !root.show-launch && root.show-library: Rectangle { + if !root.show-import && !root.show-settings && !root.show-identity && !root.show-launch && root.show-library: Rectangle { width: 100%; height: 100%; background: Theme.ground; @@ -1421,6 +1480,7 @@ in property panel-visible: true; can-import: root.import-supported; open-import() => { root.import-open(); } open-settings() => { root.settings-open(); } + open-people() => { root.identity-open(); } cell-pressed(i, ctrl, shift) => { root.library-cell-pressed(i, ctrl, shift); @@ -1486,7 +1546,7 @@ in property panel-visible: true; } } - if !root.show-import && !root.show-settings && !root.show-launch && !root.show-library: VerticalLayout { + if !root.show-import && !root.show-settings && !root.show-identity && !root.show-launch && !root.show-library: VerticalLayout { width: 100%; height: 100%; diff --git a/ui/dr-ui/ui/identity.slint b/ui/dr-ui/ui/identity.slint new file mode 100644 index 0000000..ec3608b --- /dev/null +++ b/ui/dr-ui/ui/identity.slint @@ -0,0 +1,377 @@ +// TRACES: FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5 +// +// The Identity screen: who the library knows, and how the user corrects it. +// +// A third top-level screen beside the library and develop, because it is a +// place you go to *work*. Naming a cluster, pulling a stranger out of it, and +// merging two halves of the same person have their own rhythm and need the +// whole window. +// +// # The screen is designed around the clustering being wrong +// +// That is FR-CULL-10, not pessimism: grouping over-merges on siblings, on +// parents and children, and on the same person a decade apart. So **split is +// as prominent as merge**, a suggestion always looks like a suggestion, and +// the confirm/reject pair sits on the face itself rather than behind a menu. + +import { Theme } from "theme.slint"; +import { Panel, Button, IconButton, Field } from "widgets.slint"; +import { Icon } from "icons.slint"; + +export struct IdentityPerson { + id: int, + // What to draw. Empty name becomes "Unnamed (n faces)" on the Rust side, + // so the fallback is written once rather than in both languages. + label: string, + // True while the group is entirely the system's opinion. Drawn differently + // because an unreviewed guess and a person the user has vouched for are + // not the same kind of thing, and the rail is where that difference is + // cheapest to show. + unconfirmed: bool, + confirmed-faces: int, + suggested-faces: int, + cover: image, + has-cover: bool, +} + +export struct IdentityFace { + id: int, + crop: image, + has-crop: bool, + confirmed: bool, + // "Confirmed", "83% likely", or "Confidence unavailable" — composed in + // Rust, because FR-CULL-9's rule about not showing an untuned number as + // though it were measured is a rule about *content*, and it should not be + // re-derived from a float in a second place. + confidence: string, + // Source pixels across the aligned crop. A suggestion the user disagrees + // with has causes, and "the face was 41 pixels across" is one the user can + // act on by finding a better photograph. + crop-px: int, + // Part of the current multi-select — what a split would carry. + picked: bool, +} + +// One face, with its verdict controls. +component FaceCell inherits Rectangle { + in property face; + in property compact: false; + + callback confirm(); + callback reject(); + callback toggle-pick(); + + property edge: root.compact ? 96px : 128px; + + width: edge; + height: edge + 34px; + background: face.picked ? Theme.selected : transparent; + border-radius: Theme.radius; + border-width: face.picked ? 1px : 0; + border-color: Theme.selected-ring; + + VerticalLayout { + padding: 3px; + spacing: 3px; + + Rectangle { + height: root.edge - 6px; + clip: true; + border-radius: Theme.radius-sm; + background: Theme.surface-raised; + + if face.has-crop: Image { + source: face.crop; + width: 100%; + height: 100%; + image-fit: cover; + } + // A face whose proxy has been evicted is still a real face and + // still confirmable. Drawing nothing at all would read as a bug. + if !face.has-crop: Text { + text: "no preview"; + color: Theme.ink-faint; + font-size: Theme.text-sm; + horizontal-alignment: center; + vertical-alignment: center; + } + + // A confirmed face carries a quiet marker rather than a badge: the + // grid is mostly confirmed once the user has worked through it, and + // a loud mark on the common case is just noise. + if face.confirmed: Rectangle { + x: parent.width - self.width - 4px; + y: 4px; + width: 16px; + height: 16px; + border-radius: 8px; + background: Theme.active; + Icon { + name: "check"; + ink: Theme.ground; + size: 11px; + } + } + + TouchArea { + clicked => { root.toggle-pick(); } + } + } + + HorizontalLayout { + spacing: 2px; + alignment: center; + + // Only a suggestion needs ruling on. Once confirmed, the pair + // collapses to the label — there is nothing left to decide, and + // leaving the buttons would invite an accidental un-confirm. + if !face.confirmed: IconButton { + icon: "check"; + clicked => { root.confirm(); } + } + if !face.confirmed: IconButton { + icon: "cross"; + clicked => { root.reject(); } + } + if face.confirmed: Text { + text: face.confidence; + color: Theme.ink-faint; + font-size: Theme.text-sm; + vertical-alignment: center; + } + } + } +} + +export component IdentityScreen inherits Rectangle { + background: Theme.ground; + + in property <[IdentityPerson]> people; + in property <[IdentityFace]> faces; + in property selected-person: -1; + in property selected-name; + // Faces belonging to nobody. Shown as a count rather than hidden, because + // "why is this photograph not under anyone" deserves an answer. + in property unassigned: 0; + // Whether the library's similarity calibration is fitted (FR-CULL-9). + in property calibrated: false; + in property indexing: false; + in property indexing-status; + // No model on disk: face indexing cannot run at all (docs/faces.md §2.2). + in property model-missing: false; + in property picked-count: 0; + + callback person-picked(int); + callback rename(string); + callback confirm-face(int); + callback reject-face(int); + callback toggle-pick(int); + callback confirm-all(); + callback split-picked(); + callback merge-into(int); + callback recluster(); + callback delete-all-face-data(); + callback close(); + + HorizontalLayout { + // ── the people rail ─────────────────────────────────────────────── + Panel { + width: 260px; + + VerticalLayout { + padding: Theme.gap; + spacing: Theme.gap-sm; + + HorizontalLayout { + Text { + text: "People"; + color: Theme.ink; + font-size: Theme.text-lg; + vertical-alignment: center; + } + Rectangle { } + IconButton { + icon: "cross"; + clicked => { root.close(); } + } + } + + if root.unassigned > 0: Text { + text: root.unassigned + " face(s) not yet grouped"; + color: Theme.ink-faint; + font-size: Theme.text-sm; + wrap: word-wrap; + } + + Flickable { + VerticalLayout { + spacing: 2px; + alignment: start; + + for p[i] in root.people: Rectangle { + height: 52px; + border-radius: Theme.radius; + background: p.id == root.selected-person + ? Theme.selected + : (touch.has-hover ? Theme.hover : transparent); + + HorizontalLayout { + padding: 6px; + spacing: Theme.gap-sm; + + Rectangle { + width: 40px; + height: 40px; + border-radius: Theme.radius-sm; + background: Theme.surface-raised; + clip: true; + if p.has-cover: Image { + source: p.cover; + width: 100%; + height: 100%; + image-fit: cover; + } + } + + VerticalLayout { + alignment: center; + spacing: 1px; + Text { + text: p.label; + color: p.unconfirmed ? Theme.ink-dim : Theme.ink; + font-size: Theme.text; + overflow: elide; + } + Text { + text: p.suggested-faces > 0 + ? p.confirmed-faces + " confirmed · " + p.suggested-faces + " suggested" + : p.confirmed-faces + " confirmed"; + color: Theme.ink-faint; + font-size: Theme.text-sm; + overflow: elide; + } + } + } + + touch := TouchArea { + clicked => { root.person-picked(p.id); } + } + } + } + } + + Rectangle { } + + // The NFR-SEC-5 control lives here rather than three levels + // down in settings: this is where the user's face data + // visibly is, and a delete-everything button they cannot find + // is a button that does not really exist. + Button { + text: "Delete all face data"; + clicked => { root.delete-all-face-data(); } + } + } + } + + // ── the faces ───────────────────────────────────────────────────── + VerticalLayout { + padding: Theme.gap; + spacing: Theme.gap-sm; + + // Header: who is selected, and what can be done to them. + HorizontalLayout { + spacing: Theme.gap-sm; + height: 32px; + + // Naming a cluster *is* the primary action of this screen, so + // the name is an editable field on sight rather than something + // reached through a rename command. + if root.selected-person >= 0: Field { + text: root.selected-name; + placeholder: "Name this person"; + width: 240px; + accepted(t) => { root.rename(t); } + } + if root.selected-person < 0: Text { + text: "Select a person"; + color: Theme.ink-dim; + font-size: Theme.text-lg; + vertical-alignment: center; + } + + Rectangle { } + + if root.picked-count > 0: Text { + text: root.picked-count + " selected"; + color: Theme.ink-dim; + font-size: Theme.text-sm; + vertical-alignment: center; + } + // Split is a first-class button sitting next to confirm, not a + // command hidden in a menu. FR-CULL-10: a tool that can only + // merge makes its own errors permanent. + if root.picked-count > 0: Button { + text: "Split off"; + primary: true; + clicked => { root.split-picked(); } + } + if root.selected-person >= 0 && root.picked-count == 0: Button { + text: "Confirm all"; + primary: true; + clicked => { root.confirm-all(); } + } + Button { + text: "Regroup"; + clicked => { root.recluster(); } + } + } + + if root.model-missing: Rectangle { + height: 40px; + border-radius: Theme.radius; + background: Theme.surface-raised; + Text { + text: "No face model installed — indexing is off."; + color: Theme.warn-ink; + font-size: Theme.text-sm; + } + } + + // FR-CULL-9, made visible: where the calibration is not fitted the + // screen says the confidences are unavailable rather than letting + // per-face percentages imply a measurement that was never made. + if !root.calibrated && !root.model-missing: Text { + text: "Similarity is not calibrated for this library yet, so no confidence is shown."; + color: Theme.ink-faint; + font-size: Theme.text-sm; + wrap: word-wrap; + } + + if root.indexing: Text { + text: root.indexing-status; + color: Theme.ink-dim; + font-size: Theme.text-sm; + } + + Flickable { + VerticalLayout { + alignment: start; + HorizontalLayout { + // Slint has no flow layout, so the grid is a wrapping + // row built from the model's own order; the cells are + // fixed-size, which is what makes that tractable. + spacing: Theme.gap-sm; + alignment: start; + + for f[i] in root.faces: FaceCell { + face: f; + confirm => { root.confirm-face(f.id); } + reject => { root.reject-face(f.id); } + toggle-pick => { root.toggle-pick(f.id); } + } + } + } + } + } + } +} diff --git a/ui/dr-ui/ui/library.slint b/ui/dr-ui/ui/library.slint index df3a0b0..0a30b2a 100644 --- a/ui/dr-ui/ui/library.slint +++ b/ui/dr-ui/ui/library.slint @@ -866,6 +866,7 @@ component HeaderActions inherits HorizontalLayout { callback rescan(); callback open-import(); callback open-settings(); + callback open-people(); spacing: Theme.gap; @@ -1016,6 +1017,14 @@ component HeaderActions inherits HorizontalLayout { clicked => { root.open-import(); } } + // A library action rather than a setting, so it sits with the others and + // ahead of Settings, which the comment below keeps last. + Button { + text: "People"; + y: root.centred ? (root.row-height - self.height) / 2 : 0; + clicked => { root.open-people(); } + } + // Last in the row, and unconditional. The buttons before it come and go // with what the grid is showing; settings is always reachable, and a // control that moved as its neighbours appeared would be hunted for each @@ -1165,6 +1174,7 @@ export component LibraryGrid inherits Rectangle { callback open-import(); callback open-settings(); + callback open-people(); // --- selection and drag --- // @@ -1701,6 +1711,7 @@ export component LibraryGrid inherits Rectangle { rescan => { root.rescan(); } open-import => { root.open-import(); } open-settings => { root.open-settings(); } + open-people => { root.open-people(); } } // Compact: one button in place of six. Labelled rather than a @@ -1785,6 +1796,7 @@ export component LibraryGrid inherits Rectangle { rescan => { root.rescan(); } open-import => { root.open-import(); } open-settings => { root.open-settings(); } + open-people => { root.open-people(); } } } }