mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-14 17:32:00 +01:00
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.
This commit is contained in:
1 parent
6be8a8f9bc
commit
5c184013f1
17 files changed
+5117
-4966
No files matched your search
@@ -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) {
|
||||
("<Control>minus", gettext_noop("Smaller thumbnails")),
|
||||
("<Control>0", gettext_noop("Reset thumbnail size")),
|
||||
("<Control>a", gettext_noop("Select all")),
|
||||
("Escape <Alt>Left", gettext_noop("Back")),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1584,8 +1587,17 @@ fn install_shortcuts(ui: &Rc<Ui>, 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<Ui>, 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) {
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<Vec<String>>,
|
||||
pub(crate) future: RefCell<Vec<String>>,
|
||||
/// 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<HashMap<String, FolderPlace>>,
|
||||
/// The folder whose [`Self::places`] entry the next listing restores, set
|
||||
/// by Back, Forward and Up.
|
||||
pub(crate) restore: RefCell<Option<String>>,
|
||||
/// 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<Ui>) {
|
||||
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<Ui>, 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<Ui>) {
|
||||
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<Ui>) {
|
||||
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>) {
|
||||
}
|
||||
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<Ui>, 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,10 +190,9 @@ pub(crate) fn wire_devices(ui: &Rc<Ui>, 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<Ui>, 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<Ui>) {
|
||||
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<Ui>) {
|
||||
|
||||
@@ -1428,8 +1428,10 @@ pub(crate) fn open_photo_viewer(ui: &Rc<Ui>, 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() {
|
||||
|
||||
@@ -3288,6 +3288,16 @@ fn top_anchor(ui: &Rc<Ui>) -> 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<Ui>, 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<u32> {
|
||||
(0..store.n_items()).find(|i| {
|
||||
@@ -4035,6 +4045,8 @@ pub(crate) fn load_gallery(ui: &Rc<Ui>, 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<Ui>, 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(
|
||||
|
||||
@@ -279,10 +279,7 @@ pub(crate) fn wire_shared(ui: &Rc<Ui>, 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<Ui>, 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<Ui>) {
|
||||
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.
|
||||
///
|
||||
|
||||
@@ -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::<gtk4::ScrolledWindow>()
|
||||
.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::<gtk4::ScrolledWindow>()
|
||||
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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
+572
-571
File diff suppressed because it is too large.
Load diff
+549
-548
File diff suppressed because it is too large.
Load diff
+549
-548
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user