feat(gui): show a grouped shot as one tile, and reach its other files

The grouping landed in the daemon; this is what it looks like. A tile whose shot
was stored as more than one file badges that in the corner — "RAW" when one of
the members is a raw file, since that is the word the person is looking for, and
a plain count otherwise (a live photo, a burst). The lightbox gains a control
that steps through the group's files, which is the only way to reach the RAW now
that the grid shows the shot once.

Deleting says what it will do: the daemon takes the whole group, so the
confirmation names the file count when a selection stands for more files than
tiles.
This commit is contained in:
nvrl committed 2026-09-19 15:39:14 +02:00
1 parent afd71713d5
commit c1df3b6cc2
8 files changed
+283 -5

No files matched your search

+35
View File
@@ -68,6 +68,11 @@ pub enum Request {
#[serde(default)]
favorites: bool,
},
/// Every photo of one photo's group — the JPEG and the RAW of one shot, a
/// live photo and its clip. Replies with [`Response::Photos`], holding one
/// item per file in server order; a photo that stands alone comes back as a
/// single item. The lightbox uses it to switch between a shot's files.
PhotoGroup { uid: String },
/// The months the timeline spans (newest first, with per-month counts) so a
/// front-end can build a date scrubber without paging the whole library.
/// `kind` scopes the counts to one tab when set.
@@ -99,6 +104,10 @@ pub enum Request {
/// part of the FUSE mount: the gallery knows a photo only by its uid, and
/// [`Request::Delete`] has no path to take for it. The nodes land in Proton
/// trash, so this stays recoverable from the Trash page.
///
/// A photo that belongs to a group takes the whole group with it: trashing
/// the JPEG of a shot and leaving its RAW behind is not what anyone means by
/// deleting that photo.
TrashNodes { uids: Vec<String> },
/// Fetch thumbnails for ordinary Drive image files shown outside the Photos
/// timeline. The modification time is the cache validity tag, so replacing
@@ -1956,6 +1965,32 @@ mod tests {
assert!(items[0].name.is_empty());
}
/// The lightbox asks for one shot's files by the uid of the tile that was
/// clicked, and gets them back in the shape the gallery already paints.
#[test]
fn photo_group_requests_roundtrip() {
let request = Request::PhotoGroup {
uid: "volume~first".into(),
};
let line = serde_json::to_string(&request).unwrap();
assert_eq!(line, r#"{"PhotoGroup":{"uid":"volume~first"}}"#);
let decoded: Request = serde_json::from_str(&line).unwrap();
assert_eq!(line, serde_json::to_string(&decoded).unwrap());
}
/// The group fields are new, so a page from a daemon that predates them has
/// to keep painting: one file, nothing to badge.
#[test]
fn an_older_daemons_photo_has_no_group() {
let item: PhotoItem = serde_json::from_str(
r#"{"uid":"volume~first","capture_time":1700000000,"thumb_path":null,
"name":"a.jpg","ratio":1.5,"no_thumb":false}"#,
)
.unwrap();
assert_eq!(item.group_size, 0, "absent reads the same as one file");
assert!(!item.has_raw);
}
/// The gallery trashes by uid, and reports per-uid outcomes back: a partial
/// failure has to survive the wire so the GUI can put those tiles back.
#[test]