mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
feat(core,fuse,gui): show how far photo locations have been read
The first timeline refresh after the Places update reads the metadata of the whole library, and until it is done the Places view has nothing to show and no sign that anything is happening. The refresh now records how many photos it has resolved out of how many, counting photos read by an earlier pass as done. Response::Places carries it as mapping (absent from older daemons, which parse as None). The Places view shows "Mapping photos: 120 of 480" with a bar while it is set, asks again every two seconds, and loads the new places once the pass ends. An empty listing during the pass says the photos are being mapped rather than that there are no places.
This commit is contained in:
1 parent
4ee2aed29b
commit
8749d6c2c6
17 files changed
+2064
-1747
No files matched your search
@@ -1462,6 +1462,14 @@ pub struct PlaceInfo {
|
|||||||
pub cover: PhotoItem,
|
pub cover: PhotoItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many of the timeline's photos a running refresh has read the metadata
|
||||||
|
/// (and so the location) of, out of how many. Part of [`Response::Places`].
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct MappingProgress {
|
||||||
|
pub done: usize,
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
/// One photo in a [`Request::PhotosTimeline`] page.
|
/// One photo in a [`Request::PhotosTimeline`] page.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct PhotoItem {
|
pub struct PhotoItem {
|
||||||
@@ -1823,8 +1831,14 @@ pub enum Response {
|
|||||||
/// The duplicate sets (reply to [`Request::PhotoDuplicates`]), newest
|
/// The duplicate sets (reply to [`Request::PhotoDuplicates`]), newest
|
||||||
/// first. Each set lists the copy worth keeping first.
|
/// first. Each set lists the copy worth keeping first.
|
||||||
PhotoDuplicates { sets: Vec<Vec<PhotoItem>> },
|
PhotoDuplicates { sets: Vec<Vec<PhotoItem>> },
|
||||||
/// The towns the photos were taken in (reply to [`Request::PhotoPlaces`]).
|
/// The towns the photos were taken in (reply to [`Request::PhotoPlaces`]),
|
||||||
Places { items: Vec<PlaceInfo> },
|
/// and how far a running timeline refresh has got reading photo locations
|
||||||
|
/// — `None` when none is reading any.
|
||||||
|
Places {
|
||||||
|
items: Vec<PlaceInfo>,
|
||||||
|
#[serde(default)]
|
||||||
|
mapping: Option<MappingProgress>,
|
||||||
|
},
|
||||||
/// Thumbnails for a [`Request::PhotoThumbs`] batch.
|
/// Thumbnails for a [`Request::PhotoThumbs`] batch.
|
||||||
Thumbs { items: Vec<PhotoThumb> },
|
Thumbs { items: Vec<PhotoThumb> },
|
||||||
/// Per-node outcome of a [`Request::TrashNodes`] batch. Reported per uid
|
/// Per-node outcome of a [`Request::TrashNodes`] batch. Reported per uid
|
||||||
@@ -2537,12 +2551,33 @@ mod tests {
|
|||||||
|
|
||||||
let line = r#"{"Places":{"items":[{"id":2950159,"name":"Berlin","country":"DE","photo_count":2,
|
let line = r#"{"Places":{"items":[{"id":2950159,"name":"Berlin","country":"DE","photo_count":2,
|
||||||
"cover":{"uid":"vol~b2","capture_time":1700000000,"thumb_path":null,"name":"b2.jpg","ratio":null,"no_thumb":false}}]}}"#;
|
"cover":{"uid":"vol~b2","capture_time":1700000000,"thumb_path":null,"name":"b2.jpg","ratio":null,"no_thumb":false}}]}}"#;
|
||||||
let Response::Places { items } = serde_json::from_str(line).unwrap() else {
|
let Response::Places { items, mapping } = serde_json::from_str(line).unwrap() else {
|
||||||
panic!("not a place listing");
|
panic!("not a place listing");
|
||||||
};
|
};
|
||||||
assert_eq!(items.len(), 1);
|
assert_eq!(items.len(), 1);
|
||||||
assert_eq!((items[0].id, items[0].country.as_str()), (2950159, "DE"));
|
assert_eq!((items[0].id, items[0].country.as_str()), (2950159, "DE"));
|
||||||
assert_eq!(items[0].cover.uid, "vol~b2");
|
assert_eq!(items[0].cover.uid, "vol~b2");
|
||||||
|
// A daemon from before the progress field still parses: no mapping.
|
||||||
|
assert_eq!(mapping, None);
|
||||||
|
|
||||||
|
let reply = Response::Places {
|
||||||
|
items: Vec::new(),
|
||||||
|
mapping: Some(MappingProgress {
|
||||||
|
done: 120,
|
||||||
|
total: 480,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let line = serde_json::to_string(&reply).unwrap();
|
||||||
|
let Response::Places { mapping, .. } = serde_json::from_str(&line).unwrap() else {
|
||||||
|
panic!("not a place listing");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
mapping,
|
||||||
|
Some(MappingProgress {
|
||||||
|
done: 120,
|
||||||
|
total: 480
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sharing and devices requests must survive the same line-delimited JSON
|
/// The sharing and devices requests must survive the same line-delimited JSON
|
||||||
|
|||||||
@@ -466,7 +466,10 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
},
|
},
|
||||||
Ok(CtlRequest::PhotoPlaces) => match core.photo_places() {
|
Ok(CtlRequest::PhotoPlaces) => match core.photo_places() {
|
||||||
Ok(items) => CtlResponse::Places { items },
|
Ok(items) => CtlResponse::Places {
|
||||||
|
items,
|
||||||
|
mapping: core.timeline_progress(),
|
||||||
|
},
|
||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
},
|
},
|
||||||
Ok(CtlRequest::PlacePhotos { id, offset, limit }) => {
|
Ok(CtlRequest::PlacePhotos { id, offset, limit }) => {
|
||||||
|
|||||||
@@ -550,6 +550,10 @@ struct Core {
|
|||||||
/// already running, so a burst of page requests against a stale listing kicks
|
/// already running, so a burst of page requests against a stale listing kicks
|
||||||
/// off one refresh rather than one per request.
|
/// off one refresh rather than one per request.
|
||||||
timeline_refreshing: Arc<AtomicBool>,
|
timeline_refreshing: Arc<AtomicBool>,
|
||||||
|
/// How far the running timeline refresh has got resolving photo metadata,
|
||||||
|
/// as `(done, total)` photos; `None` when no refresh is resolving any. The
|
||||||
|
/// Places view shows it while a library's locations are first read.
|
||||||
|
timeline_progress: Arc<Mutex<Option<(usize, usize)>>>,
|
||||||
/// The same, for the album listing.
|
/// The same, for the album listing.
|
||||||
albums_refreshing: Arc<AtomicBool>,
|
albums_refreshing: Arc<AtomicBool>,
|
||||||
trash_refreshing: Arc<AtomicBool>,
|
trash_refreshing: Arc<AtomicBool>,
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ pub fn mount(
|
|||||||
upload_times: Arc::new(Mutex::new(HashMap::new())),
|
upload_times: Arc::new(Mutex::new(HashMap::new())),
|
||||||
upload_cancel: Arc::new(Mutex::new(HashMap::new())),
|
upload_cancel: Arc::new(Mutex::new(HashMap::new())),
|
||||||
timeline_refreshing: Arc::new(AtomicBool::new(false)),
|
timeline_refreshing: Arc::new(AtomicBool::new(false)),
|
||||||
|
timeline_progress: Arc::new(Mutex::new(None)),
|
||||||
albums_refreshing: Arc::new(AtomicBool::new(false)),
|
albums_refreshing: Arc::new(AtomicBool::new(false)),
|
||||||
trash_refreshing: Arc::new(AtomicBool::new(false)),
|
trash_refreshing: Arc::new(AtomicBool::new(false)),
|
||||||
trash_progress: Arc::new(tokio::sync::Notify::new()),
|
trash_progress: Arc::new(tokio::sync::Notify::new()),
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use pdfs_core::control::{
|
use pdfs_core::control::{
|
||||||
FileThumbRequest, PhotoItem, PhotoKind, PhotoThumb, PlaceInfo, ThumbnailBuildStatus,
|
FileThumbRequest, MappingProgress, PhotoItem, PhotoKind, PhotoThumb, PlaceInfo,
|
||||||
is_raw_image_name, is_thumbnail_image_name,
|
ThumbnailBuildStatus, is_raw_image_name, is_thumbnail_image_name,
|
||||||
};
|
};
|
||||||
use pdfs_core::db::{self, StoredPhoto};
|
use pdfs_core::db::{self, StoredPhoto};
|
||||||
use pdfs_core::{CoreError, CoreResult};
|
use pdfs_core::{CoreError, CoreResult};
|
||||||
@@ -601,6 +601,16 @@ fn extract_raw_preview(
|
|||||||
pub(crate) type TrashOutcome = (Vec<String>, Vec<(String, String)>);
|
pub(crate) type TrashOutcome = (Vec<String>, Vec<(String, String)>);
|
||||||
|
|
||||||
/// Formats supported by the local thumbnail decoder and exposed by the GUI.
|
/// Formats supported by the local thumbnail decoder and exposed by the GUI.
|
||||||
|
/// Clears the timeline refresh's progress when dropped, so an error or a
|
||||||
|
/// cancelled refresh doesn't leave a bar standing.
|
||||||
|
struct ProgressGuard<'a>(&'a parking_lot::Mutex<Option<(usize, usize)>>);
|
||||||
|
|
||||||
|
impl Drop for ProgressGuard<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
*self.0.lock() = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What a timeline enrichment pass resolves for one photo, beyond what the
|
/// What a timeline enrichment pass resolves for one photo, beyond what the
|
||||||
/// timeline listing already carries.
|
/// timeline listing already carries.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -1768,7 +1778,14 @@ impl Core {
|
|||||||
// can land in different chunks, and one of them may not resolve at all.
|
// can land in different chunks, and one of them may not resolve at all.
|
||||||
let mut main_of: HashMap<String, String> = HashMap::new();
|
let mut main_of: HashMap<String, String> = HashMap::new();
|
||||||
let mut stopped = false;
|
let mut stopped = false;
|
||||||
for chunk in uids.chunks(TIMELINE_ENRICH_CHUNK) {
|
// Photos read before count as done, so the bar picks up where an
|
||||||
|
// interrupted pass left off. Cleared however the pass ends.
|
||||||
|
let skipped = items.len() - uids.len();
|
||||||
|
let progress = ProgressGuard(&self.timeline_progress);
|
||||||
|
if !uids.is_empty() {
|
||||||
|
*progress.0.lock() = Some((skipped, items.len()));
|
||||||
|
}
|
||||||
|
for (index, chunk) in uids.chunks(TIMELINE_ENRICH_CHUNK).enumerate() {
|
||||||
// Teardown cancels the runtime under this loop, and every chunk
|
// Teardown cancels the runtime under this loop, and every chunk
|
||||||
// still in flight then fails its decrypt task — a burst of
|
// still in flight then fails its decrypt task — a burst of
|
||||||
// "skipping undecryptable photo ... task was cancelled" warnings
|
// "skipping undecryptable photo ... task was cancelled" warnings
|
||||||
@@ -1822,7 +1839,10 @@ impl Core {
|
|||||||
}
|
}
|
||||||
Err(e) => warn!(error = %e, "resolving photo metadata for a timeline chunk failed"),
|
Err(e) => warn!(error = %e, "resolving photo metadata for a timeline chunk failed"),
|
||||||
}
|
}
|
||||||
|
let done = (skipped + (index + 1) * TIMELINE_ENRICH_CHUNK).min(items.len());
|
||||||
|
*progress.0.lock() = Some((done, items.len()));
|
||||||
}
|
}
|
||||||
|
drop(progress);
|
||||||
|
|
||||||
let rows: Vec<db::TimelineRow> = items
|
let rows: Vec<db::TimelineRow> = items
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1884,6 +1904,14 @@ impl Core {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far the running timeline refresh has got reading photo metadata, or
|
||||||
|
/// `None` when no refresh is reading any.
|
||||||
|
pub(crate) fn timeline_progress(&self) -> Option<MappingProgress> {
|
||||||
|
self.timeline_progress
|
||||||
|
.lock()
|
||||||
|
.map(|(done, total)| MappingProgress { done, total })
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a timeline refresh is running. A front-end that asked for one
|
/// Whether a timeline refresh is running. A front-end that asked for one
|
||||||
/// watches this to know when the new timeline is worth re-reading.
|
/// watches this to know when the new timeline is worth re-reading.
|
||||||
pub(crate) fn timeline_refreshing(&self) -> bool {
|
pub(crate) fn timeline_refreshing(&self) -> bool {
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ use pdfs_core::config::{AppDirs, FileSort, FilesView};
|
|||||||
|
|
||||||
use pdfs_core::control::{
|
use pdfs_core::control::{
|
||||||
ActivityEntry, ActivityKind, AlbumInfo, BookmarkInfo, ConflictInfo, ConflictKeep, DeviceInfo,
|
ActivityEntry, ActivityKind, AlbumInfo, BookmarkInfo, ConflictInfo, ConflictKeep, DeviceInfo,
|
||||||
DirEntry, ErrorKind, ImportSummary, InvitationInfo, JobItem, PendingOpInfo, PhotoItem,
|
DirEntry, ErrorKind, ImportSummary, InvitationInfo, JobItem, MappingProgress, PendingOpInfo,
|
||||||
PhotoKind, PhotoMonth, PlaceInfo, PublicLinkInfo, RefreshScope, Request, Response,
|
PhotoItem, PhotoKind, PhotoMonth, PlaceInfo, PublicLinkInfo, RefreshScope, Request, Response,
|
||||||
RestorableFolder, RestoreItem, RevisionInfo, SearchHit, ShareEntry, ShareEntryKind, SharedItem,
|
RestorableFolder, RestoreItem, RevisionInfo, SearchHit, ShareEntry, ShareEntryKind, SharedItem,
|
||||||
SyncFolderInfo, SyncPhase, SyncProgress, ThumbnailBuildStatus, TransferDirection, TransferItem,
|
SyncFolderInfo, SyncPhase, SyncProgress, ThumbnailBuildStatus, TransferDirection, TransferItem,
|
||||||
send,
|
send,
|
||||||
@@ -558,6 +558,10 @@ fn build_window(app: &adw::Application) {
|
|||||||
places_status: gallery_widgets.places_status.clone(),
|
places_status: gallery_widgets.places_status.clone(),
|
||||||
places_btn: gallery_widgets.places_btn.clone(),
|
places_btn: gallery_widgets.places_btn.clone(),
|
||||||
places_loading: Cell::new(false),
|
places_loading: Cell::new(false),
|
||||||
|
places_mapping: gallery_widgets.places_mapping.clone(),
|
||||||
|
places_mapping_label: gallery_widgets.places_mapping_label.clone(),
|
||||||
|
places_mapping_bar: gallery_widgets.places_mapping_bar.clone(),
|
||||||
|
places_poll: RefCell::new(None),
|
||||||
place_count: Cell::new(0),
|
place_count: Cell::new(0),
|
||||||
place: RefCell::new(None),
|
place: RefCell::new(None),
|
||||||
album: RefCell::new(None),
|
album: RefCell::new(None),
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ pub(crate) struct GalleryState {
|
|||||||
pub(crate) places_btn: gtk4::ToggleButton,
|
pub(crate) places_btn: gtk4::ToggleButton,
|
||||||
/// True while the place listing is in flight.
|
/// True while the place listing is in flight.
|
||||||
pub(crate) places_loading: Cell<bool>,
|
pub(crate) places_loading: Cell<bool>,
|
||||||
|
/// The row above the place grid saying how far the daemon has got reading
|
||||||
|
/// photo locations, its label and bar, and the timer that asks again while
|
||||||
|
/// it is reading.
|
||||||
|
pub(crate) places_mapping: gtk4::Box,
|
||||||
|
pub(crate) places_mapping_label: gtk4::Label,
|
||||||
|
pub(crate) places_mapping_bar: gtk4::ProgressBar,
|
||||||
|
pub(crate) places_poll: RefCell<Option<glib::SourceId>>,
|
||||||
/// How many places the grid shows, for the subtitle when back returns there.
|
/// How many places the grid shows, for the subtitle when back returns there.
|
||||||
pub(crate) place_count: Cell<usize>,
|
pub(crate) place_count: Cell<usize>,
|
||||||
/// The place currently open, paged by [`load_gallery`] like an album.
|
/// The place currently open, paged by [`load_gallery`] like an album.
|
||||||
@@ -416,6 +423,9 @@ pub(crate) struct GalleryWidgets {
|
|||||||
pub(crate) places_stack: gtk4::Stack,
|
pub(crate) places_stack: gtk4::Stack,
|
||||||
pub(crate) places_status: adw::StatusPage,
|
pub(crate) places_status: adw::StatusPage,
|
||||||
pub(crate) places_btn: gtk4::ToggleButton,
|
pub(crate) places_btn: gtk4::ToggleButton,
|
||||||
|
pub(crate) places_mapping: gtk4::Box,
|
||||||
|
pub(crate) places_mapping_label: gtk4::Label,
|
||||||
|
pub(crate) places_mapping_bar: gtk4::ProgressBar,
|
||||||
pub(crate) back: gtk4::Button,
|
pub(crate) back: gtk4::Button,
|
||||||
/// The kind toggles and date jump, as one box so an album view can hide them.
|
/// The kind toggles and date jump, as one box so an album view can hide them.
|
||||||
pub(crate) filters: gtk4::Box,
|
pub(crate) filters: gtk4::Box,
|
||||||
@@ -817,6 +827,21 @@ pub(crate) fn build_gallery_page() -> (gtk4::Widget, GalleryWidgets) {
|
|||||||
places_stack.set_vexpand(true);
|
places_stack.set_vexpand(true);
|
||||||
places_stack.add_named(&places_scroll, Some("grid"));
|
places_stack.add_named(&places_scroll, Some("grid"));
|
||||||
places_stack.add_named(&places_status, Some("status"));
|
places_stack.add_named(&places_status, Some("status"));
|
||||||
|
// While a refresh reads the library's locations (the first one after an
|
||||||
|
// update reads all of them), a bar says how far it has got.
|
||||||
|
let places_mapping_label = gtk4::Label::new(None);
|
||||||
|
places_mapping_label.add_css_class("dim-label");
|
||||||
|
let places_mapping_bar = gtk4::ProgressBar::builder()
|
||||||
|
.hexpand(true)
|
||||||
|
.valign(gtk4::Align::Center)
|
||||||
|
.build();
|
||||||
|
let places_mapping = gtk4::Box::new(gtk4::Orientation::Horizontal, 10);
|
||||||
|
places_mapping.append(&places_mapping_label);
|
||||||
|
places_mapping.append(&places_mapping_bar);
|
||||||
|
places_mapping.set_visible(false);
|
||||||
|
let places_page = gtk4::Box::new(gtk4::Orientation::Vertical, 12);
|
||||||
|
places_page.append(&places_mapping);
|
||||||
|
places_page.append(&places_stack);
|
||||||
|
|
||||||
let content = gtk4::Stack::new();
|
let content = gtk4::Stack::new();
|
||||||
content.set_vexpand(true);
|
content.set_vexpand(true);
|
||||||
@@ -824,7 +849,7 @@ pub(crate) fn build_gallery_page() -> (gtk4::Widget, GalleryWidgets) {
|
|||||||
content.add_named(&timeline, Some("timeline"));
|
content.add_named(&timeline, Some("timeline"));
|
||||||
content.add_named(&status, Some("status"));
|
content.add_named(&status, Some("status"));
|
||||||
content.add_named(&albums_page, Some("albums"));
|
content.add_named(&albums_page, Some("albums"));
|
||||||
content.add_named(&places_stack, Some("places"));
|
content.add_named(&places_page, Some("places"));
|
||||||
|
|
||||||
let import_banner = adw::Banner::builder()
|
let import_banner = adw::Banner::builder()
|
||||||
.title(gettext("Importing from Google Photos…"))
|
.title(gettext("Importing from Google Photos…"))
|
||||||
@@ -898,6 +923,9 @@ pub(crate) fn build_gallery_page() -> (gtk4::Widget, GalleryWidgets) {
|
|||||||
places_stack,
|
places_stack,
|
||||||
places_status,
|
places_status,
|
||||||
places_btn,
|
places_btn,
|
||||||
|
places_mapping,
|
||||||
|
places_mapping_label,
|
||||||
|
places_mapping_bar,
|
||||||
back,
|
back,
|
||||||
filters,
|
filters,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ use crate::*;
|
|||||||
/// Edge length in px of a place's cover, the same as an album's.
|
/// Edge length in px of a place's cover, the same as an album's.
|
||||||
const COVER_EDGE: i32 = 200;
|
const COVER_EDGE: i32 = 200;
|
||||||
|
|
||||||
|
/// How often the listing is asked for again while photo locations are read.
|
||||||
|
const MAPPING_POLL: std::time::Duration = std::time::Duration::from_secs(2);
|
||||||
|
|
||||||
/// Show the Places grid and (re)load it. Called when the Places toggle goes on.
|
/// Show the Places grid and (re)load it. Called when the Places toggle goes on.
|
||||||
pub(crate) fn show_places(ui: &Rc<Ui>) {
|
pub(crate) fn show_places(ui: &Rc<Ui>) {
|
||||||
show_place_grid(ui);
|
show_place_grid(ui);
|
||||||
@@ -49,8 +52,22 @@ pub(crate) fn load_places(ui: &Rc<Ui>) {
|
|||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
let result = rx.recv().await;
|
let result = rx.recv().await;
|
||||||
ui.gallery.places_loading.set(false);
|
ui.gallery.places_loading.set(false);
|
||||||
|
let mapping = match &result {
|
||||||
|
Ok(Ok(Response::Places { mapping, .. })) => *mapping,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
sync_mapping(&ui, mapping);
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(Response::Places { items })) if items.is_empty() => {
|
Ok(Ok(Response::Places { items, .. })) if items.is_empty() && mapping.is_some() => {
|
||||||
|
ui.gallery.place_count.set(0);
|
||||||
|
places_status(
|
||||||
|
&ui,
|
||||||
|
"mark-location-symbolic",
|
||||||
|
&gettext("Mapping your photos…"),
|
||||||
|
&gettext("Places show up here once the locations of your photos are read."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(Ok(Response::Places { items, .. })) if items.is_empty() => {
|
||||||
ui.gallery.place_count.set(0);
|
ui.gallery.place_count.set(0);
|
||||||
if ui.gallery.place.borrow().is_none() {
|
if ui.gallery.place.borrow().is_none() {
|
||||||
ui.gallery.title.set_subtitle("");
|
ui.gallery.title.set_subtitle("");
|
||||||
@@ -64,7 +81,7 @@ pub(crate) fn load_places(ui: &Rc<Ui>) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Ok(Ok(Response::Places { items })) => {
|
Ok(Ok(Response::Places { items, .. })) => {
|
||||||
fill_places(&ui, &items);
|
fill_places(&ui, &items);
|
||||||
ui.gallery.places_stack.set_visible_child_name("grid");
|
ui.gallery.places_stack.set_visible_child_name("grid");
|
||||||
}
|
}
|
||||||
@@ -87,6 +104,37 @@ pub(crate) fn load_places(ui: &Rc<Ui>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show how far the daemon has got reading photo locations, and ask again in a
|
||||||
|
/// moment while it is still reading — the new places land once it is done.
|
||||||
|
fn sync_mapping(ui: &Rc<Ui>, mapping: Option<MappingProgress>) {
|
||||||
|
if let Some(source) = ui.gallery.places_poll.take() {
|
||||||
|
source.remove();
|
||||||
|
}
|
||||||
|
ui.gallery.places_mapping.set_visible(mapping.is_some());
|
||||||
|
let Some(MappingProgress { done, total }) = mapping else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
ui.gallery.places_mapping_label.set_label(&gettext_f(
|
||||||
|
// Translators: {done} and {total} are numbers of photos.
|
||||||
|
"Mapping photos: {done} of {total}",
|
||||||
|
&[("done", &thousands(done)), ("total", &thousands(total))],
|
||||||
|
));
|
||||||
|
ui.gallery.places_mapping_bar.set_fraction(if total == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
done as f64 / total as f64
|
||||||
|
});
|
||||||
|
let ui_poll = ui.clone();
|
||||||
|
let source = glib::timeout_add_local_once(MAPPING_POLL, move || {
|
||||||
|
ui_poll.gallery.places_poll.take();
|
||||||
|
let on_places = ui_poll.gallery.content.visible_child_name().as_deref() == Some("places");
|
||||||
|
if on_places && ui_poll.gallery.place.borrow().is_none() {
|
||||||
|
load_places(&ui_poll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.gallery.places_poll.replace(Some(source));
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the grid's cards with `places`, the one with the most photos first.
|
/// Replace the grid's cards with `places`, the one with the most photos first.
|
||||||
fn fill_places(ui: &Rc<Ui>, places: &[PlaceInfo]) {
|
fn fill_places(ui: &Rc<Ui>, places: &[PlaceInfo]) {
|
||||||
let scroll = ui
|
let scroll = ui
|
||||||
|
|||||||
+212
-193
File diff suppressed because it is too large.
Load diff
+205
-191
File diff suppressed because it is too large.
Load diff
+212
-193
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user