feat(core,fuse,gui): Proton theme, photos on the map and an opt-in street map

- The Proton accent preference becomes a whole Proton theme that also
  holds against a GTK theme imported from gtk.css; folders get Proton's
  purple folder icon while it is on.
- Places map markers show one of each town's photos with a count, and
  zoomed in on a town every photo sits where it was taken
  (Request::PlaceLocations). Photos taken on one spot are listed in a
  popover. "Show on map" in the photo viewer goes to the Places map.
- The built-in map draws country borders, sea and land in their own
  colours.
- An opt-in street map from OpenFreeMap (vector tiles drawn with cairo,
  cached on disk up to 256 MB), with the OpenStreetMap credit on the map
  and in About.
- Translations for all catalogs; changelog folded into 2.3.0.
This commit is contained in:
nvrl committed 2026-09-28 03:12:57 +02:00
1 parent 4fd14eb745
commit fde9c2d271
37 files changed
+5846 -2214

No files matched your search

+23
View File
@@ -697,6 +697,29 @@ impl Db {
Ok(rows)
}
/// Every photo taken in a town, one per group as in [`Self::photos_at_place`],
/// with where exactly it was taken. A group's own location is that of the
/// photo standing for it.
pub fn photo_locations_at_place(&self, id: u32) -> Result<Vec<(StoredPhoto, (f64, f64))>> {
let photos = self.photos_at_place(id, 0, usize::MAX >> 1)?;
let located: HashMap<String, (f64, f64)> = {
let conn = self.read();
let mut stmt = conn.prepare(
"SELECT uid, latitude, longitude FROM photos \
WHERE place_id = ?1 AND latitude IS NOT NULL AND longitude IS NOT NULL",
)?;
stmt.query_map(params![id], |r| Ok((r.get(0)?, (r.get(1)?, r.get(2)?))))?
.collect::<rusqlite::Result<_>>()?
};
Ok(photos
.into_iter()
.filter_map(|photo| {
let at = *located.get(&photo.uid)?;
Some((photo, at))
})
.collect())
}
/// Sets of photos with the same content hash, each set ordered with the
/// copy worth keeping first, and the sets newest first.
///
+24
View File
@@ -339,6 +339,30 @@ fn places_group_photos_by_the_nearest_town() {
assert_eq!(berlin, ["b2", "b1-jpeg"]);
}
/// A town's photos come with where each was taken, one per RAW+JPEG shot.
#[test]
fn place_photos_carry_their_own_location() {
let db = Db::open_in_memory().unwrap();
let kreuzberg = (52.497, 13.411);
db.photos_replace(&[
located("b2", 500, "IMG_5.JPG", BERLIN),
located("b1-jpeg", 300, "IMG_3.JPG", kreuzberg),
located("b1-raw", 300, "IMG_3.CR2", kreuzberg),
])
.unwrap();
let id = db.photo_places().unwrap()[0].id;
let located: Vec<(String, (f64, f64))> = db
.photo_locations_at_place(id)
.unwrap()
.into_iter()
.map(|(photo, at)| (photo.uid, at))
.collect();
assert_eq!(
located,
[("b2".to_owned(), BERLIN), ("b1-jpeg".to_owned(), kreuzberg)]
);
}
/// A refresh that skips an already-resolved photo keeps where it was taken; one
/// that reads the node again takes what the node now says.
#[test]