From 6be8a8f9bc0d09d8d177cd62cda466ee31fb5633 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 14 Nov 2026 12:43:56 +0100 Subject: [PATCH 1/2] fix(gui): keep the user's place when navigating A folder opened in My files, Shared with me or Computers kept the scroll offset of the folder before it, because the rows are swapped in place to keep a refresh steady. Another folder, a new search and a fresh Photos load (filter, album, place) now start at the top; a refresh still keeps its place. Closing a photo opened from the Places map returns to the map instead of leaving the town's timeline behind, and the pending open is dropped when the user leaves the town before it loads. load_gallery no longer swaps Albums or Places for the timeline when a page lands late (viewer paging, upload, trash Undo); the timeline is marked stale and read when the user switches back. Coming back to Photos reuses the timeline unless it is stale, and the timeline returns filtered the way the kind tabs and date show after an album or place. A search in My files is no longer replaced by the folder listing on file changes, F5 or returning to the page. A favorite set in the viewer shows on its tile at once. --- crates/pdfs-gui/src/app/events.rs | 14 ++++++- crates/pdfs-gui/src/app/main.rs | 17 +++++++-- crates/pdfs-gui/src/app/pages/albums.rs | 21 ++++++++-- crates/pdfs-gui/src/app/pages/browser.rs | 23 ++++++++++- crates/pdfs-gui/src/app/pages/devices.rs | 5 +++ crates/pdfs-gui/src/app/pages/photo_viewer.rs | 9 ++++- crates/pdfs-gui/src/app/pages/photos.rs | 37 +++++++++++++++++- crates/pdfs-gui/src/app/pages/places.rs | 38 +++++++++++++++---- crates/pdfs-gui/src/app/pages/shared.rs | 6 ++- crates/pdfs-gui/src/app/widgets/file_list.rs | 13 +++++++ docs/CHANGELOG.md | 16 ++++++++ 11 files changed, 178 insertions(+), 21 deletions(-) diff --git a/crates/pdfs-gui/src/app/events.rs b/crates/pdfs-gui/src/app/events.rs index e951b1d..6302390 100644 --- a/crates/pdfs-gui/src/app/events.rs +++ b/crates/pdfs-gui/src/app/events.rs @@ -128,8 +128,18 @@ fn follow_changes(ui: &Rc, state: &FeedState, topics: &[Topic]) { if changed(Topic::Shares) { refresh_invitations_badge(ui, Duration::ZERO); } - match ui.stack.visible_child_name().as_deref() { - Some("browser") if changed(Topic::Files) => load_browser(ui), + let page = ui.stack.visible_child_name(); + // A timeline out of sight is read again when the user comes back to it; + // one on screen is left alone (below). + if changed(Topic::Photos) && (page.as_deref() != Some("gallery") || !photo_grid_shown(ui)) { + ui.gallery.timeline_stale.set(true); + } + match page.as_deref() { + // A search on screen is left alone: reloading would swap its hits for + // the folder's listing while the query stays in the box. + Some("browser") if changed(Topic::Files) && ui.browser.search.text().trim().is_empty() => { + load_browser(ui) + } // The timeline is left alone: reloading it would throw away the place // the user scrolled to. F5 reloads it on request. Some("gallery") diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 1678368..ba41ea3 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -726,17 +726,26 @@ fn build_window(app: &adw::Application) { cancel_file_thumbnails(&ui_nav); sync_sidebar(&ui_nav); match st.visible_child_name().as_deref() { - Some("browser") => load_browser(&ui_nav), + Some("browser") => reload_browser_view(&ui_nav), // The Albums and Places listings are re-read in place. Loading the // timeline instead would swap it in under the Albums or Places toggle. + // The timeline, an album or a place comes back as it was left, + // scrolled where it was, unless photos changed while it was out of + // sight (see `follow_changes`) or nothing is loaded yet. Some("gallery") if !ui_nav.gallery.in_collection() => { match ui_nav.gallery.view_switch.active() { VIEW_ALBUMS => load_albums(&ui_nav), VIEW_PLACES => load_places(&ui_nav), - _ => load_gallery(&ui_nav, false), + _ if ui_nav.gallery.timeline_stale.replace(false) + || ui_nav.gallery.model.n_items() == 0 => + { + load_gallery(&ui_nav, false) + } + _ => {} } } - Some("gallery") => load_gallery(&ui_nav, false), + Some("gallery") if ui_nav.gallery.model.n_items() == 0 => load_gallery(&ui_nav, false), + Some("gallery") => {} // Every visit reads the page again. The rows already on screen stay // up while it does, so arriving costs no "Loading…" flash. Some("sharedbyme") => load_shared_by_me(&ui_nav), @@ -1066,7 +1075,7 @@ fn reload_current_page(ui: &Rc) { match ui.stack.visible_child_name().as_deref() { Some("browser") => { let path = ui.browser.path.borrow().clone(); - refresh_then(ui, RefreshScope::Dir { path }, load_browser); + refresh_then(ui, RefreshScope::Dir { path }, reload_browser_view); } // One scope covers the whole photos view, so Refresh reloads whichever of // the views — a grid, or the timeline, an album or a place — is on screen. diff --git a/crates/pdfs-gui/src/app/pages/albums.rs b/crates/pdfs-gui/src/app/pages/albums.rs index 6af6ffd..96511d6 100644 --- a/crates/pdfs-gui/src/app/pages/albums.rs +++ b/crates/pdfs-gui/src/app/pages/albums.rs @@ -664,12 +664,25 @@ pub(crate) fn wire_albums(ui: &Rc) { } close_album(&ui_photos); close_place(&ui_photos); - ui_photos.gallery.filters.set_visible(true); - ui_photos.gallery.content.set_visible_child_name("timeline"); + let gallery = &ui_photos.gallery; + gallery.filters.set_visible(true); + gallery.content.set_visible_child_name("timeline"); + // An album or a place cleared the kind and the month it does not use; + // the filter controls still show them, so the timeline comes back + // filtered the way they say. + let kind = kind_for_tab(gallery.tabs.active() as usize); + let range = gallery + .date_ranges + .borrow() + .get(gallery.dates.selected() as usize) + .copied() + .flatten(); + let kind_changed = gallery.kind.replace(kind) != kind; + let range_changed = gallery.range.replace(range) != range; + let refiltered = kind_changed || range_changed; // The timeline comes back as it was left unless an album took over the // model since; then it has to be reloaded. - if ui_photos.gallery.timeline_stale.replace(false) || ui_photos.gallery.model.n_items() == 0 - { + if gallery.timeline_stale.replace(false) || refiltered || gallery.model.n_items() == 0 { load_gallery(&ui_photos, false); } else { update_gallery_subtitle(&ui_photos); diff --git a/crates/pdfs-gui/src/app/pages/browser.rs b/crates/pdfs-gui/src/app/pages/browser.rs index a3d46a0..b6fb218 100644 --- a/crates/pdfs-gui/src/app/pages/browser.rs +++ b/crates/pdfs-gui/src/app/pages/browser.rs @@ -2921,6 +2921,18 @@ pub(crate) fn load_browser(ui: &Rc) { }); } +/// Read again what My files shows: the search hits while there is a query, +/// the folder otherwise. [`load_browser`] alone would swap a search's hits +/// for the folder while the query stays in the box. +pub(crate) fn reload_browser_view(ui: &Rc) { + let query = ui.browser.search.text().trim().to_string(); + if query.is_empty() { + load_browser(ui); + } else { + run_search(ui, &query); + } +} + /// Clear the model and show the daemon's error on the status page. Used for /// in-band failures (a bad path, a permission error) — the mount is up, so Retry /// (which restarts the service) wouldn't help and isn't offered. @@ -2959,7 +2971,10 @@ pub(crate) fn browser_unreachable(ui: &Rc) { pub(crate) fn repaint_browser(ui: &Rc, entries: &[DirEntry]) { show_location_column(ui, false); *ui.browser.listing.borrow_mut() = entries.to_vec(); - *ui.browser.listed.borrow_mut() = Some(ui.browser.path.borrow().clone()); + // `listed` still names the folder the rows belong to only on a refresh; + // any other load forgot it (see `load_browser`). + let path = ui.browser.path.borrow().clone(); + let another_folder = ui.browser.listed.replace(Some(path.clone())).as_ref() != Some(&path); ui.browser.summary.set_label(&listing_summary( entries.iter().filter(|entry| !entry.is_dir).count(), entries.iter().filter(|entry| entry.is_dir).count(), @@ -2988,6 +3003,9 @@ pub(crate) fn repaint_browser(ui: &Rc, entries: &[DirEntry]) { // Only the rows that changed are swapped, so a refresh keeps the // selection, the scroll position and the thumbnails already drawn. replace_items(&ui.browser.files.model, &sorted); + if another_folder { + ui.browser.files.scroll_to_top(); + } } /// Forget which folder the rows belong to and drop the selection, before the @@ -3166,6 +3184,9 @@ pub(crate) fn repaint_search(ui: &Rc, hits: &[SearchHit]) { .collect(); sort_entries(&mut entries, ui.browser.view.get()); replace_items(&ui.browser.files.model, &entries); + // Every search starts over (`run_search` forgets the listing), so its hits + // read from the first one. + ui.browser.files.scroll_to_top(); } fn listing_summary(files: usize, folders: usize, file_bytes: u64) -> String { diff --git a/crates/pdfs-gui/src/app/pages/devices.rs b/crates/pdfs-gui/src/app/pages/devices.rs index 4956266..cbf4286 100644 --- a/crates/pdfs-gui/src/app/pages/devices.rs +++ b/crates/pdfs-gui/src/app/pages/devices.rs @@ -297,6 +297,7 @@ fn load_device_folder(ui: &Rc, nav: &[(String, String)]) { }; ui.devices.content.set_visible_child_name("browse"); repaint_device_crumb(&ui, &nav); + let another_folder = ui.devices.listed.borrow().as_ref() != Some(&nav); *ui.devices.listed.borrow_mut() = Some(nav); if entries.is_empty() { ui.devices.files.model.remove_all(); @@ -304,6 +305,10 @@ fn load_device_folder(ui: &Rc, nav: &[(String, String)]) { } else { ui.devices.files_content.set_visible_child_name("list"); replace_items(&ui.devices.files.model, &entries); + // A refresh keeps its place; another folder starts at the top. + if another_folder { + ui.devices.files.scroll_to_top(); + } } }); } diff --git a/crates/pdfs-gui/src/app/pages/photo_viewer.rs b/crates/pdfs-gui/src/app/pages/photo_viewer.rs index 7866633..c4d507d 100644 --- a/crates/pdfs-gui/src/app/pages/photo_viewer.rs +++ b/crates/pdfs-gui/src/app/pages/photo_viewer.rs @@ -1001,7 +1001,10 @@ fn show_photo_position(ui: &Rc, viewer: &Rc, index: u32, capture_tim /// /// Closing it is deliberately hard to get wrong — Escape, `q`, Ctrl+W, the close /// button, or a click on the backdrop beside the photo all dismiss it. -pub(crate) fn open_photo_viewer(ui: &Rc, initial_uid: String) { +/// +/// Returns the lightbox's window, for a caller that has to put the page +/// behind it back once it closes. +pub(crate) fn open_photo_viewer(ui: &Rc, initial_uid: String) -> gtk4::Window { let parent = ui.stack.root().and_downcast::().unwrap(); // An `adw::Window` for its breakpoints: the details panel lies over the @@ -1510,6 +1513,9 @@ pub(crate) fn open_photo_viewer(ui: &Rc, initial_uid: String) { let failed = match rx.recv().await { Ok(Ok(Response::Ok { .. })) => { set_gallery_favorite(&ui_result, &uid, favorite); + // The tiles hold their own copy of the photo, so the star + // shows on the grid only once it is laid out again. + repaint_gallery(&ui_result); None } Ok(Ok(Response::Error { message, .. })) => Some(message), @@ -1718,6 +1724,7 @@ pub(crate) fn open_photo_viewer(ui: &Rc, initial_uid: String) { window.add_controller(key_controller); window.present(); + window } /// Whether `(x, y)` — in `picture`'s coordinates — lands on the photo itself diff --git a/crates/pdfs-gui/src/app/pages/photos.rs b/crates/pdfs-gui/src/app/pages/photos.rs index 09c74b5..dd3c76a 100644 --- a/crates/pdfs-gui/src/app/pages/photos.rs +++ b/crates/pdfs-gui/src/app/pages/photos.rs @@ -3952,7 +3952,9 @@ fn show_photo_menu(ui: &Rc, photo: &PhotoItem, anchor: >k4::Button, x: f64 } else { pgettext("verb", "Open") }; - menu.item(&label, move || open_photo_viewer(&ui_c, uid.clone())); + menu.item(&label, move || { + open_photo_viewer(&ui_c, uid.clone()); + }); let (ui_c, uid) = (ui.clone(), photo.uid.clone()); // Translators: a check item in a photo's menu, on while the photo is a favorite. let favorite_label = pgettext("state", "Favorite"); @@ -4020,8 +4022,21 @@ fn set_photo_favorite(ui: &Rc, uid: String, favorite: bool) { }); } +/// Whether the photo grid is the view the Photos page shows: an album or a +/// place, or the timeline itself. Under Albums or Places it is out of sight. +pub(crate) fn photo_grid_shown(ui: &Ui) -> bool { + ui.gallery.in_collection() || ui.gallery.view_switch.active() == VIEW_TIMELINE +} + pub(crate) fn load_gallery(ui: &Rc, append: bool) { sync_filter_badge(ui); + // A load would put the grid, or its status page, on screen in place of + // the Albums or Places view the user is looking at. Leave the timeline to + // be read again when they switch back to it. + if !photo_grid_shown(ui) { + ui.gallery.timeline_stale.set(true); + return; + } if ui.gallery.loading.get() { // A filter flipped while a page is on its way: that page answers the // old question, so fetch again once it lands. More pages can wait. @@ -4098,6 +4113,12 @@ pub(crate) fn load_gallery(ui: &Rc, append: bool) { if !waiters.is_empty() { glib::idle_add_local_once(move || waiters.into_iter().for_each(|waiter| waiter())); } + // The user left for Albums or Places while the page was on its way; + // painting it now would switch the view under them. + if !photo_grid_shown(&ui) { + ui.gallery.timeline_stale.set(true); + return; + } match result { Ok(Ok(Response::Photos { available, @@ -4138,6 +4159,20 @@ pub(crate) fn load_gallery(ui: &Rc, append: bool) { ui.gallery.model.append(&BoxedAnyObject::new(item.clone())); } repaint_gallery(&ui); + // The rows are swapped in place, which keeps the old scroll + // offset; a fresh load (another kind, month, album or place) + // reads from its newest photo. + if !append + && ui + .gallery + .list + .model() + .is_some_and(|rows| rows.n_items() > 0) + { + ui.gallery + .list + .scroll_to(0, gtk4::ListScrollFlags::NONE, None); + } if ui.gallery.model.n_items() == 0 { let filtered = ui.gallery.kind.get().is_some() || ui.gallery.favorites.get() diff --git a/crates/pdfs-gui/src/app/pages/places.rs b/crates/pdfs-gui/src/app/pages/places.rs index e2bb9ed..e05505f 100644 --- a/crates/pdfs-gui/src/app/pages/places.rs +++ b/crates/pdfs-gui/src/app/pages/places.rs @@ -392,21 +392,45 @@ fn load_place_locations(ui: &Rc, id: u32) { }); } -/// Open `place` and then photo `uid` in it, paging through the town until -/// the photo has loaded. +/// Open photo `uid` of `place` from the map or a spot's list, paging through +/// the town until the photo has loaded. +/// +/// The lightbox steps through the gallery's model, so the town is opened +/// behind it. Closing the lightbox goes back to the map rather than leaving +/// the user in a town timeline they never asked for. fn open_place_photo(ui: &Rc, place: PlaceInfo, uid: String) { + let id = place.id; open_place(ui, place); - view_when_loaded(ui, uid); + view_when_loaded(ui, id, uid); } -fn view_when_loaded(ui: &Rc, uid: String) { +/// The open place's id, if a place is open. +fn open_place_id(ui: &Ui) -> Option { + ui.gallery.place.borrow().as_ref().map(|place| place.id) +} + +fn view_when_loaded(ui: &Rc, id: u32, uid: String) { let ui_page = ui.clone(); ui.gallery.page_waiters.borrow_mut().push(Box::new(move || { let ui = ui_page; + // The user went elsewhere while the town loaded: whatever page lands + // now belongs to that, and must not pop the lightbox up over it. + if open_place_id(&ui) != Some(id) { + return; + } if find_photo_index(&ui.gallery.model, &uid).is_some() { - open_photo_viewer(&ui, uid); - } else if ui.gallery.has_more.get() && ui.gallery.place.borrow().is_some() { - view_when_loaded(&ui, uid); + let window = open_photo_viewer(&ui, uid); + let ui_close = ui.clone(); + window.connect_close_request(move |_| { + // Only if the town is still what the lightbox was showing: "Show + // on map" in the lightbox has already moved on by itself. + if open_place_id(&ui_close) == Some(id) { + show_place_grid(&ui_close); + } + glib::Propagation::Proceed + }); + } else if ui.gallery.has_more.get() { + view_when_loaded(&ui, id, uid); ui.gallery.burst.set(true); load_gallery(&ui, true); } diff --git a/crates/pdfs-gui/src/app/pages/shared.rs b/crates/pdfs-gui/src/app/pages/shared.rs index 2bfd3d5..9fd3038 100644 --- a/crates/pdfs-gui/src/app/pages/shared.rs +++ b/crates/pdfs-gui/src/app/pages/shared.rs @@ -443,6 +443,7 @@ fn repaint_shared_files(ui: &Rc, entries: &[DirEntry]) { let nav = ui.shared.nav.borrow().clone(); repaint_shared_crumb(ui, &nav); let top = nav.is_empty(); + let another_folder = ui.shared.listed.borrow().as_ref() != Some(&nav); *ui.shared.listed.borrow_mut() = Some(nav); if entries.is_empty() { ui.shared.files.model.remove_all(); @@ -467,8 +468,11 @@ fn repaint_shared_files(ui: &Rc, entries: &[DirEntry]) { } ui.shared.files_content.set_visible_child_name("list"); // Only the rows that changed are swapped, so a refresh keeps the selection - // and the scroll position. + // and the scroll position. Another folder starts from its first entry. replace_items(&ui.shared.files.model, entries); + if another_folder { + ui.shared.files.scroll_to_top(); + } } /// Rebuild the breadcrumb trail for `nav`: "Shared with me", then each folder diff --git a/crates/pdfs-gui/src/app/widgets/file_list.rs b/crates/pdfs-gui/src/app/widgets/file_list.rs index 06d2d46..94b5455 100644 --- a/crates/pdfs-gui/src/app/widgets/file_list.rs +++ b/crates/pdfs-gui/src/app/widgets/file_list.rs @@ -224,6 +224,19 @@ impl FileList { .set_visible_child_name(if list { "list" } else { "grid" }); } + /// Bring both views back to the first entry. For a listing of something + /// else: [`replace_items`] keeps the scroll position, which is right for a + /// refresh, but another folder would open where the last one was left — + /// often at its bottom. + pub(crate) fn scroll_to_top(&self) { + if self.model.n_items() == 0 { + return; + } + self.grid.scroll_to(0, gtk4::ListScrollFlags::NONE, None); + self.column_view + .scroll_to(0, None, gtk4::ListScrollFlags::NONE, None); + } + /// Whether the list, rather than the grid, is on screen. pub(crate) fn shows_list(&self) -> bool { self.views.visible_child_name().as_deref() == Some("list") diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 68c4887..51e5579 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -115,6 +115,22 @@ Existing folders stay device backups. Older builds cannot open the upgraded data - **Videos over 256 MiB get a thumbnail** (B283). They never got one, and a smaller video was downloaded whole into memory for one frame. +- **A folder opens at the top.** A folder opened in My files, Shared with me or Computers could + open scrolled to where its parent was left, often at the bottom. Another folder, a new search + and a new Photos filter, album or place now start at the top, and a refresh keeps its place. + +- **Closing a photo opened from the Places map goes back to the map**, not to the town's + timeline. + +- **Albums and Places stay on screen.** They could switch to the timeline by themselves when a + timeline page arrived late, after an upload or after a trash Undo. Coming back to Photos no + longer reloads the timeline and loses its scroll position, unless photos changed while it was + out of sight. After an album or a place, the timeline came back unfiltered while the kind tab + and the date still showed the filter. + +- **A search in My files stays.** It was replaced by the folder's listing whenever files + changed, on refresh and on coming back to the page, while the query stayed in the box. + ## [3.7.0] — 2026-11-02 Schema: **44**, unchanged. From 5c184013f1cb73acde0885e936b695ae7d61f671 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 14 Nov 2026 12:47:27 +0100 Subject: [PATCH 2/2] feat(gui): restore folder places and add keyboard Back Back, Forward and Up in My files restore the folder's scroll offset and selection as they were when it was left; opening a folder any other way still starts at the top. Closing the photo viewer scrolls the grid to the photo it showed last. Alt+Left and Backspace go back a level in Shared with me and Computers and leave an open album or place; Escape leaves an album or place too, after ending selection mode. The shortcuts window lists it. A fresh Photos load, or leaving the grid for Albums or Places, ends a scrubber jump that was still paging. --- crates/pdfs-gui/src/app/main.rs | 31 +- crates/pdfs-gui/src/app/pages/browser.rs | 50 +- crates/pdfs-gui/src/app/pages/devices.rs | 15 +- crates/pdfs-gui/src/app/pages/photo_viewer.rs | 2 + crates/pdfs-gui/src/app/pages/photos.rs | 15 + crates/pdfs-gui/src/app/pages/shared.rs | 13 +- crates/pdfs-gui/src/app/widgets/file_list.rs | 30 + docs/CHANGELOG.md | 8 + po/de.po | 1097 ++++++++-------- po/en_GB.po | 1143 +++++++++-------- po/es.po | 1097 ++++++++-------- po/fr.po | 1097 ++++++++-------- po/it.po | 1097 ++++++++-------- po/nl.po | 1097 ++++++++-------- po/pdfs.pot | 1097 ++++++++-------- po/pl.po | 1097 ++++++++-------- po/pt_BR.po | 1097 ++++++++-------- 17 files changed, 5117 insertions(+), 4966 deletions(-) diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index ba41ea3..242a798 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -457,6 +457,8 @@ fn build_window(app: &adw::Application) { files: browser_widgets.files.clone(), history: RefCell::new(Vec::new()), future: RefCell::new(Vec::new()), + places: RefCell::new(HashMap::new()), + restore: RefCell::new(None), crumb: browser_widgets.crumb.clone(), content: browser_widgets.content.clone(), status: browser_widgets.status.clone(), @@ -1548,6 +1550,7 @@ fn show_shortcuts(window: &adw::ApplicationWindow) { ("minus", gettext_noop("Smaller thumbnails")), ("0", gettext_noop("Reset thumbnail size")), ("a", gettext_noop("Select all")), + ("Escape Left", gettext_noop("Back")), ], ), ( @@ -1584,8 +1587,17 @@ fn install_shortcuts(ui: &Rc, window: &adw::ApplicationWindow) { controller.connect_key_pressed(move |_, key, _, state| { let ctrl = state.contains(gtk4::gdk::ModifierType::CONTROL_MASK); let alt = state.contains(gtk4::gdk::ModifierType::ALT_MASK); - let on_browser = ui.stack.visible_child_name().as_deref() == Some("browser"); - let on_trash = ui.stack.visible_child_name().as_deref() == Some("trash"); + let page = ui.stack.visible_child_name(); + let on_browser = page.as_deref() == Some("browser"); + let on_trash = page.as_deref() == Some("trash"); + let on_gallery = page.as_deref() == Some("gallery"); + // Pages with a level to go back to, besides My files' own history. + let in_collection = on_gallery && ui.gallery.in_collection(); + let in_shared_folder = + page.as_deref() == Some("shared") && !ui.shared.nav.borrow().is_empty(); + let in_device_folder = + page.as_deref() == Some("devices") && !ui.devices.nav.borrow().is_empty(); + let back_key = (key == gtk4::gdk::Key::Left && alt) || key == gtk4::gdk::Key::BackSpace; match key { // Refresh works on every page, so it is matched before the // browser-only bindings. @@ -1642,6 +1654,21 @@ fn install_shortcuts(ui: &Rc, window: &adw::ApplicationWindow) { gtk4::gdk::Key::BackSpace if on_browser => { ui.browser.actions.activate_action("back", None) } + // Selection mode ends before Escape leaves anything; the grid + // handles this itself while it has the focus. + gtk4::gdk::Key::Escape if on_gallery && ui.gallery.selecting.get() => { + set_selection_mode(&ui, false) + } + // An album or a place is a level below Photos, as a folder is + // below its parent: Back leaves it, and so does Escape. + _ if in_collection + && !ui.gallery.selecting.get() + && (back_key || key == gtk4::gdk::Key::Escape) => + { + ui.gallery.back.emit_clicked() + } + _ if in_shared_folder && back_key => shared_back(&ui), + _ if in_device_folder && back_key => devices_back(&ui), gtk4::gdk::Key::u | gtk4::gdk::Key::U if ctrl && on_browser => prompt_upload(&ui), gtk4::gdk::Key::Menu if visible_file_list(&ui).is_some() => { if let Some(list) = visible_file_list(&ui) { diff --git a/crates/pdfs-gui/src/app/pages/browser.rs b/crates/pdfs-gui/src/app/pages/browser.rs index b6fb218..4977ecb 100644 --- a/crates/pdfs-gui/src/app/pages/browser.rs +++ b/crates/pdfs-gui/src/app/pages/browser.rs @@ -1,6 +1,14 @@ use crate::activation::{DriveActivation, drive_activation, mounted_path, present_within}; use crate::*; +/// How a folder in My files was left: scrolled `offset` px down, with the +/// entries named `selected` highlighted. +#[derive(Clone, Debug, Default, PartialEq)] +pub(crate) struct FolderPlace { + pub(crate) offset: f64, + pub(crate) selected: Vec, +} + pub(crate) struct BrowserState { // Files (browser) page. /// The grid and list; the model is repopulated per directory. @@ -10,6 +18,13 @@ pub(crate) struct BrowserState { /// breadcrumb. pub(crate) history: RefCell>, pub(crate) future: RefCell>, + /// Where each folder left by Back, Forward, Up or a descent was scrolled + /// and what was selected in it, by path. Back, Forward and Up put the + /// folder back that way; opening it afresh starts at the top. + pub(crate) places: RefCell>, + /// The folder whose [`Self::places`] entry the next listing restores, set + /// by Back, Forward and Up. + pub(crate) restore: RefCell>, /// Clickable breadcrumb trail (a button per path segment); rebuilt per load /// by [`repaint_crumb`] so each ancestor folder navigates on click. pub(crate) crumb: gtk4::Box, @@ -1556,8 +1571,29 @@ pub(crate) fn sort_entries(entries: &mut [DirEntry], view: FilesView) { }); } +/// Remember how the folder on screen is scrolled and what is selected in it, +/// for Back, Forward or Up to bring it back that way. Only a listing of the +/// folder itself counts; search hits are not its rows. +fn remember_place(ui: &Rc) { + let path = ui.browser.path.borrow().clone(); + if ui.browser.listed.borrow().as_deref() != Some(path.as_str()) { + return; + } + let files = &ui.browser.files; + let place = FolderPlace { + offset: files.scroll_offset(), + selected: files + .selected() + .into_iter() + .map(|entry| entry.name) + .collect(), + }; + ui.browser.places.borrow_mut().insert(path, place); +} + /// Open the folder on screen in a new place, remembering where we were. pub(crate) fn browse_to(ui: &Rc, target: String) { + remember_place(ui); let left = ui.browser.path.replace(target.clone()); if left != target { ui.browser.history.borrow_mut().push(left); @@ -1582,6 +1618,8 @@ fn browse_back(ui: &Rc) { let Some(previous) = ui.browser.history.borrow_mut().pop() else { return; }; + remember_place(ui); + *ui.browser.restore.borrow_mut() = Some(previous.clone()); let left = ui.browser.path.replace(previous); ui.browser.future.borrow_mut().push(left); load_browser(ui); @@ -1592,6 +1630,8 @@ fn browse_forward(ui: &Rc) { return; }; ui.browser.search.set_text(""); + remember_place(ui); + *ui.browser.restore.borrow_mut() = Some(next.clone()); let left = ui.browser.path.replace(next); ui.browser.history.borrow_mut().push(left); load_browser(ui); @@ -1604,6 +1644,7 @@ fn browse_up(ui: &Rc) { } ui.browser.search.set_text(""); let parent = path.rfind('/').map(|i| &path[..i]).unwrap_or_default(); + *ui.browser.restore.borrow_mut() = Some(parent.to_string()); browse_to(ui, parent.to_string()); } @@ -3003,8 +3044,15 @@ pub(crate) fn repaint_browser(ui: &Rc, entries: &[DirEntry]) { // Only the rows that changed are swapped, so a refresh keeps the // selection, the scroll position and the thumbnails already drawn. replace_items(&ui.browser.files.model, &sorted); + // Back, Forward and Up return to a folder as it was left; any other way + // in starts at the top. + let restore = ui.browser.restore.take().filter(|target| *target == path); if another_folder { - ui.browser.files.scroll_to_top(); + let place = restore.and_then(|path| ui.browser.places.borrow().get(&path).cloned()); + match place { + Some(place) => ui.browser.files.restore(place.offset, &place.selected), + None => ui.browser.files.scroll_to_top(), + } } } diff --git a/crates/pdfs-gui/src/app/pages/devices.rs b/crates/pdfs-gui/src/app/pages/devices.rs index cbf4286..4628f21 100644 --- a/crates/pdfs-gui/src/app/pages/devices.rs +++ b/crates/pdfs-gui/src/app/pages/devices.rs @@ -190,10 +190,9 @@ pub(crate) fn wire_devices(ui: &Rc, widgets: &DevicesWidgets) { })); let ui_back = ui.clone(); - widgets.back.connect_clicked(move |_| { - ui_back.devices.nav.borrow_mut().pop(); - load_devices(&ui_back); - }); + widgets + .back + .connect_clicked(move |_| devices_back(&ui_back)); let ui_retry = ui.clone(); widgets .retry @@ -211,6 +210,14 @@ pub(crate) fn devices_status(ui: &Rc, icon: &str, title: &str, description: ui.devices.content.set_visible_child_name("status"); } +/// Back out of a computer's folder to the level above: the page's Back button, and +/// Alt+Left or Backspace. A no-op at the top. +pub(crate) fn devices_back(ui: &Rc) { + if ui.devices.nav.borrow_mut().pop().is_some() { + load_devices(ui); + } +} + /// Load the view [`DevicesState::nav`] points at: the device list, or a folder /// of another computer's backup. pub(crate) fn load_devices(ui: &Rc) { diff --git a/crates/pdfs-gui/src/app/pages/photo_viewer.rs b/crates/pdfs-gui/src/app/pages/photo_viewer.rs index c4d507d..e67cb01 100644 --- a/crates/pdfs-gui/src/app/pages/photo_viewer.rs +++ b/crates/pdfs-gui/src/app/pages/photo_viewer.rs @@ -1428,8 +1428,10 @@ pub(crate) fn open_photo_viewer(ui: &Rc, initial_uid: String) -> gtk4::Windo // kept every closed lightbox, its window and its full-size photo in memory // for the life of the app. let keep = RefCell::new(Some(viewer.clone())); + let ui_close = ui.clone(); window.connect_close_request(move |_| { if let Some(viewer) = keep.borrow_mut().take() { + reveal_photo(&ui_close, &viewer.uid.borrow()); stop_video(&viewer); disarm_slide(&viewer); if let Some(source) = viewer.chrome_source.borrow_mut().take() { diff --git a/crates/pdfs-gui/src/app/pages/photos.rs b/crates/pdfs-gui/src/app/pages/photos.rs index dd3c76a..9070db5 100644 --- a/crates/pdfs-gui/src/app/pages/photos.rs +++ b/crates/pdfs-gui/src/app/pages/photos.rs @@ -3288,6 +3288,16 @@ fn top_anchor(ui: &Rc) -> Option<(String, f32)> { .or(first) } +/// Scroll the grid just far enough to show photo `uid`, as the lightbox +/// closes: paging with the arrow keys may have left the grid far behind. +pub(crate) fn reveal_photo(ui: &Rc, uid: &str) { + if let Some(row) = row_of_photo(&ui.gallery.groups, uid) { + ui.gallery + .list + .scroll_to(row, gtk4::ListScrollFlags::NONE, None); + } +} + /// Which row of the rendered model holds `uid`, if any. fn row_of_photo(store: &gio::ListStore, uid: &str) -> Option { (0..store.n_items()).find(|i| { @@ -4035,6 +4045,8 @@ pub(crate) fn load_gallery(ui: &Rc, append: bool) { // be read again when they switch back to it. if !photo_grid_shown(ui) { ui.gallery.timeline_stale.set(true); + // A scrubber jump was paging the grid the user has just left. + ui.gallery.jump.set(None); return; } if ui.gallery.loading.get() { @@ -4052,6 +4064,9 @@ pub(crate) fn load_gallery(ui: &Rc, append: bool) { let place = ui.gallery.place.borrow().as_ref().map(|p| p.id); let collection = album.is_some() || place.is_some(); if !append { + // A scrubber jump looks for a month of the listing being replaced: + // another filter, album or place has its own months, if any. + ui.gallery.jump.set(None); // Fresh load: clear the timeline and show Loading until the first page lands. ui.gallery.model.remove_all(); gallery_status( diff --git a/crates/pdfs-gui/src/app/pages/shared.rs b/crates/pdfs-gui/src/app/pages/shared.rs index 9fd3038..24065c9 100644 --- a/crates/pdfs-gui/src/app/pages/shared.rs +++ b/crates/pdfs-gui/src/app/pages/shared.rs @@ -279,10 +279,7 @@ pub(crate) fn wire_shared(ui: &Rc, widgets: &SharedWidgets) { })); let ui_back = ui.clone(); - widgets.back.connect_clicked(move |_| { - ui_back.shared.nav.borrow_mut().pop(); - load_shared(&ui_back); - }); + widgets.back.connect_clicked(move |_| shared_back(&ui_back)); let ui_retry = ui.clone(); widgets .retry @@ -318,6 +315,14 @@ pub(crate) fn shared_status(ui: &Rc, icon: &str, title: &str, description: & ui.shared.content.set_visible_child_name("status"); } +/// Back out of a shared folder to the level above: the page's Back button, and +/// Alt+Left or Backspace. A no-op at the top. +pub(crate) fn shared_back(ui: &Rc) { + if ui.shared.nav.borrow_mut().pop().is_some() { + load_shared(ui); + } +} + /// Fetch the top level (shared-with-me, invitations, saved links) in parallel /// and repaint the page once all three land. /// diff --git a/crates/pdfs-gui/src/app/widgets/file_list.rs b/crates/pdfs-gui/src/app/widgets/file_list.rs index 94b5455..97f6d8e 100644 --- a/crates/pdfs-gui/src/app/widgets/file_list.rs +++ b/crates/pdfs-gui/src/app/widgets/file_list.rs @@ -237,6 +237,36 @@ impl FileList { .scroll_to(0, None, gtk4::ListScrollFlags::NONE, None); } + /// How far the view on screen is scrolled, in px from the top. + pub(crate) fn scroll_offset(&self) -> f64 { + self.views + .visible_child() + .and_downcast::() + .map_or(0.0, |scroll| scroll.vadjustment().value()) + } + + /// Select the entries named `selected` and scroll the view on screen to + /// `offset`, as they were when the folder was left. The scroll waits for + /// an idle, when the views have measured the rows just put in. + pub(crate) fn restore(&self, offset: f64, selected: &[String]) { + self.selection.unselect_all(); + for i in 0..self.selection.n_items() { + if entry_at(Some(&self.selection), i) + .is_some_and(|entry| selected.contains(&entry.name)) + { + self.selection.select_item(i, false); + } + } + let Some(scroll) = self + .views + .visible_child() + .and_downcast::() + else { + return; + }; + glib::idle_add_local_once(move || scroll.vadjustment().set_value(offset)); + } + /// Whether the list, rather than the grid, is on screen. pub(crate) fn shows_list(&self) -> bool { self.views.visible_child_name().as_deref() == Some("list") diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 51e5579..fa46e1a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -74,6 +74,11 @@ Existing folders stay device backups. Older builds cannot open the upgraded data tell photos apart by their thumbnail. Ctrl+scroll, Ctrl+plus, Ctrl+minus and Ctrl+0 change it too, and the size is remembered between runs. +- **Back takes you to where you were.** Back, Forward and Up in My files return to a folder + scrolled where it was left, with the same items selected, and closing the photo viewer scrolls + the grid to the photo it was showing. Alt+Left and Backspace go back a level in Shared with me + and Computers and leave an open album or place in Photos, as Escape does too. + ### Changed - **`proton-sdk` and `proton-drive-rs` 0.11.0.** Timestamps are converted where they enter @@ -131,6 +136,9 @@ Existing folders stay device backups. Older builds cannot open the upgraded data - **A search in My files stays.** It was replaced by the folder's listing whenever files changed, on refresh and on coming back to the page, while the query stayed in the box. +- **A date jump in Photos stops when you look elsewhere.** A jump still loading the timeline + kept going after another filter, album or place was opened. + ## [3.7.0] — 2026-11-02 Schema: **44**, unchanged. diff --git a/po/de.po b/po/de.po index 57e8372..a656e2d 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-11-14 02:49+0100\n" +"POT-Creation-Date: 2026-11-14 12:53+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -31,7 +31,7 @@ msgstr[1] "vor {n} Min." #. Translators: strftime format for the time a photo was taken, shown on its tile, such as "14:05". #: crates/pdfs-gui/src/app/dates.rs:44 #: crates/pdfs-gui/src/app/pages/activity.rs:840 -#: crates/pdfs-gui/src/app/pages/photos.rs:3534 +#: crates/pdfs-gui/src/app/pages/photos.rs:3544 msgid "%H:%M" msgstr "%H:%M" @@ -57,87 +57,87 @@ msgstr "%-d. %b %Y" msgid "%A, %B %-d, %Y, %H:%M" msgstr "%A, %-d. %B %Y, %H:%M" -#: crates/pdfs-gui/src/app/main.rs:865 +#: crates/pdfs-gui/src/app/main.rs:876 #: crates/pdfs-gui/src/app/pages/activity_text.rs:543 -#: crates/pdfs-gui/src/app/pages/browser.rs:458 -#: crates/pdfs-gui/src/app/pages/browser.rs:865 -#: crates/pdfs-gui/src/app/pages/browser.rs:1281 -#: crates/pdfs-gui/src/app/pages/browser.rs:2240 -#: crates/pdfs-gui/src/app/pages/browser.rs:2319 +#: crates/pdfs-gui/src/app/pages/browser.rs:473 +#: crates/pdfs-gui/src/app/pages/browser.rs:880 +#: crates/pdfs-gui/src/app/pages/browser.rs:1296 +#: crates/pdfs-gui/src/app/pages/browser.rs:2281 +#: crates/pdfs-gui/src/app/pages/browser.rs:2360 #: crates/pdfs-gui/src/app/pages/locations.rs:1721 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:252 #: crates/pdfs-gui/src/app/pages/trash.rs:191 crates/pdfs-gui/src/prompt.rs:171 msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:866 crates/pdfs-gui/src/app/main.rs:1536 +#: crates/pdfs-gui/src/app/main.rs:877 crates/pdfs-gui/src/app/main.rs:1547 #: crates/pdfs-gui/src/app/pages/albums.rs:636 #: crates/pdfs-gui/src/app/pages/photos.rs:898 -#: crates/pdfs-gui/src/app/pages/places.rs:439 +#: crates/pdfs-gui/src/app/pages/places.rs:463 msgid "Photos" msgstr "Fotos" -#: crates/pdfs-gui/src/app/main.rs:869 +#: crates/pdfs-gui/src/app/main.rs:880 #: crates/pdfs-gui/src/app/pages/shared.rs:168 -#: crates/pdfs-gui/src/app/pages/shared.rs:484 +#: crates/pdfs-gui/src/app/pages/shared.rs:493 msgid "Shared with me" msgstr "Mit mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:874 +#: crates/pdfs-gui/src/app/main.rs:885 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "Von mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:877 +#: crates/pdfs-gui/src/app/main.rs:888 #: crates/pdfs-gui/src/app/pages/devices.rs:140 -#: crates/pdfs-gui/src/app/pages/devices.rs:493 +#: crates/pdfs-gui/src/app/pages/devices.rs:505 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:878 +#: crates/pdfs-gui/src/app/main.rs:889 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "Papierkorb" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:880 +#: crates/pdfs-gui/src/app/main.rs:891 msgid "Sync" msgstr "Synchronisierung" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1230 +#: crates/pdfs-gui/src/app/main.rs:1241 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1253 +#: crates/pdfs-gui/src/app/main.rs:1264 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1254 +#: crates/pdfs-gui/src/app/main.rs:1265 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1255 +#: crates/pdfs-gui/src/app/main.rs:1266 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1256 +#: crates/pdfs-gui/src/app/main.rs:1267 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1268 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1344 +#: crates/pdfs-gui/src/app/main.rs:1355 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1352 +#: crates/pdfs-gui/src/app/main.rs:1363 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -149,23 +149,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1357 +#: crates/pdfs-gui/src/app/main.rs:1368 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1361 +#: crates/pdfs-gui/src/app/main.rs:1372 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1365 +#: crates/pdfs-gui/src/app/main.rs:1376 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1370 +#: crates/pdfs-gui/src/app/main.rs:1381 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -190,7 +190,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -210,7 +210,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licensed under CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licencia CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, sous licence CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -209,7 +209,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licenza CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, gelicentieerd onder CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the \n" "Language-Team: LANGUAGE \n" @@ -34,7 +34,7 @@ msgstr[1] "" #. Translators: strftime format for the time a photo was taken, shown on its tile, such as "14:05". #: crates/pdfs-gui/src/app/dates.rs:44 #: crates/pdfs-gui/src/app/pages/activity.rs:840 -#: crates/pdfs-gui/src/app/pages/photos.rs:3534 +#: crates/pdfs-gui/src/app/pages/photos.rs:3544 msgid "%H:%M" msgstr "" @@ -60,87 +60,87 @@ msgstr "" msgid "%A, %B %-d, %Y, %H:%M" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:865 +#: crates/pdfs-gui/src/app/main.rs:876 #: crates/pdfs-gui/src/app/pages/activity_text.rs:543 -#: crates/pdfs-gui/src/app/pages/browser.rs:458 -#: crates/pdfs-gui/src/app/pages/browser.rs:865 -#: crates/pdfs-gui/src/app/pages/browser.rs:1281 -#: crates/pdfs-gui/src/app/pages/browser.rs:2240 -#: crates/pdfs-gui/src/app/pages/browser.rs:2319 +#: crates/pdfs-gui/src/app/pages/browser.rs:473 +#: crates/pdfs-gui/src/app/pages/browser.rs:880 +#: crates/pdfs-gui/src/app/pages/browser.rs:1296 +#: crates/pdfs-gui/src/app/pages/browser.rs:2281 +#: crates/pdfs-gui/src/app/pages/browser.rs:2360 #: crates/pdfs-gui/src/app/pages/locations.rs:1721 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:252 #: crates/pdfs-gui/src/app/pages/trash.rs:191 crates/pdfs-gui/src/prompt.rs:171 msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:866 crates/pdfs-gui/src/app/main.rs:1536 +#: crates/pdfs-gui/src/app/main.rs:877 crates/pdfs-gui/src/app/main.rs:1547 #: crates/pdfs-gui/src/app/pages/albums.rs:636 #: crates/pdfs-gui/src/app/pages/photos.rs:898 -#: crates/pdfs-gui/src/app/pages/places.rs:439 +#: crates/pdfs-gui/src/app/pages/places.rs:463 msgid "Photos" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:869 +#: crates/pdfs-gui/src/app/main.rs:880 #: crates/pdfs-gui/src/app/pages/shared.rs:168 -#: crates/pdfs-gui/src/app/pages/shared.rs:484 +#: crates/pdfs-gui/src/app/pages/shared.rs:493 msgid "Shared with me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:874 +#: crates/pdfs-gui/src/app/main.rs:885 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:877 +#: crates/pdfs-gui/src/app/main.rs:888 #: crates/pdfs-gui/src/app/pages/devices.rs:140 -#: crates/pdfs-gui/src/app/pages/devices.rs:493 +#: crates/pdfs-gui/src/app/pages/devices.rs:505 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:878 +#: crates/pdfs-gui/src/app/main.rs:889 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:880 +#: crates/pdfs-gui/src/app/main.rs:891 msgid "Sync" msgstr "" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1230 +#: crates/pdfs-gui/src/app/main.rs:1241 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1253 +#: crates/pdfs-gui/src/app/main.rs:1264 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1254 +#: crates/pdfs-gui/src/app/main.rs:1265 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1255 +#: crates/pdfs-gui/src/app/main.rs:1266 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1256 +#: crates/pdfs-gui/src/app/main.rs:1267 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1268 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1344 +#: crates/pdfs-gui/src/app/main.rs:1355 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1352 +#: crates/pdfs-gui/src/app/main.rs:1363 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -148,23 +148,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1357 +#: crates/pdfs-gui/src/app/main.rs:1368 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1361 +#: crates/pdfs-gui/src/app/main.rs:1372 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1365 +#: crates/pdfs-gui/src/app/main.rs:1376 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1370 +#: crates/pdfs-gui/src/app/main.rs:1381 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -177,21 +177,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " "public domain." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames udostępnionych na licencji CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -211,7 +211,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licenciados sob CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1390 +#: crates/pdfs-gui/src/app/main.rs:1401 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1398 +#: crates/pdfs-gui/src/app/main.rs:1409 msgid "" "The street map, when turned on, comes from OpenFreeMap in the