Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3785ba55d | ||
|
|
6b5ffc6a93 | ||
|
|
cd64166b15 | ||
|
|
fa4ad6e2d6 | ||
|
|
b0b6dd559a | ||
|
|
ffc40c42d2 |
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Keep docs/traceability.md in step with the tags in the tree.
|
||||
#
|
||||
# The gate regenerates the matrix in CI and fails if the result differs from
|
||||
# what is committed. That is the right check — a matrix that disagrees with the
|
||||
# tree is worse than none, because it is read as current — but it fails *after*
|
||||
# a push, on a commit that is otherwise fine, and it has now done so on six
|
||||
# commits in a row because adding a `TRACES:` tag and regenerating the matrix
|
||||
# are two actions and only the first is on anyone's mind.
|
||||
#
|
||||
# So it happens here instead, where the tags are being changed.
|
||||
#
|
||||
# Only when something that can carry a tag is staged: a commit touching
|
||||
# workflows, packaging or the matrix itself pays nothing.
|
||||
set -euo pipefail
|
||||
|
||||
staged="$(git diff --cached --name-only --diff-filter=ACMR)"
|
||||
if ! grep -qE '\.(rs|slint|yaml|md)$' <<< "${staged}"; then
|
||||
exit 0
|
||||
fi
|
||||
# The matrix is generated from the tree, so regenerating it because it was
|
||||
# itself edited would be circular.
|
||||
if [ "$(tr -d '[:space:]' <<< "${staged}")" = "docs/traceability.md" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
repo="$(git rev-parse --show-toplevel)"
|
||||
cd "${repo}"
|
||||
|
||||
# Quiet unless it has something to say. A hook that prints on every commit is
|
||||
# a hook people start passing --no-verify to.
|
||||
if ! cargo run -q -p traceability -- report >/dev/null 2>&1; then
|
||||
echo "pre-commit: could not run the traceability report; leaving the matrix alone" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git diff --quiet -- docs/traceability.md; then
|
||||
git add docs/traceability.md
|
||||
echo "pre-commit: regenerated docs/traceability.md and staged it"
|
||||
fi
|
||||
Generated
+17
-17
@@ -1221,7 +1221,7 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "darkroom-android"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"dr-sync-nextcloud",
|
||||
@@ -1232,7 +1232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darkroom-desktop"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dr-ui",
|
||||
@@ -1404,7 +1404,7 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dr-catalog"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-plat",
|
||||
"dr-types",
|
||||
@@ -1417,7 +1417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-decode"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"env_logger",
|
||||
@@ -1431,7 +1431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-export"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-decode",
|
||||
"dr-gpu",
|
||||
@@ -1449,7 +1449,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-gpu"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"dr-decode",
|
||||
@@ -1465,7 +1465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-ingest"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-plat",
|
||||
"dr-types",
|
||||
@@ -1477,7 +1477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-lens"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"lensfun",
|
||||
"log",
|
||||
@@ -1485,7 +1485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-pipeline"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"log",
|
||||
@@ -1494,7 +1494,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-plat"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"android-native-keyring-store",
|
||||
"dr-types",
|
||||
@@ -1507,7 +1507,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-segment"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
@@ -1520,7 +1520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-sync"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dr-types",
|
||||
@@ -1531,7 +1531,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-sync-nextcloud"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dr-decode",
|
||||
@@ -1552,7 +1552,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-thumbs"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"jpeg-encoder",
|
||||
@@ -1564,7 +1564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-types"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1573,7 +1573,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-ui"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dr-catalog",
|
||||
@@ -6896,7 +6896,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "traceability"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.92"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -134,6 +134,19 @@ impl Granularity {
|
||||
/// about 48 months — and on a linear measure the larger count always looks
|
||||
/// further away, which would bias every choice towards too few bars.
|
||||
pub fn for_span(seconds: i64) -> Self {
|
||||
Self::for_bucket(seconds.max(1) / Self::TARGET_BARS)
|
||||
}
|
||||
|
||||
/// The calendar unit nearest a bucket of `seconds`, for *labelling* one.
|
||||
///
|
||||
/// Split out from [`Self::for_span`] because the axis no longer buckets by
|
||||
/// calendar unit at all — it divides the visible span into a fixed number
|
||||
/// of equal bins (see `LibrarySettings::timeline_bars`). What is still
|
||||
/// wanted is the unit a bin is closest to, so a bin of about a day is
|
||||
/// labelled as a date and one of about a year as a year. Asked directly
|
||||
/// rather than derived from the span, because the bin count is now the
|
||||
/// user's rather than this module's target.
|
||||
pub fn for_bucket(seconds: i64) -> Self {
|
||||
let seconds = seconds.max(1) as f64;
|
||||
// Finest first, so that when two options are equally far from the
|
||||
// target the finer one wins: `min_by` keeps the first minimum it saw,
|
||||
@@ -147,15 +160,14 @@ impl Granularity {
|
||||
.into_iter()
|
||||
.min_by(|a, b| {
|
||||
let cost = |g: Granularity| {
|
||||
let bars = seconds / g.approx_seconds() as f64;
|
||||
// How far off the target, measured multiplicatively: twice as
|
||||
// many and half as many are equally wrong.
|
||||
// How far off, measured multiplicatively: twice as long and
|
||||
// half as long are equally wrong.
|
||||
//
|
||||
// Deliberately not clamped to at least one bar. A span shorter
|
||||
// than a bucket scores *worse* the coarser the bucket, which is
|
||||
// what makes an hour of photographs pick hourly bars instead of
|
||||
// every option tying at "one bar" and the coarsest winning.
|
||||
(bars / Self::TARGET_BARS as f64).ln().abs()
|
||||
// Deliberately not clamped. A bucket shorter than the unit
|
||||
// scores *worse* the coarser the unit, which is what makes an
|
||||
// hour of photographs pick hourly bars instead of every option
|
||||
// tying at "one bucket" and the coarsest winning.
|
||||
(seconds / g.approx_seconds() as f64).ln().abs()
|
||||
};
|
||||
cost(*a)
|
||||
.partial_cmp(&cost(*b))
|
||||
|
||||
@@ -17,7 +17,7 @@ pub use colour::{Chromaticities, Transfer};
|
||||
pub use selector::{ColourLabel, DateSelector, FlagState, Selector, Tier};
|
||||
pub use settings::{
|
||||
CacheSettings, CollisionPolicy, ColourSpace, DevelopSettings, ExportFormat, ExportSettings,
|
||||
ExportTarget, ImportSettings, OutputSharpening, Settings, SizingMode,
|
||||
ExportTarget, ImportSettings, LibrarySettings, OutputSharpening, Settings, SizingMode,
|
||||
};
|
||||
pub use time::{
|
||||
civil_from_unix, civil_from_unix_at, format_date, parse_date, unix_from_civil, Civil,
|
||||
|
||||
@@ -53,6 +53,60 @@ pub struct Settings {
|
||||
pub export: ExportSettings,
|
||||
pub develop: DevelopSettings,
|
||||
pub import: ImportSettings,
|
||||
pub library: LibrarySettings,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Library
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// TRACES: FR-CAT-6
|
||||
/// How the library view is drawn, where the answer depends on the screen
|
||||
/// rather than on the photographs.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct LibrarySettings {
|
||||
/// How many bars the capture-time axis is divided into.
|
||||
///
|
||||
/// **A fixed count, not a bucket size.** The axis used to bucket by
|
||||
/// calendar unit — year, month, day, hour — and take whichever came
|
||||
/// nearest a target count. Between two of those units the count is free to
|
||||
/// wander by a factor of twelve or thirty, so zooming in halved the number
|
||||
/// of bars two steps out of three: the same picture drawn wider, until it
|
||||
/// suddenly jumped back to fine. Dividing the visible span into a fixed
|
||||
/// number of equal bins makes every zoom step show the same amount of
|
||||
/// detail at a finer scale, which is what zooming is for.
|
||||
///
|
||||
/// It also makes the axis linear in *time*. Calendar buckets are only
|
||||
/// emitted where photographs exist, and the widget gives each bar an equal
|
||||
/// slot, so a library with gaps drew a February that was six months wide.
|
||||
/// Equal bins include the empty ones, so a bar's position on the track and
|
||||
/// the date under it are the same quantity — which is what lets the range
|
||||
/// band and the position marker be drawn on the same axis as the bars.
|
||||
///
|
||||
/// Why it is a setting: the right number is a question about the screen.
|
||||
/// 64 bars on a phone held in the hand is finer than a finger can aim at;
|
||||
/// 32 on a desktop monitor wastes most of a tall sidebar.
|
||||
pub timeline_bars: u32,
|
||||
}
|
||||
|
||||
impl LibrarySettings {
|
||||
/// The counts the settings page offers.
|
||||
///
|
||||
/// Two, both powers of two, and far enough apart that the difference is
|
||||
/// visible. A free number would be a control the user has to experiment
|
||||
/// with to understand, for a choice with exactly two sensible answers:
|
||||
/// one bar per finger-width on a phone, twice that where there is room.
|
||||
pub const BAR_CHOICES: [u32; 2] = [32, 64];
|
||||
}
|
||||
|
||||
impl Default for LibrarySettings {
|
||||
fn default() -> Self {
|
||||
// The coarser of the two. A bar has to be wide enough to hit with a
|
||||
// finger before it has to be narrow enough to be precise, and the
|
||||
// smallest screen is the one where getting this wrong hurts most.
|
||||
Self { timeline_bars: 32 }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -709,6 +763,18 @@ impl Settings {
|
||||
pub fn sanitise(&mut self) {
|
||||
self.export.quality = self.export.quality.clamp(1, 100);
|
||||
|
||||
// Snapped to an offered count rather than clamped to a range. The
|
||||
// settings page lights the chip whose value matches, so a
|
||||
// hand-edited 40 would leave every chip dark and the page unable to
|
||||
// say what the axis is currently doing.
|
||||
if !LibrarySettings::BAR_CHOICES.contains(&self.library.timeline_bars) {
|
||||
let wanted = self.library.timeline_bars;
|
||||
self.library.timeline_bars = LibrarySettings::BAR_CHOICES
|
||||
.into_iter()
|
||||
.min_by_key(|n| n.abs_diff(wanted))
|
||||
.unwrap_or(LibrarySettings::default().timeline_bars);
|
||||
}
|
||||
|
||||
// A zero-pixel or zero-percent export produces no image. Nudged to the
|
||||
// smallest thing that does, rather than back to the default: the user
|
||||
// clearly wanted "small", and silently restoring 2048 would ignore
|
||||
@@ -1050,6 +1116,38 @@ mod tests {
|
||||
assert_eq!(s.export.quality, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_bar_count_can_be_chosen_on_the_page() {
|
||||
// The page draws one chip per choice and lights the one that matches,
|
||||
// so a default outside the list would leave every chip dark.
|
||||
assert!(LibrarySettings::BAR_CHOICES.contains(&LibrarySettings::default().timeline_bars));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitise_snaps_a_hand_edited_bar_count_to_an_offered_one() {
|
||||
// The file is hand-editable and the page cannot show a count it does
|
||||
// not offer. Snapped to the nearest rather than reset to the default:
|
||||
// someone who wrote 60 wanted the finer axis, and 32 would ignore that.
|
||||
let mut s = Settings::default();
|
||||
s.library.timeline_bars = 60;
|
||||
s.sanitise();
|
||||
assert_eq!(s.library.timeline_bars, 64);
|
||||
|
||||
s.library.timeline_bars = 0;
|
||||
s.sanitise();
|
||||
assert_eq!(s.library.timeline_bars, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitise_leaves_an_offered_bar_count_alone() {
|
||||
for n in LibrarySettings::BAR_CHOICES {
|
||||
let mut s = Settings::default();
|
||||
s.library.timeline_bars = n;
|
||||
s.sanitise();
|
||||
assert_eq!(s.library.timeline_bars, n);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitise_rescues_a_zero_dimension() {
|
||||
let mut s = Settings::default();
|
||||
|
||||
+31
-31
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 180 |
|
||||
| TRACES tags found | 540 |
|
||||
| TRACES tags found | 555 |
|
||||
| Requirements defined | 177 |
|
||||
| Requirements covered | 89 |
|
||||
| **Coverage** | **50.3%** (89/177) |
|
||||
@@ -34,25 +34,25 @@ _None._
|
||||
| ID | Tagged in |
|
||||
|---|---|
|
||||
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs: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:62`](../core/dr-types/src/settings.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:944`](../ui/dr-ui/src/lib.rs#L944), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:692`](../ui/dr-ui/ui/library.slint#L692), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:990`](../ui/dr-ui/ui/library.slint#L990) |
|
||||
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs: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:944`](../ui/dr-ui/src/lib.rs#L944), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1147`](../ui/dr-ui/ui/library.slint#L1147), [`ui/dr-ui/ui/library.slint:849`](../ui/dr-ui/ui/library.slint#L849), [`ui/dr-ui/ui/library.slint:999`](../ui/dr-ui/ui/library.slint#L999) |
|
||||
| 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:944`](../ui/dr-ui/src/lib.rs#L944), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2081`](../ui/dr-ui/src/library.rs#L2081), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109) |
|
||||
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:365`](../core/dr-catalog/src/schema.rs#L365), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1173`](../ui/dr-ui/src/collections_ui.rs#L1173), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`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:2962`](../ui/dr-ui/src/library.rs#L2962), [`ui/dr-ui/src/library.rs:2994`](../ui/dr-ui/src/library.rs#L2994), [`ui/dr-ui/src/library_ui.rs:128`](../ui/dr-ui/src/library_ui.rs#L128), [`ui/dr-ui/src/library_ui.rs:643`](../ui/dr-ui/src/library_ui.rs#L643), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:365`](../core/dr-catalog/src/schema.rs#L365), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1173`](../ui/dr-ui/src/collections_ui.rs#L1173), [`ui/dr-ui/src/collections_ui.rs:1936`](../ui/dr-ui/src/collections_ui.rs#L1936), [`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:2962`](../ui/dr-ui/src/library.rs#L2962), [`ui/dr-ui/src/library.rs:2994`](../ui/dr-ui/src/library.rs#L2994), [`ui/dr-ui/src/library_ui.rs:128`](../ui/dr-ui/src/library_ui.rs#L128), [`ui/dr-ui/src/library_ui.rs:666`](../ui/dr-ui/src/library_ui.rs#L666), [`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:2557`](../ui/dr-ui/src/library.rs#L2557), [`ui/dr-ui/src/library.rs:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:115`](../ui/dr-ui/src/library_ui.rs#L115), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:4154`](../ui/dr-ui/src/library_ui.rs#L4154), [`ui/dr-ui/ui/app.slint:504`](../ui/dr-ui/ui/app.slint#L504), [`ui/dr-ui/ui/settings.slint:330`](../ui/dr-ui/ui/settings.slint#L330), [`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:2557`](../ui/dr-ui/src/library.rs#L2557), [`ui/dr-ui/src/library.rs:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:115`](../ui/dr-ui/src/library_ui.rs#L115), [`ui/dr-ui/src/library_ui.rs:3409`](../ui/dr-ui/src/library_ui.rs#L3409), [`ui/dr-ui/src/library_ui.rs:4339`](../ui/dr-ui/src/library_ui.rs#L4339), [`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-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) |
|
||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:126`](../core/dr-pipeline/src/sidecar.rs#L126), [`ui/dr-ui/src/library_ui.rs:5533`](../ui/dr-ui/src/library_ui.rs#L5533), [`ui/dr-ui/src/library_ui.rs:5544`](../ui/dr-ui/src/library_ui.rs#L5544), [`ui/dr-ui/src/library_ui.rs:5557`](../ui/dr-ui/src/library_ui.rs#L5557), [`ui/dr-ui/src/library_ui.rs:5572`](../ui/dr-ui/src/library_ui.rs#L5572), [`ui/dr-ui/src/library_ui.rs:5581`](../ui/dr-ui/src/library_ui.rs#L5581), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:685`](../ui/dr-ui/ui/library.slint#L685), [`ui/dr-ui/ui/library.slint:736`](../ui/dr-ui/ui/library.slint#L736) |
|
||||
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`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:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library_ui.rs:4689`](../ui/dr-ui/src/library_ui.rs#L4689), [`ui/dr-ui/src/library_ui.rs:5673`](../ui/dr-ui/src/library_ui.rs#L5673), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`ui/dr-ui/ui/library.slint:1101`](../ui/dr-ui/ui/library.slint#L1101), [`ui/dr-ui/ui/library.slint:736`](../ui/dr-ui/ui/library.slint#L736) |
|
||||
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1546`](../ui/dr-ui/src/collections_ui.rs#L1546), [`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:3224`](../ui/dr-ui/src/library_ui.rs#L3224), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/library.slint:726`](../ui/dr-ui/ui/library.slint#L726) |
|
||||
| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs: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:1273`](../core/dr-pipeline/src/sidecar.rs#L1273), [`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:2600`](../ui/dr-ui/src/develop.rs#L2600), [`ui/dr-ui/src/develop.rs:2628`](../ui/dr-ui/src/develop.rs#L2628), [`ui/dr-ui/src/export.rs:749`](../ui/dr-ui/src/export.rs#L749), [`ui/dr-ui/src/lib.rs:1169`](../ui/dr-ui/src/lib.rs#L1169), [`ui/dr-ui/src/lib.rs:1509`](../ui/dr-ui/src/lib.rs#L1509), [`ui/dr-ui/src/lib.rs:1614`](../ui/dr-ui/src/lib.rs#L1614), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796), [`ui/dr-ui/src/library.rs:1504`](../ui/dr-ui/src/library.rs#L1504), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:378`](../ui/dr-ui/src/library.rs#L378), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:655`](../ui/dr-ui/src/library.rs#L655), [`ui/dr-ui/src/library_ui.rs:4443`](../ui/dr-ui/src/library_ui.rs#L4443), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs: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:1464`](../ui/dr-ui/src/library.rs#L1464), [`ui/dr-ui/src/library.rs:1541`](../ui/dr-ui/src/library.rs#L1541), [`ui/dr-ui/src/library.rs:196`](../ui/dr-ui/src/library.rs#L196), [`ui/dr-ui/src/library.rs:3177`](../ui/dr-ui/src/library.rs#L3177), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:639`](../ui/dr-ui/src/library.rs#L639), [`ui/dr-ui/src/library.rs:655`](../ui/dr-ui/src/library.rs#L655), [`ui/dr-ui/src/library.rs:709`](../ui/dr-ui/src/library.rs#L709), [`ui/dr-ui/src/library_ui.rs:1450`](../ui/dr-ui/src/library_ui.rs#L1450), [`ui/dr-ui/src/library_ui.rs:1476`](../ui/dr-ui/src/library_ui.rs#L1476), [`ui/dr-ui/src/library_ui.rs:1492`](../ui/dr-ui/src/library_ui.rs#L1492), [`ui/dr-ui/src/library_ui.rs:1586`](../ui/dr-ui/src/library_ui.rs#L1586), [`ui/dr-ui/src/library_ui.rs:170`](../ui/dr-ui/src/library_ui.rs#L170), [`ui/dr-ui/src/library_ui.rs:203`](../ui/dr-ui/src/library_ui.rs#L203), [`ui/dr-ui/src/library_ui.rs:2119`](../ui/dr-ui/src/library_ui.rs#L2119), [`ui/dr-ui/src/library_ui.rs:2552`](../ui/dr-ui/src/library_ui.rs#L2552), [`ui/dr-ui/src/library_ui.rs:2734`](../ui/dr-ui/src/library_ui.rs#L2734), [`ui/dr-ui/src/library_ui.rs:3015`](../ui/dr-ui/src/library_ui.rs#L3015), [`ui/dr-ui/src/library_ui.rs:3087`](../ui/dr-ui/src/library_ui.rs#L3087), [`ui/dr-ui/src/library_ui.rs:3275`](../ui/dr-ui/src/library_ui.rs#L3275), [`ui/dr-ui/src/library_ui.rs:3393`](../ui/dr-ui/src/library_ui.rs#L3393), [`ui/dr-ui/src/library_ui.rs:358`](../ui/dr-ui/src/library_ui.rs#L358), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/library_ui.rs:4734`](../ui/dr-ui/src/library_ui.rs#L4734), [`ui/dr-ui/src/library_ui.rs:4849`](../ui/dr-ui/src/library_ui.rs#L4849), [`ui/dr-ui/src/presets.rs:309`](../ui/dr-ui/src/presets.rs#L309), [`ui/dr-ui/src/presets.rs:321`](../ui/dr-ui/src/presets.rs#L321), [`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:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:126`](../core/dr-pipeline/src/sidecar.rs#L126), [`ui/dr-ui/src/collections_ui.rs:1519`](../ui/dr-ui/src/collections_ui.rs#L1519), [`ui/dr-ui/src/collections_ui.rs:1538`](../ui/dr-ui/src/collections_ui.rs#L1538), [`ui/dr-ui/src/library_ui.rs:5902`](../ui/dr-ui/src/library_ui.rs#L5902), [`ui/dr-ui/src/library_ui.rs:5913`](../ui/dr-ui/src/library_ui.rs#L5913), [`ui/dr-ui/src/library_ui.rs:5926`](../ui/dr-ui/src/library_ui.rs#L5926), [`ui/dr-ui/src/library_ui.rs:5941`](../ui/dr-ui/src/library_ui.rs#L5941), [`ui/dr-ui/src/library_ui.rs:5950`](../ui/dr-ui/src/library_ui.rs#L5950), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:585`](../ui/dr-ui/ui/app.slint#L585), [`ui/dr-ui/ui/library.slint:1195`](../ui/dr-ui/ui/library.slint#L1195), [`ui/dr-ui/ui/library.slint:1198`](../ui/dr-ui/ui/library.slint#L1198), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:893`](../ui/dr-ui/ui/library.slint#L893) |
|
||||
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`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:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:3123`](../ui/dr-ui/src/library.rs#L3123), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:4885`](../ui/dr-ui/src/library_ui.rs#L4885), [`ui/dr-ui/src/library_ui.rs:4938`](../ui/dr-ui/src/library_ui.rs#L4938), [`ui/dr-ui/src/library_ui.rs:6042`](../ui/dr-ui/src/library_ui.rs#L6042), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:585`](../ui/dr-ui/ui/app.slint#L585), [`ui/dr-ui/ui/app.slint:814`](../ui/dr-ui/ui/app.slint#L814), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1264`](../ui/dr-ui/ui/library.slint#L1264), [`ui/dr-ui/ui/library.slint:893`](../ui/dr-ui/ui/library.slint#L893), [`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:1634`](../ui/dr-ui/src/collections_ui.rs#L1634), [`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:3267`](../ui/dr-ui/src/library_ui.rs#L3267), [`ui/dr-ui/src/library_ui.rs:3930`](../ui/dr-ui/src/library_ui.rs#L3930), [`ui/dr-ui/ui/app.slint:580`](../ui/dr-ui/ui/app.slint#L580), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2496`](../ui/dr-ui/ui/library.slint#L2496), [`ui/dr-ui/ui/library.slint:864`](../ui/dr-ui/ui/library.slint#L864), [`ui/dr-ui/ui/library.slint:883`](../ui/dr-ui/ui/library.slint#L883) |
|
||||
| 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:1273`](../core/dr-pipeline/src/sidecar.rs#L1273), [`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:2600`](../ui/dr-ui/src/develop.rs#L2600), [`ui/dr-ui/src/develop.rs:2628`](../ui/dr-ui/src/develop.rs#L2628), [`ui/dr-ui/src/export.rs:749`](../ui/dr-ui/src/export.rs#L749), [`ui/dr-ui/src/lib.rs:1181`](../ui/dr-ui/src/lib.rs#L1181), [`ui/dr-ui/src/lib.rs:1521`](../ui/dr-ui/src/lib.rs#L1521), [`ui/dr-ui/src/lib.rs:1626`](../ui/dr-ui/src/lib.rs#L1626), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796), [`ui/dr-ui/src/library.rs:1504`](../ui/dr-ui/src/library.rs#L1504), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:378`](../ui/dr-ui/src/library.rs#L378), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:655`](../ui/dr-ui/src/library.rs#L655), [`ui/dr-ui/src/library_ui.rs:4628`](../ui/dr-ui/src/library_ui.rs#L4628), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs: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:1464`](../ui/dr-ui/src/library.rs#L1464), [`ui/dr-ui/src/library.rs:1541`](../ui/dr-ui/src/library.rs#L1541), [`ui/dr-ui/src/library.rs:196`](../ui/dr-ui/src/library.rs#L196), [`ui/dr-ui/src/library.rs:3230`](../ui/dr-ui/src/library.rs#L3230), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:639`](../ui/dr-ui/src/library.rs#L639), [`ui/dr-ui/src/library.rs:655`](../ui/dr-ui/src/library.rs#L655), [`ui/dr-ui/src/library.rs:709`](../ui/dr-ui/src/library.rs#L709), [`ui/dr-ui/src/library_ui.rs:1473`](../ui/dr-ui/src/library_ui.rs#L1473), [`ui/dr-ui/src/library_ui.rs:1499`](../ui/dr-ui/src/library_ui.rs#L1499), [`ui/dr-ui/src/library_ui.rs:1515`](../ui/dr-ui/src/library_ui.rs#L1515), [`ui/dr-ui/src/library_ui.rs:1609`](../ui/dr-ui/src/library_ui.rs#L1609), [`ui/dr-ui/src/library_ui.rs:170`](../ui/dr-ui/src/library_ui.rs#L170), [`ui/dr-ui/src/library_ui.rs:203`](../ui/dr-ui/src/library_ui.rs#L203), [`ui/dr-ui/src/library_ui.rs:2162`](../ui/dr-ui/src/library_ui.rs#L2162), [`ui/dr-ui/src/library_ui.rs:2595`](../ui/dr-ui/src/library_ui.rs#L2595), [`ui/dr-ui/src/library_ui.rs:2777`](../ui/dr-ui/src/library_ui.rs#L2777), [`ui/dr-ui/src/library_ui.rs:3058`](../ui/dr-ui/src/library_ui.rs#L3058), [`ui/dr-ui/src/library_ui.rs:3130`](../ui/dr-ui/src/library_ui.rs#L3130), [`ui/dr-ui/src/library_ui.rs:3318`](../ui/dr-ui/src/library_ui.rs#L3318), [`ui/dr-ui/src/library_ui.rs:3436`](../ui/dr-ui/src/library_ui.rs#L3436), [`ui/dr-ui/src/library_ui.rs:381`](../ui/dr-ui/src/library_ui.rs#L381), [`ui/dr-ui/src/library_ui.rs:439`](../ui/dr-ui/src/library_ui.rs#L439), [`ui/dr-ui/src/library_ui.rs:4983`](../ui/dr-ui/src/library_ui.rs#L4983), [`ui/dr-ui/src/library_ui.rs:5098`](../ui/dr-ui/src/library_ui.rs#L5098), [`ui/dr-ui/src/presets.rs:309`](../ui/dr-ui/src/presets.rs#L309), [`ui/dr-ui/src/presets.rs:321`](../ui/dr-ui/src/presets.rs#L321), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) |
|
||||
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs: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:126`](../core/dr-pipeline/src/sidecar.rs#L126), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340) |
|
||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:318`](../core/dr-pipeline/src/operation.rs#L318) |
|
||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:395`](../core/dr-gpu/src/adjust.rs#L395), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:120`](../core/dr-pipeline/src/graph.rs#L120), [`core/dr-pipeline/src/graph.rs:397`](../core/dr-pipeline/src/graph.rs#L397), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:439`](../core/dr-pipeline/src/operation.rs#L439), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs: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:1273`](../core/dr-pipeline/src/sidecar.rs#L1273), [`core/dr-pipeline/src/sidecar.rs:1329`](../core/dr-pipeline/src/sidecar.rs#L1329), [`core/dr-pipeline/src/sidecar.rs:147`](../core/dr-pipeline/src/sidecar.rs#L147), [`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:2600`](../ui/dr-ui/src/develop.rs#L2600), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2984`](../ui/dr-ui/src/develop.rs#L2984), [`ui/dr-ui/src/develop.rs:3038`](../ui/dr-ui/src/develop.rs#L3038), [`ui/dr-ui/src/develop.rs:3082`](../ui/dr-ui/src/develop.rs#L3082), [`ui/dr-ui/src/develop.rs:3132`](../ui/dr-ui/src/develop.rs#L3132), [`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:1205`](../ui/dr-ui/src/lib.rs#L1205), [`ui/dr-ui/src/lib.rs:1857`](../ui/dr-ui/src/lib.rs#L1857), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:378`](../ui/dr-ui/src/library.rs#L378), [`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:1880`](../ui/dr-ui/ui/app.slint#L1880), [`ui/dr-ui/ui/app.slint:888`](../ui/dr-ui/ui/app.slint#L888) |
|
||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:395`](../core/dr-gpu/src/adjust.rs#L395), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:120`](../core/dr-pipeline/src/graph.rs#L120), [`core/dr-pipeline/src/graph.rs:397`](../core/dr-pipeline/src/graph.rs#L397), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:439`](../core/dr-pipeline/src/operation.rs#L439), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs: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:1273`](../core/dr-pipeline/src/sidecar.rs#L1273), [`core/dr-pipeline/src/sidecar.rs:1329`](../core/dr-pipeline/src/sidecar.rs#L1329), [`core/dr-pipeline/src/sidecar.rs:147`](../core/dr-pipeline/src/sidecar.rs#L147), [`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:2600`](../ui/dr-ui/src/develop.rs#L2600), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2984`](../ui/dr-ui/src/develop.rs#L2984), [`ui/dr-ui/src/develop.rs:3038`](../ui/dr-ui/src/develop.rs#L3038), [`ui/dr-ui/src/develop.rs:3082`](../ui/dr-ui/src/develop.rs#L3082), [`ui/dr-ui/src/develop.rs:3132`](../ui/dr-ui/src/develop.rs#L3132), [`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:1217`](../ui/dr-ui/src/lib.rs#L1217), [`ui/dr-ui/src/lib.rs:1869`](../ui/dr-ui/src/lib.rs#L1869), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:378`](../ui/dr-ui/src/library.rs#L378), [`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:1903`](../ui/dr-ui/ui/app.slint#L1903), [`ui/dr-ui/ui/app.slint:901`](../ui/dr-ui/ui/app.slint#L901) |
|
||||
| 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:184`](../core/dr-pipeline/src/graph.rs#L184), [`core/dr-pipeline/src/graph.rs:18`](../core/dr-pipeline/src/graph.rs#L18), [`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:294`](../core/dr-pipeline/src/operation.rs#L294), [`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:526`](../ui/dr-ui/src/lib.rs#L526) |
|
||||
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/operation.rs:294`](../core/dr-pipeline/src/operation.rs#L294) |
|
||||
| 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:184`](../core/dr-pipeline/src/graph.rs#L184), [`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:3711`](../ui/dr-ui/src/develop.rs#L3711) |
|
||||
@@ -60,36 +60,36 @@ _None._
|
||||
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs: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:690`](../core/dr-gpu/src/adjust.rs#L690), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1341`](../core/dr-pipeline/src/operation.rs#L1341), [`core/dr-pipeline/src/operation.rs:1366`](../core/dr-pipeline/src/operation.rs#L1366), [`core/dr-pipeline/src/operation.rs:1381`](../core/dr-pipeline/src/operation.rs#L1381), [`core/dr-pipeline/src/operation.rs:1405`](../core/dr-pipeline/src/operation.rs#L1405), [`core/dr-pipeline/src/operation.rs:369`](../core/dr-pipeline/src/operation.rs#L369), [`core/dr-pipeline/src/operation.rs:379`](../core/dr-pipeline/src/operation.rs#L379), [`core/dr-pipeline/src/operation.rs:510`](../core/dr-pipeline/src/operation.rs#L510) |
|
||||
| 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:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2644`](../ui/dr-ui/src/develop.rs#L2644), [`ui/dr-ui/src/develop.rs:2654`](../ui/dr-ui/src/develop.rs#L2654), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1197`](../ui/dr-ui/src/lib.rs#L1197) |
|
||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:128`](../core/dr-types/src/settings.rs#L128), [`ui/dr-ui/src/develop.rs:2594`](../ui/dr-ui/src/develop.rs#L2594), [`ui/dr-ui/src/develop.rs:2615`](../ui/dr-ui/src/develop.rs#L2615), [`ui/dr-ui/src/lib.rs:1169`](../ui/dr-ui/src/lib.rs#L1169), [`ui/dr-ui/src/library.rs:1504`](../ui/dr-ui/src/library.rs#L1504), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:368`](../ui/dr-ui/src/library.rs#L368), [`ui/dr-ui/src/library_ui.rs:2379`](../ui/dr-ui/src/library_ui.rs#L2379), [`ui/dr-ui/src/library_ui.rs:2734`](../ui/dr-ui/src/library_ui.rs#L2734), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1118`](../ui/dr-ui/ui/library.slint#L1118), [`ui/dr-ui/ui/library.slint:666`](../ui/dr-ui/ui/library.slint#L666), [`ui/dr-ui/ui/library.slint:747`](../ui/dr-ui/ui/library.slint#L747), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
|
||||
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2644`](../ui/dr-ui/src/develop.rs#L2644), [`ui/dr-ui/src/develop.rs:2654`](../ui/dr-ui/src/develop.rs#L2654), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1209`](../ui/dr-ui/src/lib.rs#L1209) |
|
||||
| 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:2594`](../ui/dr-ui/src/develop.rs#L2594), [`ui/dr-ui/src/develop.rs:2615`](../ui/dr-ui/src/develop.rs#L2615), [`ui/dr-ui/src/lib.rs:1181`](../ui/dr-ui/src/lib.rs#L1181), [`ui/dr-ui/src/library.rs:1504`](../ui/dr-ui/src/library.rs#L1504), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library.rs:368`](../ui/dr-ui/src/library.rs#L368), [`ui/dr-ui/src/library_ui.rs:2422`](../ui/dr-ui/src/library_ui.rs#L2422), [`ui/dr-ui/src/library_ui.rs:2777`](../ui/dr-ui/src/library_ui.rs#L2777), [`ui/dr-ui/src/library_ui.rs:407`](../ui/dr-ui/src/library_ui.rs#L407), [`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:1291`](../ui/dr-ui/ui/library.slint#L1291), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:904`](../ui/dr-ui/ui/library.slint#L904), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
|
||||
| FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265) |
|
||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:1783`](../core/dr-gpu/src/adjust.rs#L1783), [`core/dr-gpu/src/adjust.rs:1860`](../core/dr-gpu/src/adjust.rs#L1860), [`core/dr-gpu/src/adjust.rs:1945`](../core/dr-gpu/src/adjust.rs#L1945), [`core/dr-gpu/src/adjust.rs:506`](../core/dr-gpu/src/adjust.rs#L506), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs: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:367`](../core/dr-pipeline/src/graph.rs#L367), [`core/dr-pipeline/src/graph.rs:397`](../core/dr-pipeline/src/graph.rs#L397), [`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:2792`](../ui/dr-ui/src/develop.rs#L2792), [`ui/dr-ui/src/develop.rs:3256`](../ui/dr-ui/src/develop.rs#L3256), [`ui/dr-ui/src/develop.rs:3290`](../ui/dr-ui/src/develop.rs#L3290), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:667`](../ui/dr-ui/src/lib.rs#L667) |
|
||||
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
|
||||
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4358`](../ui/dr-ui/src/develop.rs#L4358), [`ui/dr-ui/src/develop.rs:4390`](../ui/dr-ui/src/develop.rs#L4390), [`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:1256`](../ui/dr-ui/src/lib.rs#L1256), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`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:4358`](../ui/dr-ui/src/develop.rs#L4358), [`ui/dr-ui/src/develop.rs:4390`](../ui/dr-ui/src/develop.rs#L4390), [`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:1268`](../ui/dr-ui/src/lib.rs#L1268), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`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-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2093`](../core/dr-gpu/src/adjust.rs#L2093), [`core/dr-pipeline/src/graph.rs:357`](../core/dr-pipeline/src/graph.rs#L357), [`core/dr-pipeline/src/graph.rs:410`](../core/dr-pipeline/src/graph.rs#L410), [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:541`](../core/dr-types/src/settings.rs#L541), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2093`](../core/dr-gpu/src/adjust.rs#L2093), [`core/dr-pipeline/src/graph.rs:357`](../core/dr-pipeline/src/graph.rs#L357), [`core/dr-pipeline/src/graph.rs:410`](../core/dr-pipeline/src/graph.rs#L410), [`core/dr-pipeline/src/operation.rs:412`](../core/dr-pipeline/src/operation.rs#L412), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs: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:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:561`](../ui/dr-ui/src/settings_ui.rs#L561) |
|
||||
| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:941`](../ui/dr-ui/src/export.rs#L941), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175), [`ui/dr-ui/src/lib.rs:1780`](../ui/dr-ui/src/lib.rs#L1780), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library_ui.rs:3104`](../ui/dr-ui/src/library_ui.rs#L3104), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:546`](../ui/dr-ui/src/library_ui.rs#L546), [`ui/dr-ui/src/library_ui.rs:5515`](../ui/dr-ui/src/library_ui.rs#L5515), [`ui/dr-ui/src/library_ui.rs:5592`](../ui/dr-ui/src/library_ui.rs#L5592), [`ui/dr-ui/src/library_ui.rs:5604`](../ui/dr-ui/src/library_ui.rs#L5604), [`ui/dr-ui/src/library_ui.rs:603`](../ui/dr-ui/src/library_ui.rs#L603), [`ui/dr-ui/ui/app.slint:1349`](../ui/dr-ui/ui/app.slint#L1349), [`ui/dr-ui/ui/app.slint:979`](../ui/dr-ui/ui/app.slint#L979), [`ui/dr-ui/ui/library.slint:1126`](../ui/dr-ui/ui/library.slint#L1126), [`ui/dr-ui/ui/library.slint:670`](../ui/dr-ui/ui/library.slint#L670), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) |
|
||||
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1222`](../core/dr-decode/src/locate.rs#L1222), [`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:259`](../core/dr-types/src/settings.rs#L259), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:776`](../ui/dr-ui/src/export.rs#L776), [`ui/dr-ui/src/export.rs:793`](../ui/dr-ui/src/export.rs#L793), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs: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:941`](../ui/dr-ui/src/export.rs#L941), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175), [`ui/dr-ui/src/lib.rs:1792`](../ui/dr-ui/src/lib.rs#L1792), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library_ui.rs:3147`](../ui/dr-ui/src/library_ui.rs#L3147), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:569`](../ui/dr-ui/src/library_ui.rs#L569), [`ui/dr-ui/src/library_ui.rs:5884`](../ui/dr-ui/src/library_ui.rs#L5884), [`ui/dr-ui/src/library_ui.rs:5961`](../ui/dr-ui/src/library_ui.rs#L5961), [`ui/dr-ui/src/library_ui.rs:5973`](../ui/dr-ui/src/library_ui.rs#L5973), [`ui/dr-ui/src/library_ui.rs:626`](../ui/dr-ui/src/library_ui.rs#L626), [`ui/dr-ui/ui/app.slint:1372`](../ui/dr-ui/ui/app.slint#L1372), [`ui/dr-ui/ui/app.slint:992`](../ui/dr-ui/ui/app.slint#L992), [`ui/dr-ui/ui/library.slint:1299`](../ui/dr-ui/ui/library.slint#L1299), [`ui/dr-ui/ui/library.slint:827`](../ui/dr-ui/ui/library.slint#L827), [`ui/dr-ui/ui/library.slint:919`](../ui/dr-ui/ui/library.slint#L919) |
|
||||
| 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:1222`](../core/dr-decode/src/locate.rs#L1222), [`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:776`](../ui/dr-ui/src/export.rs#L776), [`ui/dr-ui/src/export.rs:793`](../ui/dr-ui/src/export.rs#L793), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
| FR-EXP-9 | [`core/dr-decode/src/lib.rs: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:791`](../core/dr-gpu/src/adjust.rs#L791), [`ui/dr-ui/src/develop.rs:2284`](../ui/dr-ui/src/develop.rs#L2284), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328) |
|
||||
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) |
|
||||
| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library.rs:1541`](../ui/dr-ui/src/library.rs#L1541), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:709`](../ui/dr-ui/src/library.rs#L709), [`ui/dr-ui/src/library.rs:906`](../ui/dr-ui/src/library.rs#L906), [`ui/dr-ui/src/library_ui.rs:1492`](../ui/dr-ui/src/library_ui.rs#L1492), [`ui/dr-ui/src/library_ui.rs:3104`](../ui/dr-ui/src/library_ui.rs#L3104), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library.rs:1541`](../ui/dr-ui/src/library.rs#L1541), [`ui/dr-ui/src/library.rs:415`](../ui/dr-ui/src/library.rs#L415), [`ui/dr-ui/src/library.rs:709`](../ui/dr-ui/src/library.rs#L709), [`ui/dr-ui/src/library.rs:906`](../ui/dr-ui/src/library.rs#L906), [`ui/dr-ui/src/library_ui.rs:1515`](../ui/dr-ui/src/library_ui.rs#L1515), [`ui/dr-ui/src/library_ui.rs:3147`](../ui/dr-ui/src/library_ui.rs#L3147), [`ui/dr-ui/src/library_ui.rs:439`](../ui/dr-ui/src/library_ui.rs#L439), [`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:2557`](../ui/dr-ui/src/library.rs#L2557), [`ui/dr-ui/src/library.rs:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:4154`](../ui/dr-ui/src/library_ui.rs#L4154), [`ui/dr-ui/ui/app.slint:504`](../ui/dr-ui/ui/app.slint#L504), [`ui/dr-ui/ui/settings.slint:330`](../ui/dr-ui/ui/settings.slint#L330), [`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:2557`](../ui/dr-ui/src/library.rs#L2557), [`ui/dr-ui/src/library.rs:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3409`](../ui/dr-ui/src/library_ui.rs#L3409), [`ui/dr-ui/src/library_ui.rs:4339`](../ui/dr-ui/src/library_ui.rs#L4339), [`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-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:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2965`](../ui/dr-ui/src/collections_ui.rs#L2965), [`ui/dr-ui/src/collections_ui.rs:576`](../ui/dr-ui/src/collections_ui.rs#L576), [`ui/dr-ui/src/collections_ui.rs:638`](../ui/dr-ui/src/collections_ui.rs#L638), [`ui/dr-ui/src/lib.rs:1540`](../ui/dr-ui/src/lib.rs#L1540), [`ui/dr-ui/src/lib.rs:2280`](../ui/dr-ui/src/lib.rs#L2280), [`ui/dr-ui/src/library.rs:1283`](../ui/dr-ui/src/library.rs#L1283), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1464`](../ui/dr-ui/src/library.rs#L1464), [`ui/dr-ui/src/library_ui.rs:1024`](../ui/dr-ui/src/library_ui.rs#L1024), [`ui/dr-ui/src/library_ui.rs:1125`](../ui/dr-ui/src/library_ui.rs#L1125), [`ui/dr-ui/src/library_ui.rs:1180`](../ui/dr-ui/src/library_ui.rs#L1180), [`ui/dr-ui/src/library_ui.rs:1298`](../ui/dr-ui/src/library_ui.rs#L1298), [`ui/dr-ui/src/library_ui.rs:1417`](../ui/dr-ui/src/library_ui.rs#L1417), [`ui/dr-ui/src/library_ui.rs:1895`](../ui/dr-ui/src/library_ui.rs#L1895), [`ui/dr-ui/src/library_ui.rs:211`](../ui/dr-ui/src/library_ui.rs#L211), [`ui/dr-ui/src/library_ui.rs:222`](../ui/dr-ui/src/library_ui.rs#L222), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:242`](../ui/dr-ui/src/library_ui.rs#L242), [`ui/dr-ui/src/library_ui.rs:251`](../ui/dr-ui/src/library_ui.rs#L251), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:327`](../ui/dr-ui/src/library_ui.rs#L327), [`ui/dr-ui/src/library_ui.rs:369`](../ui/dr-ui/src/library_ui.rs#L369), [`ui/dr-ui/src/library_ui.rs:432`](../ui/dr-ui/src/library_ui.rs#L432), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:4751`](../ui/dr-ui/src/library_ui.rs#L4751), [`ui/dr-ui/src/library_ui.rs:475`](../ui/dr-ui/src/library_ui.rs#L475), [`ui/dr-ui/src/library_ui.rs:4769`](../ui/dr-ui/src/library_ui.rs#L4769), [`ui/dr-ui/src/library_ui.rs:4781`](../ui/dr-ui/src/library_ui.rs#L4781), [`ui/dr-ui/src/library_ui.rs:951`](../ui/dr-ui/src/library_ui.rs#L951), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2378`](../ui/dr-ui/ui/app.slint#L2378), [`ui/dr-ui/ui/app.slint:565`](../ui/dr-ui/ui/app.slint#L565), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:808`](../ui/dr-ui/ui/library.slint#L808) |
|
||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1180`](../ui/dr-ui/src/library_ui.rs#L1180) |
|
||||
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs: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:2965`](../ui/dr-ui/src/collections_ui.rs#L2965), [`ui/dr-ui/src/collections_ui.rs:576`](../ui/dr-ui/src/collections_ui.rs#L576), [`ui/dr-ui/src/collections_ui.rs:638`](../ui/dr-ui/src/collections_ui.rs#L638), [`ui/dr-ui/src/library_ui.rs:1024`](../ui/dr-ui/src/library_ui.rs#L1024), [`ui/dr-ui/src/library_ui.rs:951`](../ui/dr-ui/src/library_ui.rs#L951), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
||||
| FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/ui/settings.slint:330`](../ui/dr-ui/ui/settings.slint#L330) |
|
||||
| FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:62`](../core/dr-types/src/settings.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:944`](../ui/dr-ui/src/lib.rs#L944), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3053`](../ui/dr-ui/src/collections_ui.rs#L3053), [`ui/dr-ui/src/collections_ui.rs:576`](../ui/dr-ui/src/collections_ui.rs#L576), [`ui/dr-ui/src/collections_ui.rs:638`](../ui/dr-ui/src/collections_ui.rs#L638), [`ui/dr-ui/src/lib.rs:1552`](../ui/dr-ui/src/lib.rs#L1552), [`ui/dr-ui/src/lib.rs:2292`](../ui/dr-ui/src/lib.rs#L2292), [`ui/dr-ui/src/library.rs:1283`](../ui/dr-ui/src/library.rs#L1283), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1464`](../ui/dr-ui/src/library.rs#L1464), [`ui/dr-ui/src/library_ui.rs:1047`](../ui/dr-ui/src/library_ui.rs#L1047), [`ui/dr-ui/src/library_ui.rs:1148`](../ui/dr-ui/src/library_ui.rs#L1148), [`ui/dr-ui/src/library_ui.rs:1203`](../ui/dr-ui/src/library_ui.rs#L1203), [`ui/dr-ui/src/library_ui.rs:1321`](../ui/dr-ui/src/library_ui.rs#L1321), [`ui/dr-ui/src/library_ui.rs:1440`](../ui/dr-ui/src/library_ui.rs#L1440), [`ui/dr-ui/src/library_ui.rs:1918`](../ui/dr-ui/src/library_ui.rs#L1918), [`ui/dr-ui/src/library_ui.rs:211`](../ui/dr-ui/src/library_ui.rs#L211), [`ui/dr-ui/src/library_ui.rs:222`](../ui/dr-ui/src/library_ui.rs#L222), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:242`](../ui/dr-ui/src/library_ui.rs#L242), [`ui/dr-ui/src/library_ui.rs:251`](../ui/dr-ui/src/library_ui.rs#L251), [`ui/dr-ui/src/library_ui.rs:340`](../ui/dr-ui/src/library_ui.rs#L340), [`ui/dr-ui/src/library_ui.rs:350`](../ui/dr-ui/src/library_ui.rs#L350), [`ui/dr-ui/src/library_ui.rs:392`](../ui/dr-ui/src/library_ui.rs#L392), [`ui/dr-ui/src/library_ui.rs:455`](../ui/dr-ui/src/library_ui.rs#L455), [`ui/dr-ui/src/library_ui.rs:486`](../ui/dr-ui/src/library_ui.rs#L486), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5000`](../ui/dr-ui/src/library_ui.rs#L5000), [`ui/dr-ui/src/library_ui.rs:5018`](../ui/dr-ui/src/library_ui.rs#L5018), [`ui/dr-ui/src/library_ui.rs:5030`](../ui/dr-ui/src/library_ui.rs#L5030), [`ui/dr-ui/src/library_ui.rs:974`](../ui/dr-ui/src/library_ui.rs#L974), [`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:2401`](../ui/dr-ui/ui/app.slint#L2401), [`ui/dr-ui/ui/app.slint:574`](../ui/dr-ui/ui/app.slint#L574), [`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:965`](../ui/dr-ui/ui/library.slint#L965) |
|
||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1203`](../ui/dr-ui/src/library_ui.rs#L1203) |
|
||||
| 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:3053`](../ui/dr-ui/src/collections_ui.rs#L3053), [`ui/dr-ui/src/collections_ui.rs:576`](../ui/dr-ui/src/collections_ui.rs#L576), [`ui/dr-ui/src/collections_ui.rs:638`](../ui/dr-ui/src/collections_ui.rs#L638), [`ui/dr-ui/src/library_ui.rs:1047`](../ui/dr-ui/src/library_ui.rs#L1047), [`ui/dr-ui/src/library_ui.rs:974`](../ui/dr-ui/src/library_ui.rs#L974), [`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:2583`](../ui/dr-ui/src/library.rs#L2583), [`ui/dr-ui/src/library_ui.rs:3409`](../ui/dr-ui/src/library_ui.rs#L3409), [`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:944`](../ui/dr-ui/src/lib.rs#L944), [`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:944`](../ui/dr-ui/src/lib.rs#L944) |
|
||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1509`](../ui/dr-ui/src/lib.rs#L1509), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library_ui.rs:384`](../ui/dr-ui/src/library_ui.rs#L384) |
|
||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1521`](../ui/dr-ui/src/lib.rs#L1521), [`ui/dr-ui/src/library.rs:340`](../ui/dr-ui/src/library.rs#L340), [`ui/dr-ui/src/library_ui.rs:407`](../ui/dr-ui/src/library_ui.rs#L407) |
|
||||
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:147`](../core/dr-pipeline/src/sidecar.rs#L147), [`core/dr-pipeline/src/sidecar.rs:301`](../core/dr-pipeline/src/sidecar.rs#L301), [`ui/dr-ui/src/library.rs:709`](../ui/dr-ui/src/library.rs#L709), [`ui/dr-ui/src/library.rs:839`](../ui/dr-ui/src/library.rs#L839) |
|
||||
| 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) |
|
||||
@@ -98,14 +98,14 @@ _None._
|
||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1365`](../core/dr-decode/src/locate.rs#L1365) |
|
||||
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175) |
|
||||
| FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) |
|
||||
| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2362`](../ui/dr-ui/src/lib.rs#L2362), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866) |
|
||||
| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:1450`](../ui/dr-ui/src/collections_ui.rs#L1450), [`ui/dr-ui/src/collections_ui.rs:1464`](../ui/dr-ui/src/collections_ui.rs#L1464), [`ui/dr-ui/src/collections_ui.rs:1509`](../ui/dr-ui/src/collections_ui.rs#L1509), [`ui/dr-ui/src/collections_ui.rs:1519`](../ui/dr-ui/src/collections_ui.rs#L1519), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:943`](../ui/dr-ui/src/collections_ui.rs#L943), [`ui/dr-ui/src/collections_ui.rs:953`](../ui/dr-ui/src/collections_ui.rs#L953), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:4781`](../ui/dr-ui/src/library_ui.rs#L4781), [`ui/dr-ui/src/library_ui.rs:5623`](../ui/dr-ui/src/library_ui.rs#L5623), [`ui/dr-ui/ui/app.slint:2107`](../ui/dr-ui/ui/app.slint#L2107), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:595`](../ui/dr-ui/ui/app.slint#L595), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/library.slint:1015`](../ui/dr-ui/ui/library.slint#L1015), [`ui/dr-ui/ui/library.slint:1022`](../ui/dr-ui/ui/library.slint#L1022), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:2003`](../ui/dr-ui/ui/library.slint#L2003), [`ui/dr-ui/ui/library.slint:658`](../ui/dr-ui/ui/library.slint#L658), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/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:3949`](../ui/dr-ui/src/library_ui.rs#L3949), [`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:1880`](../ui/dr-ui/ui/app.slint#L1880), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) |
|
||||
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:124`](../ui/dr-ui/src/collections_ui.rs#L124), [`ui/dr-ui/src/collections_ui.rs:1450`](../ui/dr-ui/src/collections_ui.rs#L1450), [`ui/dr-ui/src/collections_ui.rs:1464`](../ui/dr-ui/src/collections_ui.rs#L1464), [`ui/dr-ui/src/collections_ui.rs:1509`](../ui/dr-ui/src/collections_ui.rs#L1509), [`ui/dr-ui/src/collections_ui.rs:1519`](../ui/dr-ui/src/collections_ui.rs#L1519), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1546`](../ui/dr-ui/src/collections_ui.rs#L1546), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:537`](../ui/dr-ui/src/collections_ui.rs#L537), [`ui/dr-ui/src/collections_ui.rs:943`](../ui/dr-ui/src/collections_ui.rs#L943), [`ui/dr-ui/src/collections_ui.rs:953`](../ui/dr-ui/src/collections_ui.rs#L953), [`ui/dr-ui/src/library_ui.rs:3949`](../ui/dr-ui/src/library_ui.rs#L3949), [`ui/dr-ui/src/library_ui.rs:3994`](../ui/dr-ui/src/library_ui.rs#L3994), [`ui/dr-ui/src/library_ui.rs:4097`](../ui/dr-ui/src/library_ui.rs#L4097), [`ui/dr-ui/src/library_ui.rs:4125`](../ui/dr-ui/src/library_ui.rs#L4125), [`ui/dr-ui/src/library_ui.rs:4769`](../ui/dr-ui/src/library_ui.rs#L4769), [`ui/dr-ui/src/library_ui.rs:4781`](../ui/dr-ui/src/library_ui.rs#L4781), [`ui/dr-ui/ui/app.slint:1576`](../ui/dr-ui/ui/app.slint#L1576), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/library.slint:1015`](../ui/dr-ui/ui/library.slint#L1015), [`ui/dr-ui/ui/library.slint:1022`](../ui/dr-ui/ui/library.slint#L1022), [`ui/dr-ui/ui/library.slint:1028`](../ui/dr-ui/ui/library.slint#L1028), [`ui/dr-ui/ui/library.slint:2003`](../ui/dr-ui/ui/library.slint#L2003), [`ui/dr-ui/ui/library.slint:658`](../ui/dr-ui/ui/library.slint#L658), [`ui/dr-ui/ui/library.slint:707`](../ui/dr-ui/ui/library.slint#L707), [`ui/dr-ui/ui/library.slint:726`](../ui/dr-ui/ui/library.slint#L726) |
|
||||
| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2007`](../ui/dr-ui/src/lib.rs#L2007), [`ui/dr-ui/src/lib.rs:2396`](../ui/dr-ui/src/lib.rs#L2396), [`ui/dr-ui/src/lib.rs:2581`](../ui/dr-ui/src/lib.rs#L2581), [`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:2374`](../ui/dr-ui/src/lib.rs#L2374), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1023`](../ui/dr-ui/ui/library.slint#L1023) |
|
||||
| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:1450`](../ui/dr-ui/src/collections_ui.rs#L1450), [`ui/dr-ui/src/collections_ui.rs:1464`](../ui/dr-ui/src/collections_ui.rs#L1464), [`ui/dr-ui/src/collections_ui.rs:1509`](../ui/dr-ui/src/collections_ui.rs#L1509), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1607`](../ui/dr-ui/src/collections_ui.rs#L1607), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:943`](../ui/dr-ui/src/collections_ui.rs#L943), [`ui/dr-ui/src/collections_ui.rs:953`](../ui/dr-ui/src/collections_ui.rs#L953), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:230`](../ui/dr-ui/src/library_ui.rs#L230), [`ui/dr-ui/src/library_ui.rs:4885`](../ui/dr-ui/src/library_ui.rs#L4885), [`ui/dr-ui/src/library_ui.rs:5030`](../ui/dr-ui/src/library_ui.rs#L5030), [`ui/dr-ui/src/library_ui.rs:5992`](../ui/dr-ui/src/library_ui.rs#L5992), [`ui/dr-ui/ui/app.slint:2130`](../ui/dr-ui/ui/app.slint#L2130), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:604`](../ui/dr-ui/ui/app.slint#L604), [`ui/dr-ui/ui/app.slint:611`](../ui/dr-ui/ui/app.slint#L611), [`ui/dr-ui/ui/library.slint:1172`](../ui/dr-ui/ui/library.slint#L1172), [`ui/dr-ui/ui/library.slint:1179`](../ui/dr-ui/ui/library.slint#L1179), [`ui/dr-ui/ui/library.slint:1185`](../ui/dr-ui/ui/library.slint#L1185), [`ui/dr-ui/ui/library.slint:2246`](../ui/dr-ui/ui/library.slint#L2246), [`ui/dr-ui/ui/library.slint:815`](../ui/dr-ui/ui/library.slint#L815), [`ui/dr-ui/ui/library.slint:864`](../ui/dr-ui/ui/library.slint#L864), [`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:4134`](../ui/dr-ui/src/library_ui.rs#L4134), [`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:1903`](../ui/dr-ui/ui/app.slint#L1903), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) |
|
||||
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:124`](../ui/dr-ui/src/collections_ui.rs#L124), [`ui/dr-ui/src/collections_ui.rs:1450`](../ui/dr-ui/src/collections_ui.rs#L1450), [`ui/dr-ui/src/collections_ui.rs:1464`](../ui/dr-ui/src/collections_ui.rs#L1464), [`ui/dr-ui/src/collections_ui.rs:1509`](../ui/dr-ui/src/collections_ui.rs#L1509), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1607`](../ui/dr-ui/src/collections_ui.rs#L1607), [`ui/dr-ui/src/collections_ui.rs:1634`](../ui/dr-ui/src/collections_ui.rs#L1634), [`ui/dr-ui/src/collections_ui.rs:444`](../ui/dr-ui/src/collections_ui.rs#L444), [`ui/dr-ui/src/collections_ui.rs:472`](../ui/dr-ui/src/collections_ui.rs#L472), [`ui/dr-ui/src/collections_ui.rs:537`](../ui/dr-ui/src/collections_ui.rs#L537), [`ui/dr-ui/src/collections_ui.rs:943`](../ui/dr-ui/src/collections_ui.rs#L943), [`ui/dr-ui/src/collections_ui.rs:953`](../ui/dr-ui/src/collections_ui.rs#L953), [`ui/dr-ui/src/library_ui.rs:4134`](../ui/dr-ui/src/library_ui.rs#L4134), [`ui/dr-ui/src/library_ui.rs:4179`](../ui/dr-ui/src/library_ui.rs#L4179), [`ui/dr-ui/src/library_ui.rs:4282`](../ui/dr-ui/src/library_ui.rs#L4282), [`ui/dr-ui/src/library_ui.rs:4310`](../ui/dr-ui/src/library_ui.rs#L4310), [`ui/dr-ui/src/library_ui.rs:5018`](../ui/dr-ui/src/library_ui.rs#L5018), [`ui/dr-ui/src/library_ui.rs:5030`](../ui/dr-ui/src/library_ui.rs#L5030), [`ui/dr-ui/ui/app.slint:1599`](../ui/dr-ui/ui/app.slint#L1599), [`ui/dr-ui/ui/app.slint:580`](../ui/dr-ui/ui/app.slint#L580), [`ui/dr-ui/ui/app.slint:611`](../ui/dr-ui/ui/app.slint#L611), [`ui/dr-ui/ui/library.slint:1172`](../ui/dr-ui/ui/library.slint#L1172), [`ui/dr-ui/ui/library.slint:1179`](../ui/dr-ui/ui/library.slint#L1179), [`ui/dr-ui/ui/library.slint:1185`](../ui/dr-ui/ui/library.slint#L1185), [`ui/dr-ui/ui/library.slint:2246`](../ui/dr-ui/ui/library.slint#L2246), [`ui/dr-ui/ui/library.slint:815`](../ui/dr-ui/ui/library.slint#L815), [`ui/dr-ui/ui/library.slint:864`](../ui/dr-ui/ui/library.slint#L864), [`ui/dr-ui/ui/library.slint:883`](../ui/dr-ui/ui/library.slint#L883) |
|
||||
| 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:2019`](../ui/dr-ui/src/lib.rs#L2019), [`ui/dr-ui/src/lib.rs:2408`](../ui/dr-ui/src/lib.rs#L2408), [`ui/dr-ui/src/lib.rs:2593`](../ui/dr-ui/src/lib.rs#L2593), [`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-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) |
|
||||
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
||||
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1552`](../ui/dr-ui/src/export.rs#L1552), [`ui/dr-ui/src/export.rs:1578`](../ui/dr-ui/src/export.rs#L1578), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1814`](../ui/dr-ui/src/lib.rs#L1814), [`ui/dr-ui/ui/app.slint:990`](../ui/dr-ui/ui/app.slint#L990), [`ui/dr-ui/ui/library.slint:762`](../ui/dr-ui/ui/library.slint#L762) |
|
||||
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1552`](../ui/dr-ui/src/export.rs#L1552), [`ui/dr-ui/src/export.rs:1578`](../ui/dr-ui/src/export.rs#L1578), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1826`](../ui/dr-ui/src/lib.rs#L1826), [`ui/dr-ui/ui/app.slint:1003`](../ui/dr-ui/ui/app.slint#L1003), [`ui/dr-ui/ui/library.slint:919`](../ui/dr-ui/ui/library.slint#L919) |
|
||||
| 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) |
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# makes `makepkg -si` in this directory install what you are actually working
|
||||
# on. Swap `source` for a tagged tarball when there is something to release.
|
||||
pkgname=darkroom
|
||||
pkgver=0.5.0
|
||||
pkgver=0.6.0
|
||||
pkgrel=1
|
||||
pkgdesc="Non-destructive RAW photo library and editor"
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -1516,6 +1516,94 @@ pub fn wire<S, R, C>(
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5
|
||||
// Put the selection down without leaving select mode.
|
||||
//
|
||||
// Distinct from "Done", which does both: after picking forty photographs
|
||||
// and filing them, the next forty start with the mode already on, and
|
||||
// making the user re-enter it is asking them to undo a step they did not
|
||||
// take. It is also the only way back from a mis-tap that is not tapping
|
||||
// every one of them again.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_clear_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5
|
||||
// A collection holding exactly what is selected.
|
||||
//
|
||||
// This was four steps — make a collection, find it in the tree, select the
|
||||
// photographs again because creating one changed the scope, then add them
|
||||
// — and the selection is usually made *because* it is going somewhere. One
|
||||
// press instead.
|
||||
//
|
||||
// Created at the top level rather than inside the current scope, unlike
|
||||
// the tree's "+". A selection can be gathered from anywhere, including
|
||||
// across collections, so filing it under whichever one happens to be open
|
||||
// would put it somewhere its contents did not come from.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_library_collection_from_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let images = ctl.selected();
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
w.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name(cat.connection(), None);
|
||||
let made = coll::create(cat.connection(), &name, None, CollectionKind::Manual)
|
||||
.and_then(|id| coll::add_images(cat.connection(), id, &images).map(|n| (id, n)));
|
||||
|
||||
match made {
|
||||
Ok((id, added)) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{added} photograph{} in {name}",
|
||||
if added == 1 { "" } else { "s" }
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
drop(borrow);
|
||||
|
||||
// The selection has been filed; holding on to it invites
|
||||
// the next press acting on photographs the user considers
|
||||
// dealt with.
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
|
||||
// Into the name field, for the same reason `collection_new`
|
||||
// does it: "New collection" is a placeholder nobody wants
|
||||
// to keep, and making them find the rename afterwards is
|
||||
// asking them to finish a job we started.
|
||||
w.set_collection_renaming(id.0 as i32);
|
||||
reload();
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.set_collection_error(format!("making a collection: {e}").into());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4
|
||||
// The button half of selection mode. The long press is faster and this is
|
||||
// the one that can be found — a gesture with no visible counterpart is a
|
||||
|
||||
@@ -1015,6 +1015,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let stored = settings.snapshot();
|
||||
library.set_cache_budget(stored.cache.original_budget_bytes);
|
||||
library.set_keep_opened_originals(stored.cache.keep_opened_originals);
|
||||
library.set_timeline_bars(stored.library.timeline_bars);
|
||||
}
|
||||
|
||||
// --- the export folder picker ----------------------------------------
|
||||
@@ -1151,6 +1152,17 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
refresh_export_label(&w, &ctl);
|
||||
}
|
||||
|
||||
// The axis is cut into bars when the window is loaded, so a new
|
||||
// count only shows once something reloads it. Done here, and only
|
||||
// when the figure actually moved: a setting that appears to do
|
||||
// nothing until the user scrolls is one they will change twice and
|
||||
// then leave wrong.
|
||||
if lib.set_timeline_bars(s.library.timeline_bars) {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
library_ui::reload(&w, &lib);
|
||||
}
|
||||
}
|
||||
|
||||
// Lowering the ceiling evicts, so the figure beside it has just
|
||||
// changed — leaving the old one would show the cache still over a
|
||||
// limit that was enforced a moment ago.
|
||||
|
||||
+174
-31
@@ -3120,44 +3120,97 @@ pub fn span_scoped(
|
||||
.and_then(|(lo, hi)| Some((lo?, hi?)))
|
||||
}
|
||||
|
||||
/// Histogram buckets within `scope` and a date range.
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The capture-time histogram as a fixed number of equal bins across
|
||||
/// `from..=to`, empty ones included.
|
||||
///
|
||||
/// `Catalog::timeline_range` counts the whole library; a collection needs the
|
||||
/// membership join, and the rating filter applied too, or the bars disagree
|
||||
/// with the grid beneath them.
|
||||
pub fn timeline_scoped(
|
||||
/// # Why not calendar buckets
|
||||
///
|
||||
/// [`timeline_scoped`] groups by year, month, day or hour, which has two
|
||||
/// consequences the axis cannot live with.
|
||||
///
|
||||
/// It emits **only the buckets that hold photographs**, and the widget gives
|
||||
/// every bar an equal slot — so a library with a gap in it drew a February
|
||||
/// that was six months wide. The position marker, the range band and a click
|
||||
/// are all linear in time, so on a sparse library they pointed at bars that
|
||||
/// were somewhere else. Equal bins including the empty ones make a bar's
|
||||
/// position on the track and the date under it the same quantity.
|
||||
///
|
||||
/// And the **count is free to jump by a factor of twelve** between one unit
|
||||
/// and the next, so each zoom step halved the number of bars until a
|
||||
/// threshold was crossed: zooming in made the picture coarser, twice out of
|
||||
/// every three steps. A fixed count re-bins on every zoom instead, which is
|
||||
/// what makes each step show finer structure rather than the same structure
|
||||
/// drawn wider.
|
||||
///
|
||||
/// The date range is lifted from the filter, like the bars' other terms are
|
||||
/// not: this histogram is *how a range is chosen*, and drawing through the
|
||||
/// range would empty every bin outside it and leave nothing to widen into.
|
||||
///
|
||||
/// `bins` is clamped to at least one — a zero would be a division by zero in
|
||||
/// SQL, and the caller's number comes from a hand-editable settings file.
|
||||
pub fn timeline_uniform(
|
||||
catalog: &Catalog,
|
||||
scope: Option<dr_types::CollectionId>,
|
||||
filter: &RatingFilter,
|
||||
granularity: dr_catalog::Granularity,
|
||||
from: i64,
|
||||
to: i64,
|
||||
bins: u32,
|
||||
) -> Result<Vec<dr_catalog::TimeBucket>, dr_catalog::CatalogError> {
|
||||
let (clause, mut params) = scope_clause(catalog, scope)?;
|
||||
let bins = bins.max(1) as i64;
|
||||
// At least one second, or every photograph lands in bin zero.
|
||||
let span = (to - from).max(1);
|
||||
let (clause, params) = scope_clause(catalog, scope)?;
|
||||
let rated = filter.without_date_range().sql();
|
||||
|
||||
// The bin index is arithmetic on the stored UTC instant, not `strftime` on
|
||||
// a local one. A bin is not a calendar unit — it has no local midnight to
|
||||
// respect — and the axis it is drawn on is labelled from the same UTC
|
||||
// instants, so bucketing the two differently is the one way they could
|
||||
// disagree about which bar a photograph belongs to.
|
||||
//
|
||||
// `min` caps the last edge: an image captured at exactly `to` divides to
|
||||
// `bins`, which would be a bin past the end of the axis.
|
||||
//
|
||||
// Integers this code owns, formatted straight in — the same rule the
|
||||
// rating terms follow. They cannot be bound parameters here without
|
||||
// ordering them against the scope clause's, which appears later in the
|
||||
// text but is bound first.
|
||||
let sql = format!(
|
||||
"SELECT min(i.captured_at) AS start, count(*) AS n FROM images i
|
||||
"SELECT min({bins} - 1, (i.captured_at - {from}) * {bins} / {span}) AS b,
|
||||
count(*) AS n
|
||||
FROM images i
|
||||
WHERE {VISIBLE}{rated} AND i.captured_at IS NOT NULL{clause}
|
||||
AND i.captured_at >= ? AND i.captured_at <= ?
|
||||
GROUP BY strftime('{}', i.captured_at
|
||||
+ coalesce(i.captured_offset, 0) * 60, 'unixepoch')
|
||||
ORDER BY start ASC",
|
||||
granularity.strftime()
|
||||
AND i.captured_at >= {from} AND i.captured_at <= {to}
|
||||
GROUP BY b
|
||||
ORDER BY b ASC"
|
||||
);
|
||||
params.push(rusqlite::types::Value::Integer(from));
|
||||
params.push(rusqlite::types::Value::Integer(to));
|
||||
|
||||
let conn = catalog.connection();
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt
|
||||
let counted = stmt
|
||||
.query_map(rusqlite::params_from_iter(params.iter()), |r| {
|
||||
Ok(dr_catalog::TimeBucket {
|
||||
start: r.get(0)?,
|
||||
count: r.get::<_, i64>(1)? as u32,
|
||||
})
|
||||
Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)? as u32))
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(rows)
|
||||
|
||||
// Every bin, in order, whether or not the query returned one for it. The
|
||||
// start is the bin's own left edge rather than the earliest photograph in
|
||||
// it: an empty bin has no photograph to take one from, and a bar drawn at
|
||||
// its contents' position rather than its bin's would put the axis back
|
||||
// where the calendar buckets left it.
|
||||
let mut bars: Vec<dr_catalog::TimeBucket> = (0..bins)
|
||||
.map(|i| dr_catalog::TimeBucket {
|
||||
start: from + (i * span) / bins,
|
||||
count: 0,
|
||||
})
|
||||
.collect();
|
||||
for (i, n) in counted {
|
||||
if let Some(bar) = bars.get_mut(i.clamp(0, bins - 1) as usize) {
|
||||
bar.count = n;
|
||||
}
|
||||
}
|
||||
Ok(bars)
|
||||
}
|
||||
|
||||
/// Total images in the catalog, honouring the rating filter.
|
||||
@@ -4165,16 +4218,7 @@ mod tests {
|
||||
fn the_histogram_ignores_the_range_it_is_used_to_choose() {
|
||||
// Drawing the axis through the chosen range would collapse it onto the
|
||||
// selection, leaving nowhere to widen back out from.
|
||||
let catalog = scanned(3);
|
||||
let conn = catalog.connection();
|
||||
for (n, at) in [(1, 1_000), (2, 5_000), (3, 9_000)] {
|
||||
conn.execute(
|
||||
"UPDATE images SET captured_at = ?2 WHERE source_ref LIKE ?1",
|
||||
rusqlite::params![format!("%IMG_000{n}%"), at],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let catalog = dated_three();
|
||||
let ranged = RatingFilter {
|
||||
captured_from: Some(4_000),
|
||||
captured_to: Some(6_000),
|
||||
@@ -4187,6 +4231,105 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Three photographs at 1000, 5000 and 9000 seconds.
|
||||
fn dated_three() -> Catalog {
|
||||
let catalog = scanned(3);
|
||||
let conn = catalog.connection();
|
||||
for (n, at) in [(1, 1_000), (2, 5_000), (3, 9_000)] {
|
||||
conn.execute(
|
||||
"UPDATE images SET captured_at = ?2 WHERE source_ref LIKE ?1",
|
||||
rusqlite::params![format!("%IMG_000{n}%"), at],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
catalog
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_histogram_has_the_number_of_bins_it_was_asked_for() {
|
||||
// Fixed, whatever the span holds. The axis draws one bar per bin and
|
||||
// positions it by index, so a query that returned only the occupied
|
||||
// ones would put the bars at the wrong dates.
|
||||
let catalog = dated_three();
|
||||
let filter = RatingFilter::default();
|
||||
for bins in [1_u32, 8, 32, 64] {
|
||||
let bars = timeline_uniform(&catalog, None, &filter, 1_000, 9_000, bins).unwrap();
|
||||
assert_eq!(bars.len() as u32, bins);
|
||||
assert_eq!(
|
||||
bars.iter().map(|b| b.count).sum::<u32>(),
|
||||
3,
|
||||
"every photograph is counted exactly once"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bin_starts_where_the_axis_says_it_does() {
|
||||
// The bar's start is its bin's left edge, not the earliest photograph
|
||||
// in it. It is what the position marker and the range band are drawn
|
||||
// against, and an empty bin has no photograph to borrow a date from.
|
||||
let catalog = dated_three();
|
||||
let bars = timeline_uniform(&catalog, None, &RatingFilter::default(), 0, 8_000, 8).unwrap();
|
||||
for (i, bar) in bars.iter().enumerate() {
|
||||
assert_eq!(bar.start, i as i64 * 1_000);
|
||||
}
|
||||
// 1000 and 5000 land in their own bins, 9000 is past the end.
|
||||
assert_eq!(bars[1].count, 1);
|
||||
assert_eq!(bars[5].count, 1);
|
||||
assert_eq!(bars.iter().map(|b| b.count).sum::<u32>(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_bin_holds_a_photograph_taken_at_the_very_end() {
|
||||
// The division puts an image captured at exactly `to` one bin past the
|
||||
// axis. Uncapped it would be dropped from the histogram — and it is
|
||||
// precisely the image that defines the extent, so it would go missing
|
||||
// on every unzoomed library.
|
||||
let catalog = dated_three();
|
||||
let bars =
|
||||
timeline_uniform(&catalog, None, &RatingFilter::default(), 1_000, 9_000, 4).unwrap();
|
||||
assert_eq!(bars.len(), 4);
|
||||
assert_eq!(bars[3].count, 1, "the image at 9000 is in the last bin");
|
||||
assert_eq!(bars[0].count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bins_ignore_the_range_they_are_used_to_choose() {
|
||||
// Same rule as the extent: the bars outside the band are what the
|
||||
// range is widened back into, so counting through the range would
|
||||
// leave every one of them empty.
|
||||
let catalog = dated_three();
|
||||
let ranged = RatingFilter {
|
||||
captured_from: Some(4_000),
|
||||
captured_to: Some(6_000),
|
||||
..Default::default()
|
||||
};
|
||||
let bars = timeline_uniform(&catalog, None, &ranged, 1_000, 9_000, 4).unwrap();
|
||||
assert_eq!(
|
||||
bars.iter().map(|b| b.count).sum::<u32>(),
|
||||
3,
|
||||
"all three, not just the one inside the range"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bins_still_honour_every_other_filter() {
|
||||
// A histogram of the five-star frames is a fair question, and the bars
|
||||
// have to agree with the grid beneath them.
|
||||
let catalog = dated_three();
|
||||
let strict = RatingFilter {
|
||||
min_rating: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let bars = timeline_uniform(&catalog, None, &strict, 1_000, 9_000, 4).unwrap();
|
||||
assert_eq!(bars.len(), 4, "the axis keeps its shape");
|
||||
assert_eq!(
|
||||
bars.iter().map(|b| b.count).sum::<u32>(),
|
||||
0,
|
||||
"nothing here is rated"
|
||||
);
|
||||
}
|
||||
|
||||
fn entry(path: &str, file_id: u64, size: u64) -> RemoteEntry {
|
||||
RemoteEntry {
|
||||
id: RemoteId::Stable(file_id),
|
||||
|
||||
+427
-58
@@ -256,6 +256,16 @@ pub struct LibraryController {
|
||||
/// small budget still keeps a working set. A metered or small-disk device
|
||||
/// wants the first.
|
||||
keep_opened: std::cell::Cell<bool>,
|
||||
/// TRACES: FR-CAT-6
|
||||
/// How many bars the capture-time axis is cut into, from the settings
|
||||
/// page.
|
||||
///
|
||||
/// Held here beside the cache budget and for the same reason: the axis is
|
||||
/// redrawn from a dozen callbacks that have no config access, and reading
|
||||
/// a JSON file on every scroll to answer "how many bars" would be the
|
||||
/// wrong shape of question. A `Cell` because the page can change it while
|
||||
/// a library is open.
|
||||
timeline_bars: std::cell::Cell<u32>,
|
||||
/// Where every worker this module starts reports what it is doing.
|
||||
///
|
||||
/// Held on the controller rather than passed to each function because the
|
||||
@@ -311,9 +321,22 @@ impl LibraryController {
|
||||
keep_opened: std::cell::Cell::new(
|
||||
dr_types::CacheSettings::default().keep_opened_originals,
|
||||
),
|
||||
timeline_bars: std::cell::Cell::new(dr_types::LibrarySettings::default().timeline_bars),
|
||||
})
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-6
|
||||
/// How many bars the capture-time axis is cut into.
|
||||
///
|
||||
/// Answers whether it changed, so the caller can leave the grid alone when
|
||||
/// it did not: this is set on every settings edit, and reloading the
|
||||
/// window because a user changed their export folder would be a visible
|
||||
/// stutter for nothing.
|
||||
pub fn set_timeline_bars(&self, bars: u32) -> bool {
|
||||
let bars = bars.max(1);
|
||||
self.timeline_bars.replace(bars) != bars
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether a develop open should keep the original it downloads.
|
||||
///
|
||||
@@ -1910,7 +1933,12 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
} else {
|
||||
library::total_images_scoped(catalog, scope, &filter).unwrap_or(0)
|
||||
};
|
||||
// Read before it is overwritten: the property still holds what the library
|
||||
// was last time this ran, and a library that has become shorter is the
|
||||
// signal that something was deleted out from under the view.
|
||||
let was = window.get_library_total().max(0) as usize;
|
||||
window.set_library_total(total as i32);
|
||||
let shrank = total < was;
|
||||
|
||||
// Clamp so a scrub to the very end still fills the window rather than
|
||||
// showing a handful of cells.
|
||||
@@ -2064,6 +2092,21 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
// they are refreshed here rather than per cell.
|
||||
refresh_rating_counts(window, catalog);
|
||||
|
||||
// The library got shorter while the view was looking at it — a delete.
|
||||
//
|
||||
// Two things have already moved by this point and neither touches the
|
||||
// viewport: the scrollable height shrank, and `offset` was re-clamped so
|
||||
// the loaded window still fills. So the view is left pointing either past
|
||||
// the end of the content (a blank grid) or at a different part of the
|
||||
// library (an apparent jump to nowhere). Re-anchoring here puts it back on
|
||||
// the photographs the user was actually looking at.
|
||||
//
|
||||
// Only on a shrink. Doing it on every load would fight a scrub, which sets
|
||||
// exactly this property to go somewhere the user asked for.
|
||||
if shrank {
|
||||
restore_position(window, ctl);
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -3531,6 +3574,12 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
|
||||
// fortnight in Arosa landed in one column of a fifteen-year axis.
|
||||
let scope = *ctl.scope.borrow();
|
||||
let filter = *ctl.filter.borrow();
|
||||
// Passed whole, date range included, and both queries below lift that
|
||||
// range themselves — see `span_scoped` and `timeline_uniform`. Every other
|
||||
// term still applies, because a histogram of the five-star frames is a
|
||||
// fair question; the range does not, because the axis is *how a range is
|
||||
// chosen* and drawing through it would empty every bin outside the band
|
||||
// and leave nothing to widen back into.
|
||||
let span = match library::span_scoped(catalog, scope, &filter) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -3542,27 +3591,24 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
|
||||
}
|
||||
};
|
||||
|
||||
// A chosen date range is what the axis draws, when there is one.
|
||||
// A range is drawn *over* this span as a band, not substituted for it —
|
||||
// see the band below. The scale a narrow range deserves is reached by
|
||||
// zooming the axis instead, which is a gesture a finger has (pinch) and
|
||||
// does not disturb the range while it is being adjusted.
|
||||
//
|
||||
// It used to keep drawing the full extent while a range was on, because
|
||||
// the axis was the only way back out: narrow it to the range and there
|
||||
// would be nothing left to widen from. The range now has typed ends, and
|
||||
// those are the way back out — so the axis is free to show the span the
|
||||
// user actually asked about, at the granularity that span deserves. A
|
||||
// fortnight drawn across fifteen years is one column; drawn across itself
|
||||
// it is fourteen days.
|
||||
let span = match (filter.captured_from, filter.captured_to) {
|
||||
(Some(from), Some(to)) if to > from => (from, to),
|
||||
_ => span,
|
||||
};
|
||||
|
||||
// Zoom narrows the span around wherever the view sits rather than around
|
||||
// the library's midpoint, so zooming in keeps what you were looking at.
|
||||
let zoom = *ctl.timeline_zoom.borrow();
|
||||
let (from, to) = zoomed_span(span, zoom, *ctl.timeline_centre.borrow());
|
||||
|
||||
let granularity = dr_catalog::Granularity::for_span(to - from);
|
||||
let buckets = match library::timeline_scoped(catalog, scope, &filter, granularity, from, to) {
|
||||
// A fixed number of equal bins, re-cut on every zoom (FR-CAT-6). The
|
||||
// count is the user's — see `LibrarySettings::timeline_bars` for why the
|
||||
// calendar units it replaced made zooming in draw a coarser picture.
|
||||
let bins = ctl.timeline_bars.get().max(1);
|
||||
// Calendar units survive as a *label* vocabulary: what a bin is called
|
||||
// depends on how long it is, not on how the counting was done.
|
||||
let granularity = dr_catalog::Granularity::for_bucket((to - from).max(1) / bins as i64);
|
||||
let buckets = match library::timeline_uniform(catalog, scope, &filter, from, to, bins) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
log::debug!("timeline: {e}");
|
||||
@@ -3596,7 +3642,16 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
|
||||
// Square root rather than linear: it keeps a 3-image day
|
||||
// visible beside a 400-image one without a log scale's
|
||||
// misleading flatness.
|
||||
height: ((b.count as f32 / peak as f32).sqrt()).clamp(0.02, 1.0),
|
||||
//
|
||||
// A bin holding nothing draws nothing. The floor exists so a
|
||||
// quiet day is not rounded away, and empty bins are common now
|
||||
// that every one of them is emitted — a sliver on each would
|
||||
// draw a library that has photographs in months it does not.
|
||||
height: if b.count == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((b.count as f32 / peak as f32).sqrt()).clamp(0.02, 1.0)
|
||||
},
|
||||
start: b.start as i32,
|
||||
count: b.count as i32,
|
||||
label: format_bucket(b.start, granularity).into(),
|
||||
@@ -3618,12 +3673,33 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
|
||||
// slot contained the instant — near enough on a dense uniform axis, plainly
|
||||
// wrong on a sparse one, and never under the click.
|
||||
let current = *ctl.current_bucket.borrow();
|
||||
let fraction = current
|
||||
.map(|t| ((t - from) as f64 / (to - from).max(1) as f64).clamp(0.0, 1.0) as f32)
|
||||
.unwrap_or(-1.0);
|
||||
let fraction = current.map(|t| fraction_at((from, to), t)).unwrap_or(-1.0);
|
||||
|
||||
window.set_library_current_bucket(current.unwrap_or(0) as i32);
|
||||
// The chosen range in those same coordinates, for the band. Both ends or
|
||||
// neither: half a band is a line the user cannot tell from a handle.
|
||||
let (band_from, band_to) = match (filter.captured_from, filter.captured_to) {
|
||||
(Some(a), Some(b)) => (fraction_at((from, to), a), fraction_at((from, to), b)),
|
||||
_ => (-1.0, -1.0),
|
||||
};
|
||||
|
||||
// Which bar to light: the *bin* holding the grid's instant, not the
|
||||
// instant itself. A bar's start is its bin's left edge — one of a fixed
|
||||
// set of positions — so comparing it against an arbitrary capture time
|
||||
// would light nothing, where before every bar started on a photograph.
|
||||
//
|
||||
// Taken from the bars themselves rather than recomputed, because the edges
|
||||
// are integer division and a second spelling of `from + i * span / bins`
|
||||
// is a second chance to be a second out.
|
||||
let lit = current
|
||||
.and_then(|t| {
|
||||
let i = bin_of((from, to), t, bins);
|
||||
buckets.get(i).map(|b| b.start)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
window.set_library_current_bucket(lit as i32);
|
||||
window.set_library_current_fraction(fraction);
|
||||
window.set_library_range_from_fraction(band_from);
|
||||
window.set_library_range_to_fraction(band_to);
|
||||
window.set_library_timeline_anchored(current.is_some());
|
||||
window
|
||||
.set_library_timeline_label(format!("{} – {}", format_date(from), format_date(to)).into());
|
||||
@@ -3633,8 +3709,16 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
|
||||
/// Whether this bucket size is fine enough for month labels to mean anything.
|
||||
///
|
||||
/// A year-bucketed axis labelled by month would put twelve labels on one bar.
|
||||
/// Month-sized bins are excluded too, now that the axis has a fixed number of
|
||||
/// them: a bin of about a month changes month at nearly every bar, so the rule
|
||||
/// below would write a label on all sixty-four of them and the right-hand
|
||||
/// strip would be a column of overlapping text. Below a month the labels are
|
||||
/// occasional again, which is what makes them readable.
|
||||
fn granularity_labels_months(g: dr_catalog::Granularity) -> bool {
|
||||
!matches!(g, dr_catalog::Granularity::Year)
|
||||
matches!(
|
||||
g,
|
||||
dr_catalog::Granularity::Day | dr_catalog::Granularity::Hour
|
||||
)
|
||||
}
|
||||
|
||||
/// Full month name, for the grid's headings.
|
||||
@@ -3663,6 +3747,64 @@ fn month_abbrev(m: i64) -> &'static str {
|
||||
NAMES[((m - 1).clamp(0, 11)) as usize]
|
||||
}
|
||||
|
||||
/// The instant a fraction along a span names.
|
||||
///
|
||||
/// One home for `from + (to - from) * f`, because three things now depend on
|
||||
/// it agreeing with itself: a scrub, the marker drawn where that scrub landed,
|
||||
/// and the ends of the date range dragged along the same axis. A second copy
|
||||
/// is a second chance for a click and what it produces to disagree.
|
||||
fn instant_at(span: (i64, i64), f: f32) -> i64 {
|
||||
span.0 + ((span.1 - span.0) as f64 * f.clamp(0.0, 1.0) as f64) as i64
|
||||
}
|
||||
|
||||
/// Where an instant sits along a span, 0..1. The exact inverse of
|
||||
/// [`instant_at`], clamped so an instant off the end of the axis is drawn at
|
||||
/// the end it went past rather than off the widget.
|
||||
fn fraction_at(span: (i64, i64), t: i64) -> f32 {
|
||||
((t - span.0) as f64 / (span.1 - span.0).max(1) as f64).clamp(0.0, 1.0) as f32
|
||||
}
|
||||
|
||||
/// Which of `bins` equal bins across `span` holds `t`.
|
||||
///
|
||||
/// The same arithmetic `library::timeline_uniform` groups by, in the same
|
||||
/// order — integer division on the offset from the start — so the bar this
|
||||
/// names is the bar that counted the photograph. Clamped, because the grid's
|
||||
/// instant can sit outside a zoomed axis.
|
||||
fn bin_of(span: (i64, i64), t: i64, bins: u32) -> usize {
|
||||
let width = (span.1 - span.0).max(1);
|
||||
let i = (t - span.0).clamp(0, width) * bins as i64 / width;
|
||||
i.clamp(0, bins as i64 - 1) as usize
|
||||
}
|
||||
|
||||
/// Midnight UTC at or before `t`.
|
||||
///
|
||||
/// `rem_euclid` rather than `%`: a negative timestamp — a scanned archive
|
||||
/// holds photographs from before 1970 — would otherwise truncate *towards*
|
||||
/// zero and name the following midnight, putting the day off by one.
|
||||
fn day_start(t: i64) -> i64 {
|
||||
t - t.rem_euclid(86_400)
|
||||
}
|
||||
|
||||
/// The range a drag of the band's two ends names, as it is stored.
|
||||
///
|
||||
/// Whole days at both ends, which does three things. It is what the typed
|
||||
/// fields mean, so a dragged range reads back out of them as the two days it
|
||||
/// covers rather than as two instants nobody chose. It gives the band a floor:
|
||||
/// two handles dragged onto each other name one day, not an empty grid. And it
|
||||
/// makes the gesture reproducible — the same day is the same range however
|
||||
/// close the finger came to a bucket edge.
|
||||
///
|
||||
/// Ends dragged past each other are put back in order rather than refused: the
|
||||
/// user has described the same two edges either way round.
|
||||
fn dragged_range(span: (i64, i64), from: f32, to: f32) -> (i64, i64) {
|
||||
let (from, to) = if from <= to { (from, to) } else { (to, from) };
|
||||
let start = day_start(instant_at(span, from));
|
||||
// Inclusive of the closing day. `parse_date` gives midnight, and a range
|
||||
// ending there would exclude every photograph taken on the day named.
|
||||
let end = day_start(instant_at(span, to)) + 86_400 - 1;
|
||||
(start, end)
|
||||
}
|
||||
|
||||
/// Narrow a span by a zoom level, centred on `centre`.
|
||||
///
|
||||
/// Each step halves or doubles the visible duration. Clamped to the library's
|
||||
@@ -3701,6 +3843,12 @@ fn zoomed_span(full: (i64, i64), zoom: i32, centre: Option<i64>) -> (i64, i64) {
|
||||
/// collection, a scrub landed at an instant the collection did not contain and
|
||||
/// the view jumped somewhere the user had not asked for.
|
||||
fn catalog_span(catalog: &Catalog, ctl: &Rc<LibraryController>) -> Option<(i64, i64)> {
|
||||
// `span_scoped` reports the extent with the date range lifted, which is
|
||||
// what every caller here wants: they measure the axis that is on screen,
|
||||
// and that axis spans the library rather than the chosen range. Measured
|
||||
// through the range, narrowing it would move the ground under the very
|
||||
// handles doing the narrowing — each drag re-scaling the axis, so the next
|
||||
// one meant something else.
|
||||
library::span_scoped(catalog, *ctl.scope.borrow(), &ctl.filter.borrow())
|
||||
}
|
||||
|
||||
@@ -3779,6 +3927,43 @@ fn scrub_to(window: &AppWindow, ctl: &Rc<LibraryController>, when: i64) {
|
||||
load_window(window, ctl);
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-7
|
||||
/// Put the view back where the photographer was after the library changed
|
||||
/// under it.
|
||||
///
|
||||
/// Deleting is the case this exists for. The grid draws each cell at its
|
||||
/// absolute place in the library — `(i + offset) / columns` — and a delete does
|
||||
/// two things at once: the library gets shorter, and `offset` is re-clamped
|
||||
/// downward so the loaded window still fills. Neither touches `viewport-y`, so
|
||||
/// the view ends up pointing at a different part of the library, or past the
|
||||
/// end of it entirely. That is the "goes blank and jumps somewhere random".
|
||||
///
|
||||
/// Anchored on the *ordinal the view was showing*, clamped into what is left.
|
||||
/// Not on the deleted image's own position, which no longer exists, and not on
|
||||
/// the top of the library, which would throw away the scroll position on every
|
||||
/// delete — after removing one frame from a wall of twenty thousand, the next
|
||||
/// one you want is the one that just moved into its place.
|
||||
pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
let total = window.get_library_total().max(0) as usize;
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
// The first *visible* ordinal, not the loaded window's start.
|
||||
//
|
||||
// Anchoring on `offset` was the first attempt and it still jumped, because
|
||||
// the two are deliberately different: the window begins about a quarter of
|
||||
// a screen above the view so scrolling up has rows ready, and by this point
|
||||
// in `load_window` it has also been re-clamped against the new, smaller
|
||||
// total. Seeking to it therefore moved the view to somewhere the user was
|
||||
// not — a smaller jump than before, but the same fault.
|
||||
//
|
||||
// `resume_at` is what the grid last reported as its first visible image,
|
||||
// which is the thing the photographer is actually looking at.
|
||||
let anchor = ctl.resume_at.get().min(total - 1);
|
||||
window.set_library_scroll_to(anchor as i32);
|
||||
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
|
||||
}
|
||||
|
||||
/// Format a bucket start for the histogram's hover label.
|
||||
fn format_bucket(t: i64, g: dr_catalog::Granularity) -> String {
|
||||
let (y, m, d, h) = civil_from_unix(t);
|
||||
@@ -4376,7 +4561,7 @@ pub fn wire<F>(
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
let when = from + ((to - from) as f64 * f.clamp(0.0, 1.0) as f64) as i64;
|
||||
let when = instant_at((from, to), f);
|
||||
drop(borrow);
|
||||
scrub_to(&w, &ctl, when);
|
||||
});
|
||||
@@ -4626,12 +4811,18 @@ pub fn wire<F>(
|
||||
});
|
||||
}
|
||||
|
||||
// Narrow the grid to the period the histogram is showing.
|
||||
// Turn the range on, over the period the histogram is showing.
|
||||
//
|
||||
// The range is taken from the timeline rather than typed into two date
|
||||
// fields: finding the fortnight is what the histogram is *for*, and having
|
||||
// found it the user should not have to read the dates off the axis and key
|
||||
// them back in. Zoom, then say "only that".
|
||||
// them back in.
|
||||
//
|
||||
// A starting point rather than the answer. What this puts on the axis is a
|
||||
// band with two draggable ends, and dragging them is how the range is
|
||||
// actually stated — on a phone it is the only way that does not need a
|
||||
// keyboard. Seeding it to the whole visible span means the first drag
|
||||
// narrows from what the user is already looking at.
|
||||
//
|
||||
// Toggling off clears both ends rather than remembering them — a range you
|
||||
// cannot see the extent of is a filter that looks like an empty library.
|
||||
@@ -4670,7 +4861,12 @@ pub fn wire<F>(
|
||||
)
|
||||
})
|
||||
};
|
||||
if let Some((from, to)) = seeded {
|
||||
if let Some(span) = seeded {
|
||||
// Through the same days-inclusive conversion a drag uses,
|
||||
// so a seeded range and a dragged one are the same kind of
|
||||
// thing — and so `show_range` reads this one back out as
|
||||
// the two days it covers.
|
||||
let (from, to) = dragged_range(span, 0.0, 1.0);
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
@@ -4686,6 +4882,59 @@ pub fn wire<F>(
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6 | FR-UI-2
|
||||
// The ends of the range, dragged along the axis they were chosen from.
|
||||
//
|
||||
// This is the control the two typed fields were standing in for. On a
|
||||
// phone they could not do the job: `YYYY-MM-DD` keyed into a 108px field
|
||||
// behind a soft keyboard, to name a day already drawn on the axis a thumb
|
||||
// away. Nor was the chip a way round them — its span comes from the
|
||||
// timeline's zoom and pan, and those are a wheel and a middle button,
|
||||
// neither of which a touch screen has. So on Android the date range was a
|
||||
// filter that could be turned on and not aimed.
|
||||
//
|
||||
// The span is recomputed here rather than remembered from the last draw.
|
||||
// It is the same expression `refresh_timeline` uses, over the same
|
||||
// date-lifted extent, so a handle dropped at a fraction of the track names
|
||||
// the instant that fraction was drawn at.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_timeline_range_changed(move |a, b| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
let span = zoomed_span(
|
||||
full,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
drop(borrow);
|
||||
|
||||
let (from, to) = dragged_range(span, a, b);
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
}
|
||||
|
||||
// A dragged range cannot be mistyped, so any standing complaint
|
||||
// about the fields is now about a range that no longer exists.
|
||||
w.set_library_range_invalid(false);
|
||||
w.set_library_range_active(true);
|
||||
show_range(&w, &ctl);
|
||||
// Reloads the window, and `load_window` redraws the timeline —
|
||||
// which is what puts the band back under the finger that dropped
|
||||
// it, at the whole days it was snapped to.
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6
|
||||
// The ends of the range, typed.
|
||||
//
|
||||
@@ -4721,7 +4970,7 @@ pub fn wire<F>(
|
||||
// Inclusive of the last day. "To the 5th" means the whole of
|
||||
// the 5th — `parse_date` returns its midnight, and a range
|
||||
// ending there would exclude every photograph taken on the day
|
||||
// the user named.
|
||||
// the user named. The same rule `dragged_range` applies.
|
||||
f.captured_to = Some(to + 86_400 - 1);
|
||||
}
|
||||
w.set_library_range_invalid(false);
|
||||
@@ -4885,12 +5134,6 @@ fn stop(slot: &RefCell<Option<slint::Timer>>) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The instant a scrub fraction names within a span, as the handler
|
||||
/// computes it.
|
||||
fn instant_at(span: (i64, i64), f: f32) -> i64 {
|
||||
span.0 + ((span.1 - span.0) as f64 * f.clamp(0.0, 1.0) as f64) as i64
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scrub_fraction_interpolates_within_the_span() {
|
||||
// The point of going fractional: a slow drag must advance
|
||||
@@ -4904,12 +5147,6 @@ mod tests {
|
||||
assert_ne!(instant_at(span, 0.10), instant_at(span, 0.11));
|
||||
}
|
||||
|
||||
/// Where the marker is drawn for an instant, as `refresh_timeline`
|
||||
/// computes it.
|
||||
fn marker_at(span: (i64, i64), t: i64) -> f32 {
|
||||
((t - span.0) as f64 / (span.1 - span.0).max(1) as f64).clamp(0.0, 1.0) as f32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_marker_lands_on_the_fraction_that_was_clicked() {
|
||||
// The bug this guards: the marker was placed from a *bar index* while
|
||||
@@ -4917,7 +5154,7 @@ mod tests {
|
||||
// the pointer. Marker and click must be inverses.
|
||||
let span = (1_000, 9_000);
|
||||
for f in [0.0_f32, 0.1, 0.25, 0.5, 0.75, 1.0] {
|
||||
let round_trip = marker_at(span, instant_at(span, f));
|
||||
let round_trip = fraction_at(span, instant_at(span, f));
|
||||
assert!(
|
||||
(round_trip - f).abs() < 1e-3,
|
||||
"clicked {f}, marker drawn at {round_trip}"
|
||||
@@ -4930,8 +5167,8 @@ mod tests {
|
||||
// Zooming in leaves the grid's instant outside the axis. The marker
|
||||
// belongs at the edge it went past, not off the widget.
|
||||
let span = (1_000, 2_000);
|
||||
assert_eq!(marker_at(span, 0), 0.0);
|
||||
assert_eq!(marker_at(span, 5_000), 1.0);
|
||||
assert_eq!(fraction_at(span, 0), 0.0);
|
||||
assert_eq!(fraction_at(span, 5_000), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4943,6 +5180,92 @@ mod tests {
|
||||
assert_eq!(instant_at(span, 9.9), 200);
|
||||
}
|
||||
|
||||
/// A fortnight in March 2024, in whole days, as the axis would span it.
|
||||
const MARCH: (i64, i64) = (1_709_251_200, 1_710_460_800);
|
||||
|
||||
#[test]
|
||||
fn a_dragged_range_covers_whole_days_at_both_ends() {
|
||||
// What makes a dragged range the same kind of thing as a typed one:
|
||||
// `show_range` reads the closing day back out of the stored second, so
|
||||
// an end left mid-afternoon would be shown as the day before.
|
||||
let (from, to) = dragged_range(MARCH, 0.13, 0.61);
|
||||
assert_eq!(day_start(from), from, "opens at a midnight");
|
||||
assert_eq!(
|
||||
day_start(to + 1),
|
||||
to + 1,
|
||||
"closes at the last second of a day"
|
||||
);
|
||||
assert_eq!(dr_types::format_date(from), "2024-03-02");
|
||||
assert_eq!(dr_types::format_date(to - 86_400 + 1), "2024-03-09");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ends_dragged_past_each_other_name_the_same_range() {
|
||||
// Pulling the early end below the late one is how a range that was
|
||||
// narrowed too far gets corrected; refusing it would make starting
|
||||
// again the easier move.
|
||||
assert_eq!(
|
||||
dragged_range(MARCH, 0.6, 0.2),
|
||||
dragged_range(MARCH, 0.2, 0.6)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_ends_dragged_together_name_one_day() {
|
||||
// The floor the day snapping buys. Two handles on the same pixel is a
|
||||
// gesture that happens; an empty grid is not what it means.
|
||||
let (from, to) = dragged_range(MARCH, 0.5, 0.5);
|
||||
assert_eq!(to - from, 86_399);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_band_is_drawn_where_the_handle_was_dropped() {
|
||||
// The band is placed by `fraction_at` and the range comes from
|
||||
// `instant_at`. If those two ever disagreed the band would settle
|
||||
// somewhere other than where the finger let go, which reads as the
|
||||
// drag having been ignored.
|
||||
for f in [0.0_f32, 0.25, 0.5, 0.75, 1.0] {
|
||||
let (from, _) = dragged_range(MARCH, f, f);
|
||||
let drawn = fraction_at(MARCH, from);
|
||||
// Within a day of the span, which is the snapping and nothing else.
|
||||
let day = 86_400.0 / (MARCH.1 - MARCH.0) as f32;
|
||||
assert!((drawn - f).abs() <= day, "dropped at {f}, drawn at {drawn}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_instant_on_the_axis_lands_in_a_bin() {
|
||||
// The bar under a date and the bar that counted it must be the same
|
||||
// bar: `bin_of` is the UI's copy of the division the SQL groups by.
|
||||
let span = (1_000, 9_000);
|
||||
assert_eq!(bin_of(span, 1_000, 8), 0);
|
||||
assert_eq!(bin_of(span, 1_999, 8), 0);
|
||||
assert_eq!(bin_of(span, 2_000, 8), 1);
|
||||
// The last instant belongs to the last bin rather than to one past it.
|
||||
assert_eq!(bin_of(span, 9_000, 8), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instant_off_the_zoomed_axis_lands_at_the_end_it_went_past() {
|
||||
// Zooming in leaves the grid's position outside the span. The lit bar
|
||||
// belongs at the edge, not at an index that does not exist.
|
||||
let span = (1_000, 9_000);
|
||||
assert_eq!(bin_of(span, -50_000, 8), 0);
|
||||
assert_eq!(bin_of(span, 50_000, 8), 7);
|
||||
// And a degenerate axis still answers with a bin.
|
||||
assert_eq!(bin_of((5, 5), 5, 32), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_day_before_1970_starts_at_its_own_midnight() {
|
||||
// `%` truncates towards zero, which on a negative timestamp names the
|
||||
// *following* midnight — a scanned archive of negatives would have
|
||||
// every range off by a day at the early end.
|
||||
assert_eq!(day_start(-1), -86_400);
|
||||
assert_eq!(day_start(-86_400), -86_400);
|
||||
assert_eq!(day_start(-86_401), -172_800);
|
||||
}
|
||||
|
||||
/// One pinch update, as the handler applies it: fold the ratio in, take
|
||||
/// out whole doublings, carry the remainder.
|
||||
fn pinch_step(accum: &mut f32, ratio: f32) -> i32 {
|
||||
@@ -5189,32 +5512,38 @@ mod tests {
|
||||
assert_eq!(y, 1969);
|
||||
}
|
||||
|
||||
/// The axis follows a chosen range, so its granularity does too.
|
||||
/// A bar is named after how long it is.
|
||||
///
|
||||
/// The complaint this fixes: a fortnight selected out of a fifteen-year
|
||||
/// library was still drawn as one column of a year-per-bar axis, which is
|
||||
/// the least useful picture of a fortnight available.
|
||||
/// The axis no longer buckets by calendar unit — it cuts the visible span
|
||||
/// into a fixed number of equal bins — so the unit survives only as a
|
||||
/// vocabulary for *labelling* one. What this guards is that zooming in
|
||||
/// never produces a coarser label: the complaint it came from was a
|
||||
/// fortnight drawn as one column of a year-per-bar axis.
|
||||
#[test]
|
||||
fn a_chosen_range_decides_the_granularity() {
|
||||
fn a_bin_is_labelled_at_its_own_scale() {
|
||||
use dr_catalog::Granularity;
|
||||
const DAY: i64 = 86_400;
|
||||
|
||||
// Fifteen years unfiltered: a bar per year, fifteen of them.
|
||||
assert_eq!(Granularity::for_span(15 * 365 * DAY), Granularity::Year);
|
||||
// Fifteen years across 32 bars is nearly six months a bar, which on a
|
||||
// ratio measure is nearer a year than a month — so the bars are
|
||||
// labelled by year, which is what a fifteen-year axis wants anyway.
|
||||
assert_eq!(
|
||||
Granularity::for_bucket(15 * 365 * DAY / 32),
|
||||
Granularity::Year
|
||||
);
|
||||
|
||||
// Narrowed to a fortnight, the axis is handed the range rather than
|
||||
// the extent, and a fortnight is drawn as fourteen days.
|
||||
assert_eq!(Granularity::for_span(14 * DAY), Granularity::Day);
|
||||
// A fortnight across the same 32 bars is about ten hours a bar, which
|
||||
// is nearer a day than an hour and is labelled as a date.
|
||||
assert_eq!(Granularity::for_bucket(14 * DAY / 32), Granularity::Day);
|
||||
|
||||
// A single day's shoot is drawn by the hour.
|
||||
assert_eq!(Granularity::for_span(DAY), Granularity::Hour);
|
||||
// Zoom on until a bar is an hour or two, and it is named by the hour.
|
||||
assert_eq!(Granularity::for_bucket(2 * 3600), Granularity::Hour);
|
||||
|
||||
// The property rather than the thresholds, because the thresholds are
|
||||
// no longer what decides it: a narrower range never produces a coarser
|
||||
// bucket than a wider one.
|
||||
// The property rather than the thresholds: a shorter bin never gets a
|
||||
// coarser label than a longer one.
|
||||
let mut previous = i64::MAX;
|
||||
for days in [3650, 730, 180, 60, 14, 3, 1] {
|
||||
let g = Granularity::for_span(days * DAY);
|
||||
let g = Granularity::for_bucket(days * DAY / 32);
|
||||
let bucket = match g {
|
||||
Granularity::Year => 365 * DAY,
|
||||
Granularity::Month => 30 * DAY,
|
||||
@@ -5223,12 +5552,52 @@ mod tests {
|
||||
};
|
||||
assert!(
|
||||
bucket <= previous,
|
||||
"{days} days chose a coarser bucket than the range above it"
|
||||
"{days} days chose a coarser bin label than the span above it"
|
||||
);
|
||||
previous = bucket;
|
||||
}
|
||||
}
|
||||
|
||||
/// Deleting must not move the view somewhere the user did not ask to be.
|
||||
///
|
||||
/// The grid draws each cell at its absolute place in the library, and a
|
||||
/// delete shortens the library *and* re-clamps the loaded window's offset.
|
||||
/// Neither touches the viewport, so before this the grid went blank — the
|
||||
/// view left pointing past the end of the content — or appeared to jump,
|
||||
/// the same scroll position now addressing different photographs.
|
||||
///
|
||||
/// The anchor is the last *visible* ordinal the grid reported, clamped into
|
||||
/// what is left — not the loaded window's `offset`, which sits about a
|
||||
/// quarter of a screen above the view and is itself re-clamped by the
|
||||
/// delete. Anchoring on that was the first attempt and still jumped.
|
||||
#[test]
|
||||
fn a_shorter_library_anchors_the_view_instead_of_losing_it() {
|
||||
// The rule `restore_position` applies, stated where it can be checked.
|
||||
let anchor = |offset: usize, total: usize| -> Option<usize> {
|
||||
if total == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(offset.min(total - 1))
|
||||
}
|
||||
};
|
||||
|
||||
// The everyday case: one frame removed from the middle of a wall of
|
||||
// twenty thousand. The view does not move — the photograph that slid
|
||||
// into the gap is the one you want next.
|
||||
assert_eq!(anchor(12_000, 19_999), Some(12_000));
|
||||
|
||||
// Near the end, which is where the blank came from: the view was
|
||||
// showing ordinals past what now exists, so it lands on the last.
|
||||
assert_eq!(anchor(19_990, 5), Some(4));
|
||||
|
||||
// Emptied entirely. Seeking into an empty library would be the same
|
||||
// fault in the other direction, so there is nothing to do.
|
||||
assert_eq!(anchor(500, 0), None);
|
||||
|
||||
// The boundary: an offset equal to the new total is one past the end.
|
||||
assert_eq!(anchor(10, 10), Some(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_labels_match_their_granularity() {
|
||||
use dr_catalog::Granularity;
|
||||
|
||||
@@ -26,8 +26,8 @@ use std::rc::Rc;
|
||||
|
||||
use dr_types::settings::budget;
|
||||
use dr_types::{
|
||||
CollisionPolicy, ColourSpace, ExportFormat, ExportTarget, OutputSharpening, Settings,
|
||||
SizingMode,
|
||||
CollisionPolicy, ColourSpace, ExportFormat, ExportTarget, LibrarySettings, OutputSharpening,
|
||||
Settings, SizingMode,
|
||||
};
|
||||
use slint::ComponentHandle;
|
||||
|
||||
@@ -146,6 +146,23 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
|
||||
window.set_settings_copy_includes_framing(s.develop.copy_includes_framing);
|
||||
window.set_settings_cache_usage(controller.usage_label.borrow().clone().into());
|
||||
|
||||
// --- library -------------------------------------------------------
|
||||
//
|
||||
// Labelled here rather than in the page, so the chips cannot offer a count
|
||||
// the record does not: `BAR_CHOICES` is what `sanitise` snaps a
|
||||
// hand-edited file to, and a third chip drawn beside them would be a
|
||||
// choice that silently became one of the other two.
|
||||
window.set_settings_timeline_bar_labels(slint::ModelRc::new(slint::VecModel::from(
|
||||
LibrarySettings::BAR_CHOICES
|
||||
.iter()
|
||||
.map(|n| slint::SharedString::from(n.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
window.set_settings_timeline_bars_selected(index_of(
|
||||
&LibrarySettings::BAR_CHOICES,
|
||||
&s.library.timeline_bars,
|
||||
));
|
||||
|
||||
// --- export --------------------------------------------------------
|
||||
//
|
||||
// Choice rows are sent as labels plus the selected index rather than as a
|
||||
@@ -404,6 +421,18 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = controller.clone();
|
||||
window.on_settings_timeline_bars_picked(move |i| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(n) = LibrarySettings::BAR_CHOICES.get(i as usize).copied() {
|
||||
ctl.edit(|s| s.library.timeline_bars = n);
|
||||
}
|
||||
render(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = controller.clone();
|
||||
|
||||
@@ -455,11 +455,20 @@ export component AppWindow inherits Window {
|
||||
/// Whether the grid is narrowed to the timeline's visible span.
|
||||
in-out property <bool> library-range-active: false;
|
||||
callback library-toggle-date-range();
|
||||
/// TRACES: FR-CAT-5
|
||||
callback library-clear-selection();
|
||||
callback library-collection-from-selection();
|
||||
/// TRACES: FR-CAT-6
|
||||
in property <string> library-range-from;
|
||||
in property <string> library-range-to;
|
||||
in property <bool> library-range-invalid: false;
|
||||
callback library-range-edited(string, string);
|
||||
/// The same range as a band on the capture-time axis, and the ends of it
|
||||
/// dragged. Fractions of the span the axis is drawn over — Rust owns the
|
||||
/// conversion both ways, so the band lands where the finger left it.
|
||||
in property <float> library-range-from-fraction: -1;
|
||||
in property <float> library-range-to-fraction: -1;
|
||||
callback library-timeline-range-changed(float, float);
|
||||
callback library-retry-connection();
|
||||
|
||||
// --- pinning a collection offline (FR-NC-6a) ---
|
||||
@@ -802,6 +811,10 @@ export component AppWindow inherits Window {
|
||||
callback settings-thumbnail-unlimited-toggled(bool);
|
||||
callback settings-keep-opened-toggled(bool);
|
||||
callback settings-copy-includes-framing-toggled(bool);
|
||||
/// TRACES: FR-CAT-6
|
||||
in property <[string]> settings-timeline-bar-labels;
|
||||
in property <int> settings-timeline-bars-selected: 0;
|
||||
callback settings-timeline-bars-picked(int);
|
||||
|
||||
in property <[string]> settings-format-labels;
|
||||
in property <int> settings-format-selected: 0;
|
||||
@@ -1133,6 +1146,9 @@ in property <bool> panel-visible: true;
|
||||
thumbnail-unlimited: root.settings-thumbnail-unlimited;
|
||||
keep-opened: root.settings-keep-opened;
|
||||
copy-includes-framing: root.settings-copy-includes-framing;
|
||||
timeline-bar-labels: root.settings-timeline-bar-labels;
|
||||
timeline-bars-selected: root.settings-timeline-bars-selected;
|
||||
timeline-bars-picked(i) => { root.settings-timeline-bars-picked(i); }
|
||||
cache-usage: root.settings-cache-usage;
|
||||
|
||||
original-budget-changed(t) => { root.settings-original-budget-changed(t); }
|
||||
@@ -1321,10 +1337,17 @@ in property <bool> panel-visible: true;
|
||||
toggle-local-only() => { root.library-toggle-local-only(); }
|
||||
range-active: root.library-range-active;
|
||||
toggle-date-range() => { root.library-toggle-date-range(); }
|
||||
clear-selection() => { root.library-clear-selection(); }
|
||||
collection-from-selection() => { root.library-collection-from-selection(); }
|
||||
range-from: root.library-range-from;
|
||||
range-to: root.library-range-to;
|
||||
range-invalid: root.library-range-invalid;
|
||||
range-edited(a, b) => { root.library-range-edited(a, b); }
|
||||
range-from-fraction: root.library-range-from-fraction;
|
||||
range-to-fraction: root.library-range-to-fraction;
|
||||
timeline-range-changed(a, b) => {
|
||||
root.library-timeline-range-changed(a, b);
|
||||
}
|
||||
|
||||
collections-visible: root.collections-visible;
|
||||
toggle-collections() => { root.toggle-collections(); }
|
||||
|
||||
+273
-8
@@ -68,6 +68,7 @@ export struct TimelineBar {
|
||||
// - **click or drag** — scrub the grid to that instant
|
||||
// - **drag with the middle button, or shift-drag** — pan the visible span
|
||||
// - **wheel** — zoom, which changes the bucket size Rust picks
|
||||
// - **drag either end of the range band** — narrow the grid to a period
|
||||
//
|
||||
// # One hit area, not one per bar
|
||||
//
|
||||
@@ -105,6 +106,21 @@ export component Timeline inherits Rectangle {
|
||||
/// them together. Continuous, unlike the wheel's discrete steps.
|
||||
callback pinch(float);
|
||||
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The chosen date range, as fractions of the drawn span — the same
|
||||
/// coordinates `scrub-to` reports and `current-fraction` is drawn in, so
|
||||
/// the band, the marker and a click cannot disagree about where a date
|
||||
/// sits. Negative in both when no range is set.
|
||||
in property <float> range-from: -1;
|
||||
in property <float> range-to: -1;
|
||||
/// An end of the range was dragged, and where both ends now are.
|
||||
///
|
||||
/// Sent on release rather than continuously: each one of these re-runs the
|
||||
/// grid's query and reloads its window, and doing that per frame of a drag
|
||||
/// is how a filter turns into a stutter. The band tracks the finger
|
||||
/// meanwhile — that is what `live-from` and `live-to` are for.
|
||||
callback range-changed(float, float);
|
||||
|
||||
width: 96px;
|
||||
background: Theme.surface;
|
||||
|
||||
@@ -113,6 +129,21 @@ export component Timeline inherits Rectangle {
|
||||
property <length> track-height: max(1px, self.height - self.track-top - 6px);
|
||||
property <length> slot: root.track-height / max(1, root.bars.length);
|
||||
|
||||
property <bool> has-range: root.range-from >= 0 && root.range-to >= 0;
|
||||
/// Which end is under the finger: 0 neither, 1 the early end, 2 the late.
|
||||
property <int> grabbed: 0;
|
||||
/// The ends as the drag has them, before Rust has been told. Drawing from
|
||||
/// these is what lets the band follow the finger without the grid being
|
||||
/// re-queried on every frame.
|
||||
property <float> live-from;
|
||||
property <float> live-to;
|
||||
property <float> shown-from: root.grabbed > 0 ? root.live-from : root.range-from;
|
||||
property <float> shown-to: root.grabbed > 0 ? root.live-to : root.range-to;
|
||||
/// How near a press has to land to take hold of an end, as a fraction of
|
||||
/// the track. Sized for a finger rather than a pointer: this control
|
||||
/// exists because on a phone the range could only be typed (FR-UI-2).
|
||||
property <float> grab: 22px / root.track-height;
|
||||
|
||||
/// Bucket index under a y coordinate, clamped to the ends so a drag that
|
||||
/// leaves the widget still scrubs to the nearest bucket rather than
|
||||
/// stopping dead.
|
||||
@@ -132,6 +163,50 @@ export component Timeline inherits Rectangle {
|
||||
return clamp((y - root.track-top) / max(1px, root.track-height), 0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Where a fraction sits on the track. The inverse of `fraction-at`, and
|
||||
/// it has to stay so: the band is drawn with this and dragged with that.
|
||||
function y-at(f: float) -> length {
|
||||
return root.track-top + clamp(f, 0.0, 1.0) * root.track-height;
|
||||
}
|
||||
|
||||
/// Which end of the range a press at `y` takes hold of, 0 for neither.
|
||||
///
|
||||
/// Compared in fractions rather than pixels because the ends are held as
|
||||
/// fractions — converting them to a y here and back to a fraction on the
|
||||
/// drag would be a second copy of the same arithmetic to keep in step.
|
||||
function handle-at(y: length) -> int {
|
||||
if (!root.has-range) {
|
||||
return 0;
|
||||
}
|
||||
if (min(abs(root.fraction-at(y) - root.shown-from),
|
||||
abs(root.fraction-at(y) - root.shown-to)) > root.grab) {
|
||||
return 0;
|
||||
}
|
||||
// A range narrowed to a day draws one line, not two. Taking the
|
||||
// nearer end there would always answer "the early one" and leave the
|
||||
// late end impossible to pull back out; which side of the line the
|
||||
// press landed on is what the user meant by it.
|
||||
if (abs(root.shown-from - root.shown-to) < 0.001) {
|
||||
return root.fraction-at(y) > root.shown-from ? 2 : 1;
|
||||
}
|
||||
return abs(root.fraction-at(y) - root.shown-from)
|
||||
< abs(root.fraction-at(y) - root.shown-to) ? 1 : 2;
|
||||
}
|
||||
|
||||
/// Move the held end to `y`.
|
||||
///
|
||||
/// The ends are allowed to cross. Dragging one past the other is a normal
|
||||
/// way to say "no, from *here* instead", and Rust puts the pair back in
|
||||
/// order — stopping the finger dead at the other end would make a range
|
||||
/// that has been narrowed too far harder to correct than to start again.
|
||||
function drag-end(y: length) {
|
||||
if (root.grabbed == 1) {
|
||||
root.live-from = root.fraction-at(y);
|
||||
} else {
|
||||
root.live-to = root.fraction-at(y);
|
||||
}
|
||||
}
|
||||
|
||||
// Header: the hovered bucket, else the whole span.
|
||||
Caption {
|
||||
x: 8px;
|
||||
@@ -183,10 +258,68 @@ export component Timeline inherits Rectangle {
|
||||
background: Theme.active;
|
||||
opacity: root.anchored ? 1.0 : 0.35;
|
||||
y: root.anchored && root.current-fraction >= 0
|
||||
? root.track-top + clamp(root.current-fraction, 0.0, 1.0) * root.track-height
|
||||
? root.y-at(root.current-fraction)
|
||||
: root.track-top + root.track-height / 2;
|
||||
}
|
||||
|
||||
// --- the chosen date range ------------------------------------------
|
||||
//
|
||||
// Drawn *on* the axis, and dragged there. The two typed date fields under
|
||||
// the filter chips were the only way to state a range, and on a phone they
|
||||
// were barely a way at all: `YYYY-MM-DD` keyed into a 108px field behind a
|
||||
// soft keyboard, to name a day that is already drawn on the axis a thumb
|
||||
// away. The other route — zoom the axis to a period, then "limit to range"
|
||||
// — needs a wheel to zoom and a middle button to pan, and a touch screen
|
||||
// has neither.
|
||||
//
|
||||
// The axis keeps its full extent while a range is on (see
|
||||
// `refresh_timeline`, and `Filter::without_date_range`). It has to: the
|
||||
// bars outside the band are what the range is widened back *into*, and an
|
||||
// axis that redrew itself to the band would move the ground under the very
|
||||
// handles doing the narrowing.
|
||||
if root.has-range: Rectangle {
|
||||
// Outside the range, held back rather than hidden — those bars are
|
||||
// still the shape of the library, and still where the band is going
|
||||
// next. Starts at the track, so the header line stays legible.
|
||||
Rectangle {
|
||||
x: 0;
|
||||
y: root.track-top;
|
||||
width: parent.width;
|
||||
height: max(0px, root.y-at(min(root.shown-from, root.shown-to)) - root.track-top);
|
||||
background: Theme.surface;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: 0;
|
||||
y: root.y-at(max(root.shown-from, root.shown-to));
|
||||
width: parent.width;
|
||||
height: max(0px, parent.height - self.y);
|
||||
background: Theme.surface;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
// The ends themselves: a line across the axis with a grip at the left,
|
||||
// where the bars start. The right-hand strip carries the year and
|
||||
// month labels, and a grip there would sit on top of them.
|
||||
for edge[i] in [0, 1]: Rectangle {
|
||||
x: 0;
|
||||
y: root.y-at(i == 0 ? root.shown-from : root.shown-to) - 1px;
|
||||
width: parent.width;
|
||||
height: 2px;
|
||||
background: Theme.active;
|
||||
|
||||
Rectangle {
|
||||
x: 2px;
|
||||
y: -5px;
|
||||
width: 18px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
background: Theme.active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The single hit area. Everything above is inert.
|
||||
// Two-finger pinch, for tablet. There is no wheel there, so without this
|
||||
// the axis could only be zoomed by a control a finger cannot reach.
|
||||
@@ -218,7 +351,11 @@ export component Timeline inherits Rectangle {
|
||||
property <bool> panning;
|
||||
|
||||
moved => {
|
||||
if (self.panning) {
|
||||
if (root.grabbed > 0) {
|
||||
// The band follows the finger. The grid hears about it on
|
||||
// release — see `range-changed`.
|
||||
root.drag-end(self.mouse-y);
|
||||
} else if (self.panning) {
|
||||
// Fractional, so a slow drag moves the view continuously
|
||||
// rather than sitting still until it crosses a bucket edge.
|
||||
root.pan((self.press-y - self.mouse-y) / max(1px, root.track-height));
|
||||
@@ -236,11 +373,31 @@ export component Timeline inherits Rectangle {
|
||||
// modifier state belongs to the key handler, not the pointer
|
||||
// event, and a middle-drag is the unambiguous gesture.
|
||||
self.panning = e.button == PointerEventButton.middle;
|
||||
if (!self.panning && root.bars.length > 0) {
|
||||
// An end of the range first, if the press landed on one: a
|
||||
// press within a finger's width of a handle means the handle,
|
||||
// not the scrub it would otherwise have been.
|
||||
root.grabbed = self.panning ? 0 : root.handle-at(self.mouse-y);
|
||||
if (root.grabbed > 0) {
|
||||
root.live-from = root.range-from;
|
||||
root.live-to = root.range-to;
|
||||
} else if (!self.panning && root.bars.length > 0) {
|
||||
root.scrub-to(root.fraction-at(self.mouse-y));
|
||||
}
|
||||
}
|
||||
if (e.kind == PointerEventKind.up) { self.panning = false; }
|
||||
if (e.kind == PointerEventKind.up) {
|
||||
self.panning = false;
|
||||
if (root.grabbed > 0) {
|
||||
root.grabbed = 0;
|
||||
root.range-changed(root.live-from, root.live-to);
|
||||
}
|
||||
}
|
||||
// A cancelled gesture — the system taking the touch for a
|
||||
// back-swipe, most often — is not a range the user stated. The
|
||||
// band goes back to where Rust still has it.
|
||||
if (e.kind == PointerEventKind.cancel) {
|
||||
self.panning = false;
|
||||
root.grabbed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
scroll-event(e) => {
|
||||
@@ -1035,6 +1192,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// as the ctrl flag rather than as a third selection policy.
|
||||
in property <bool> select-mode: false;
|
||||
callback toggle-select-mode();
|
||||
/// TRACES: FR-CAT-5
|
||||
/// Drop the selection without leaving select mode.
|
||||
callback clear-selection();
|
||||
/// TRACES: FR-CAT-5
|
||||
/// Make a new collection holding exactly what is selected.
|
||||
callback collection-from-selection();
|
||||
/// The drag payload: the selected image ids, wrapped by Rust. Called when a
|
||||
/// drag starts, so it always reflects the selection as it is at that moment.
|
||||
pure callback drag-payload() -> data-transfer;
|
||||
@@ -1099,6 +1262,16 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <bool> range-active: false;
|
||||
callback toggle-date-range();
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The same range as fractions of the span the axis is drawn over, for
|
||||
/// the band on the timeline. Negative in both when there is none.
|
||||
///
|
||||
/// A second representation of one range, which is worth it: the fields
|
||||
/// below are how a date is *stated* and the band is how a period is
|
||||
/// *found*, and neither is a good way to do the other's job. Both come
|
||||
/// from the one filter in Rust, so they cannot drift apart.
|
||||
in property <float> range-from-fraction: -1;
|
||||
in property <float> range-to-fraction: -1;
|
||||
callback timeline-range-changed(float, float);
|
||||
/// The ends of the range, as `YYYY-MM-DD`, when one is set.
|
||||
///
|
||||
/// Strings rather than instants because this is what the user types and
|
||||
@@ -1730,10 +1903,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
// Narrow the grid to the span the timeline is showing. The
|
||||
// histogram is already how you find a period, so the range is
|
||||
// taken from it rather than typed: zoom to the fortnight you
|
||||
// want, then say "only that". Clicking again lifts it.
|
||||
// Turn the range on, over the span the timeline is showing.
|
||||
// The histogram is already how you find a period, so this is
|
||||
// a starting point rather than the answer: the band it puts on
|
||||
// the axis is then dragged to the fortnight you meant, which
|
||||
// is the one way in that needs neither a keyboard nor a wheel.
|
||||
// Pressing again lifts the range.
|
||||
//
|
||||
// The histogram deliberately keeps drawing the full extent
|
||||
// while this is on, or there would be nothing left to widen
|
||||
@@ -1767,6 +1942,66 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- what is selected, and what to do with it --------------------
|
||||
//
|
||||
// The count and these actions used to live only in the header row,
|
||||
// which scrolls sideways: on a tablet "12 selected" and everything
|
||||
// beside it sat past the right-hand edge, so a selection was something
|
||||
// you could make and then not see. A selection you cannot see is one
|
||||
// you act on by accident.
|
||||
//
|
||||
// Its own strip, like the date range's, appearing only while there is
|
||||
// a selection — so it costs nothing the rest of the time, and when it
|
||||
// is there it has the width of the window rather than the tail of a
|
||||
// row that did not fit.
|
||||
if root.selected-count > 0: Rectangle {
|
||||
height: 40px;
|
||||
background: Theme.surface;
|
||||
|
||||
HorizontalLayout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap-sm;
|
||||
alignment: start;
|
||||
|
||||
// State rather than a label: it is what the buttons beside it
|
||||
// act on.
|
||||
Value {
|
||||
text: root.selected-count + (root.selected-count == 1
|
||||
? " photograph selected" : " photographs selected");
|
||||
modified: true;
|
||||
font-weight: 600;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
// The way out that is not "undo every tap". Distinct from
|
||||
// "Done", which leaves select mode entirely: clearing keeps the
|
||||
// mode, so the next selection can start straight away.
|
||||
Button {
|
||||
text: "Clear";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.clear-selection(); }
|
||||
}
|
||||
|
||||
// Filing a selection into a collection that does not exist yet
|
||||
// took four steps: make a collection, find it, select the
|
||||
// photographs again, add them. One press instead, which is how
|
||||
// a selection is usually meant.
|
||||
Button {
|
||||
text: "New collection from selection";
|
||||
primary: true;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.collection-from-selection(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- the date range's ends ---------------------------------------
|
||||
//
|
||||
// Its own strip, a sibling of the chips rather than a child of them.
|
||||
@@ -1777,6 +2012,11 @@ export component LibraryGrid inherits Rectangle {
|
||||
// into the chip row — which is 34px tall and scrolls sideways, so
|
||||
// they were both clipped and off past the right-hand edge.
|
||||
//
|
||||
// They are no longer the only way to state a range — the band on the
|
||||
// axis is, and on a phone it is the only usable one. These stay for
|
||||
// the two things dragging cannot do: name an exact day, and say what
|
||||
// the range currently is in words rather than as a position.
|
||||
//
|
||||
// A `Rectangle` stacks its children at the origin rather than laying
|
||||
// them out, which is why putting a second row inside the chips' one
|
||||
// drew it over them instead of under them. This is a row of the
|
||||
@@ -1958,7 +2198,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
current-start: root.current-bucket;
|
||||
current-fraction: root.current-bucket-fraction;
|
||||
anchored: root.timeline-anchored;
|
||||
range-from: root.range-from-fraction;
|
||||
range-to: root.range-to-fraction;
|
||||
|
||||
range-changed(a, b) => { root.timeline-range-changed(a, b); }
|
||||
scrub-to(f) => { root.scrub-fraction(f); }
|
||||
pinch(r) => { root.timeline-pinch(r); }
|
||||
pan(d) => { root.timeline-pan(d); }
|
||||
@@ -2250,6 +2493,28 @@ export component LibraryGrid inherits Rectangle {
|
||||
// window is swapped underneath to match.
|
||||
viewport-height: root.total-rows * (root.cell-size + Theme.gap) + Theme.gap;
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
// Stay inside the content when the content shrinks.
|
||||
//
|
||||
// Deleting photographs makes the library shorter, and the
|
||||
// viewport is sized to the *whole* library — so a view that was
|
||||
// scrolled near the end is suddenly scrolled past it. Slint
|
||||
// does not pull a Flickable back on its own, so the grid went
|
||||
// blank: the cells were still there, above a viewport looking
|
||||
// at empty space below them.
|
||||
//
|
||||
// Worse than blank, it also *moved*. Cells are drawn at
|
||||
// `(i + offset) / columns`, and a delete re-clamps `offset`
|
||||
// downward to keep the window full — so the same `viewport-y`
|
||||
// now points at a different part of the library, and the grid
|
||||
// appeared to jump somewhere arbitrary. Re-seeking below is the
|
||||
// other half of this; this half stops the blank.
|
||||
changed viewport-height => {
|
||||
if (-self.viewport-y > max(0px, self.viewport-height - self.height)) {
|
||||
self.viewport-y = -max(0px, self.viewport-height - self.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Report the first fully-scrolled-past row so Rust can move the
|
||||
// window. Derived rather than eventful: Slint has no scroll
|
||||
// callback, and a `changed` handler on a derived integer fires only
|
||||
|
||||
@@ -111,6 +111,16 @@ export component SettingsPage inherits Rectangle {
|
||||
callback keep-opened-toggled(bool);
|
||||
callback copy-includes-framing-toggled(bool);
|
||||
|
||||
// --- library -------------------------------------------------------
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The bar counts the capture-time axis can be cut into, and which one is
|
||||
/// chosen. Labels from Rust, like every other choice row: the numbers are
|
||||
/// the ones the settings record offers, and a list written again here
|
||||
/// would be a second place for them to go stale.
|
||||
in property <[string]> timeline-bar-labels;
|
||||
in property <int> timeline-bars-selected: 0;
|
||||
callback timeline-bars-picked(int);
|
||||
|
||||
// --- export --------------------------------------------------------
|
||||
in property <[string]> format-labels;
|
||||
in property <int> format-selected: 0;
|
||||
@@ -472,6 +482,34 @@ export component SettingsPage inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- library ---------------------------------------------
|
||||
Rectangle {
|
||||
width: content.column;
|
||||
height: library-panel.preferred-height;
|
||||
|
||||
library-panel := Panel {
|
||||
width: 100%;
|
||||
spacing: Theme.gap;
|
||||
|
||||
PanelHeading { text: "LIBRARY"; }
|
||||
|
||||
Segmented {
|
||||
label: "Timeline detail";
|
||||
// Says what the choice is *about*, since neither
|
||||
// number means anything on its own: it is how
|
||||
// finely the capture-time axis beside the grid is
|
||||
// divided, and the honest trade is precision
|
||||
// against being able to hit a bar with a thumb.
|
||||
hint: "bars on the capture-time axis — fewer are "
|
||||
+ "easier to hit with a finger, more show "
|
||||
+ "finer structure";
|
||||
options: root.timeline-bar-labels;
|
||||
selected: root.timeline-bars-selected;
|
||||
picked(i) => { root.timeline-bars-picked(i); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- export ----------------------------------------------
|
||||
Rectangle {
|
||||
width: content.column;
|
||||
|
||||
Reference in New Issue
Block a user