feat(core,fuse): group the RAW and the JPEG of one shot

A camera that writes both puts two files on the volume, and the timeline showed
two tiles of the same photo. They are one shot to the person who took it.

Schema v30 records what ties them together: the server's own duplicate hash and
main-photo relation, plus a `group_key` every member of a shot carries. The key
is computed when the timeline is replaced, in three steps — the server relation
first, because it knows about live photos and bursts and we do not have to guess
at them; then same capture day, same file-name stem and one member raw while the
other is not, which is the pair a camera writes; otherwise the photo is its own
group. The member the grid shows is the non-raw one, since a JPEG decodes in
milliseconds and is what the person expects to see.

The gallery therefore pages by group and carries `group_size` and `has_raw` on
each entry, and the date scrubber counts what the page shows. The Raw tab is the
exception and still lists every raw file, because that tab exists to answer
"which raws do I have". Album pages are not grouped at all: an album is a list
someone made, and it shows exactly what they put in it.

`TimelineRow` is a struct now. It had grown to five positional fields and was
about to grow to seven, at which point nobody can read a call site.
This commit is contained in:
nvrl committed 2026-09-19 15:34:17 +02:00
1 parent c6bdba024f
commit afd71713d5
6 files changed
+554 -111

No files matched your search

+9
View File
@@ -1287,6 +1287,15 @@ pub struct PhotoItem {
/// tags it has not learned yet.
#[serde(default)]
pub favorite: bool,
/// How many files this entry stands for: a shot stored as a RAW and a JPEG
/// is one entry and two files. Older daemons omit it, and `0` reads the same
/// as `1` — one file, nothing to indicate.
#[serde(default)]
pub group_size: u32,
/// Whether one of those files is a camera raw. The tile badges it, because
/// the JPEG it shows is not the whole of what is stored.
#[serde(default)]
pub has_raw: bool,
}
/// A daemon too old to classify a timeline entry is assumed to have served a