mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
feat(core,gui): file browser menus, sort, history and folder-picker move (phase 4)
The My Files header gets a New menu, a grid/list split button whose menu sorts by name, size or date (persisted as files_view in the config), and a folder menu for Build Thumbnails and Open in File Manager. Navigation keeps a back/forward history (Alt+Left/Right, mouse buttons, Alt+Up). Move to… browses Drive folders and handles a whole selection, rename preselects the stem, and files dropped from a file manager upload into the folder on screen.
This commit is contained in:
1 parent
792eee75a4
commit
8bc52edeb4
6 files changed
+856
-195
No files matched your search
@@ -124,6 +124,63 @@ pub struct AppConfig {
|
|||||||
/// predating the field.
|
/// predating the field.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proton_accent: Option<bool>,
|
pub proton_accent: Option<bool>,
|
||||||
|
/// How the desktop app's My Files page lays out and orders a folder.
|
||||||
|
/// Defaulted for configs predating the field.
|
||||||
|
#[serde(default)]
|
||||||
|
pub files_view: FilesView,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The My Files page's view choices, remembered between runs.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct FilesView {
|
||||||
|
/// Columns instead of the icon grid.
|
||||||
|
pub list: bool,
|
||||||
|
pub sort: FileSort,
|
||||||
|
pub descending: bool,
|
||||||
|
/// Folders ahead of files, whatever the sort.
|
||||||
|
pub folders_first: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FilesView {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
list: false,
|
||||||
|
sort: FileSort::Name,
|
||||||
|
descending: false,
|
||||||
|
folders_first: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The key a My Files listing is ordered by.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum FileSort {
|
||||||
|
#[default]
|
||||||
|
Name,
|
||||||
|
Size,
|
||||||
|
Modified,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSort {
|
||||||
|
/// The name used in the desktop app's actions and the config file.
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FileSort::Name => "name",
|
||||||
|
FileSort::Size => "size",
|
||||||
|
FileSort::Modified => "modified",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(value: &str) -> Option<Self> {
|
||||||
|
match value {
|
||||||
|
"name" => Some(FileSort::Name),
|
||||||
|
"size" => Some(FileSort::Size),
|
||||||
|
"modified" => Some(FileSort::Modified),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
@@ -139,6 +196,7 @@ impl Default for AppConfig {
|
|||||||
open_with: None,
|
open_with: None,
|
||||||
prompt: None,
|
prompt: None,
|
||||||
proton_accent: None,
|
proton_accent: None,
|
||||||
|
files_view: FilesView::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,12 +542,29 @@ mod tests {
|
|||||||
open_with: None,
|
open_with: None,
|
||||||
prompt: None,
|
prompt: None,
|
||||||
proton_accent: Some(true),
|
proton_accent: Some(true),
|
||||||
|
files_view: FilesView {
|
||||||
|
list: true,
|
||||||
|
sort: FileSort::Modified,
|
||||||
|
descending: true,
|
||||||
|
folders_first: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
let decoded: AppConfig = serde_json::from_str(&json).unwrap();
|
let decoded: AppConfig = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0");
|
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0");
|
||||||
assert_eq!(decoded.user_agent, "test-agent/1.0");
|
assert_eq!(decoded.user_agent, "test-agent/1.0");
|
||||||
assert_eq!(decoded.proton_accent, Some(true));
|
assert_eq!(decoded.proton_accent, Some(true));
|
||||||
|
assert_eq!(decoded.files_view, config.files_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_config_without_a_files_view_gets_the_default() {
|
||||||
|
let decoded: AppConfig =
|
||||||
|
serde_json::from_str(r#"{"app_version":"a","user_agent":"b"}"#).unwrap();
|
||||||
|
assert_eq!(decoded.files_view, FilesView::default());
|
||||||
|
let partial: FilesView = serde_json::from_str(r#"{"sort":"size"}"#).unwrap();
|
||||||
|
assert_eq!(partial.sort, FileSort::Size);
|
||||||
|
assert!(partial.folders_first);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ progressbar.browser-status-meter trough, progressbar.browser-status-meter progre
|
|||||||
.bulk-bar { padding: 6px 8px; border-radius: 12px; background: color-mix(in srgb, var(--accent-bg-color) 12%, transparent); }
|
.bulk-bar { padding: 6px 8px; border-radius: 12px; background: color-mix(in srgb, var(--accent-bg-color) 12%, transparent); }
|
||||||
.dropzone { border: 2px dashed alpha(currentColor, 0.25); border-radius: 16px; padding: 28px 18px; transition: background 160ms ease, border-color 160ms ease; }
|
.dropzone { border: 2px dashed alpha(currentColor, 0.25); border-radius: 16px; padding: 28px 18px; transition: background 160ms ease, border-color 160ms ease; }
|
||||||
.dropzone-active { border-color: var(--accent-bg-color); background: color-mix(in srgb, var(--accent-bg-color) 10%, transparent); }
|
.dropzone-active { border-color: var(--accent-bg-color); background: color-mix(in srgb, var(--accent-bg-color) 10%, transparent); }
|
||||||
|
.files-drop-active { outline: 2px dashed var(--accent-bg-color); outline-offset: -4px; border-radius: 12px; background: color-mix(in srgb, var(--accent-bg-color) 6%, transparent); }
|
||||||
|
|
||||||
/* Photo viewer: always light-on-dark, whatever the system style */
|
/* Photo viewer: always light-on-dark, whatever the system style */
|
||||||
.photo-viewer-window { background-color: #111014; }
|
.photo-viewer-window { background-color: #111014; }
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ use gtk4::glib::BoxedAnyObject;
|
|||||||
|
|
||||||
use pdfs_core::auth;
|
use pdfs_core::auth;
|
||||||
|
|
||||||
use pdfs_core::config::AppDirs;
|
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,
|
||||||
@@ -394,7 +394,8 @@ fn build_window(app: &adw::Application) {
|
|||||||
},
|
},
|
||||||
browser: BrowserState {
|
browser: BrowserState {
|
||||||
model: browser_widgets.model.clone(),
|
model: browser_widgets.model.clone(),
|
||||||
back: browser_widgets.back.clone(),
|
history: RefCell::new(Vec::new()),
|
||||||
|
future: RefCell::new(Vec::new()),
|
||||||
crumb: browser_widgets.crumb.clone(),
|
crumb: browser_widgets.crumb.clone(),
|
||||||
content: browser_widgets.content.clone(),
|
content: browser_widgets.content.clone(),
|
||||||
status: browser_widgets.status.clone(),
|
status: browser_widgets.status.clone(),
|
||||||
@@ -402,9 +403,10 @@ fn build_window(app: &adw::Application) {
|
|||||||
split: browser_widgets.split.clone(),
|
split: browser_widgets.split.clone(),
|
||||||
path: RefCell::new(String::new()),
|
path: RefCell::new(String::new()),
|
||||||
search: browser_widgets.search.clone(),
|
search: browser_widgets.search.clone(),
|
||||||
new_folder: browser_widgets.new_folder.clone(),
|
actions: browser_widgets.actions.clone(),
|
||||||
upload: browser_widgets.upload.clone(),
|
view_button: browser_widgets.view_button.clone(),
|
||||||
upload_folder: browser_widgets.upload_folder.clone(),
|
view: Cell::new(FilesView::default()),
|
||||||
|
listing: RefCell::new(Vec::new()),
|
||||||
build_thumbnails: browser_widgets.build_thumbnails.clone(),
|
build_thumbnails: browser_widgets.build_thumbnails.clone(),
|
||||||
thumbnail_build_row: browser_widgets.thumbnail_build_row.clone(),
|
thumbnail_build_row: browser_widgets.thumbnail_build_row.clone(),
|
||||||
thumbnail_progress: browser_widgets.thumbnail_progress.clone(),
|
thumbnail_progress: browser_widgets.thumbnail_progress.clone(),
|
||||||
@@ -420,6 +422,7 @@ fn build_window(app: &adw::Application) {
|
|||||||
bulk_trash: browser_widgets.bulk_trash.clone(),
|
bulk_trash: browser_widgets.bulk_trash.clone(),
|
||||||
bulk_pin: browser_widgets.bulk_pin.clone(),
|
bulk_pin: browser_widgets.bulk_pin.clone(),
|
||||||
bulk_unpin: browser_widgets.bulk_unpin.clone(),
|
bulk_unpin: browser_widgets.bulk_unpin.clone(),
|
||||||
|
bulk_move: browser_widgets.bulk_move.clone(),
|
||||||
empty_actions: browser_widgets.empty_actions.clone(),
|
empty_actions: browser_widgets.empty_actions.clone(),
|
||||||
summary: browser_widgets.summary.clone(),
|
summary: browser_widgets.summary.clone(),
|
||||||
zoom: browser_widgets.zoom.clone(),
|
zoom: browser_widgets.zoom.clone(),
|
||||||
@@ -609,13 +612,7 @@ fn build_window(app: &adw::Application) {
|
|||||||
&browser_widgets.empty_upload,
|
&browser_widgets.empty_upload,
|
||||||
&browser_widgets.empty_new_folder,
|
&browser_widgets.empty_new_folder,
|
||||||
);
|
);
|
||||||
wire_browser_actions(
|
wire_browser_actions(&ui, &browser_widgets.build_thumbnails);
|
||||||
&ui,
|
|
||||||
&browser_widgets.new_folder,
|
|
||||||
&browser_widgets.upload,
|
|
||||||
&browser_widgets.upload_folder,
|
|
||||||
&browser_widgets.build_thumbnails,
|
|
||||||
);
|
|
||||||
wire_details(&ui);
|
wire_details(&ui);
|
||||||
wire_search(&ui);
|
wire_search(&ui);
|
||||||
wire_gallery(
|
wire_gallery(
|
||||||
@@ -1197,6 +1194,9 @@ fn show_shortcuts(window: &adw::ApplicationWindow) {
|
|||||||
&[
|
&[
|
||||||
("<Control>n", "New folder"),
|
("<Control>n", "New folder"),
|
||||||
("<Control>u", "Upload files"),
|
("<Control>u", "Upload files"),
|
||||||
|
("<Alt>Left <Alt>Right", "Back / forward"),
|
||||||
|
("<Alt>Up", "Parent folder"),
|
||||||
|
("<Control>1 <Control>2", "Grid / list"),
|
||||||
("F2", "Rename"),
|
("F2", "Rename"),
|
||||||
("Delete", "Move to Trash"),
|
("Delete", "Move to Trash"),
|
||||||
("Escape", "Clear the selection"),
|
("Escape", "Clear the selection"),
|
||||||
@@ -1260,6 +1260,7 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
|
|||||||
let ui = ui.clone();
|
let ui = ui.clone();
|
||||||
controller.connect_key_pressed(move |_, key, _, state| {
|
controller.connect_key_pressed(move |_, key, _, state| {
|
||||||
let ctrl = state.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
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_browser = ui.stack.visible_child_name().as_deref() == Some("browser");
|
||||||
match key {
|
match key {
|
||||||
// Refresh works on every page, so it is matched before the
|
// Refresh works on every page, so it is matched before the
|
||||||
@@ -1276,6 +1277,29 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
|
|||||||
ui.browser.search.grab_focus();
|
ui.browser.search.grab_focus();
|
||||||
}
|
}
|
||||||
gtk4::gdk::Key::n | gtk4::gdk::Key::N if ctrl && on_browser => prompt_new_folder(&ui),
|
gtk4::gdk::Key::n | gtk4::gdk::Key::N if ctrl && on_browser => prompt_new_folder(&ui),
|
||||||
|
gtk4::gdk::Key::_1 if ctrl && on_browser => set_files_view(
|
||||||
|
&ui,
|
||||||
|
FilesView {
|
||||||
|
list: false,
|
||||||
|
..ui.browser.view.get()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
gtk4::gdk::Key::_2 if ctrl && on_browser => set_files_view(
|
||||||
|
&ui,
|
||||||
|
FilesView {
|
||||||
|
list: true,
|
||||||
|
..ui.browser.view.get()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
gtk4::gdk::Key::Left if alt && on_browser => {
|
||||||
|
ui.browser.actions.activate_action("back", None)
|
||||||
|
}
|
||||||
|
gtk4::gdk::Key::Right if alt && on_browser => {
|
||||||
|
ui.browser.actions.activate_action("forward", None)
|
||||||
|
}
|
||||||
|
gtk4::gdk::Key::Up if alt && on_browser => {
|
||||||
|
ui.browser.actions.activate_action("up", None)
|
||||||
|
}
|
||||||
gtk4::gdk::Key::u | gtk4::gdk::Key::U if ctrl && on_browser => prompt_upload(&ui),
|
gtk4::gdk::Key::u | gtk4::gdk::Key::U if ctrl && on_browser => prompt_upload(&ui),
|
||||||
gtk4::gdk::Key::F2 if on_browser => {
|
gtk4::gdk::Key::F2 if on_browser => {
|
||||||
// Renaming is one name at a time; saying so beats a key that
|
// Renaming is one name at a time; saying so beats a key that
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -726,14 +726,7 @@ fn quota_status_display(max_space: i64, used_space: i64) -> Option<(f64, String,
|
|||||||
/// that can only fail. A greyed control says so up front.
|
/// that can only fail. A greyed control says so up front.
|
||||||
pub(crate) fn set_mounted(ui: &Rc<Ui>, mounted: bool) {
|
pub(crate) fn set_mounted(ui: &Rc<Ui>, mounted: bool) {
|
||||||
*ui.mounted.borrow_mut() = mounted;
|
*ui.mounted.borrow_mut() = mounted;
|
||||||
ui.browser.new_folder.set_sensitive(mounted);
|
sync_mounted_actions(ui, mounted);
|
||||||
ui.browser.upload.set_sensitive(mounted);
|
|
||||||
ui.browser.upload_folder.set_sensitive(mounted);
|
|
||||||
ui.browser.build_thumbnails.set_sensitive(
|
|
||||||
mounted
|
|
||||||
&& (!ui.browser.thumbnail_build_running.get()
|
|
||||||
|| !ui.browser.thumbnail_cancel_pending.get()),
|
|
||||||
);
|
|
||||||
ui.gallery.upload.set_sensitive(mounted);
|
ui.gallery.upload.set_sensitive(mounted);
|
||||||
ui.details.details.pin_row.set_sensitive(mounted);
|
ui.details.details.pin_row.set_sensitive(mounted);
|
||||||
ui.details.details.rename_button.set_sensitive(mounted);
|
ui.details.details.rename_button.set_sensitive(mounted);
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ Schema: **30** (unchanged).
|
|||||||
state, storage and the account; the Settings page became a Preferences dialog (Ctrl+,), with
|
state, storage and the account; the Settings page became a Preferences dialog (Ctrl+,), with
|
||||||
transfers on the Sync page and developer details in About. The app follows the system accent
|
transfers on the Sync page and developer details in About. The app follows the system accent
|
||||||
colour; the Proton purple accent is a preference.
|
colour; the Proton purple accent is a preference.
|
||||||
|
- **My Files header.** New folder and both uploads sit in one **New** menu; the view button switches
|
||||||
|
grid and list, and its menu sorts by name, size or date, reversed or not, folders first or not.
|
||||||
|
The choice is remembered. Build Thumbnails and Open in File Manager moved to the folder menu.
|
||||||
|
- **Browsing history.** Back and forward buttons (Alt+Left/Right, mouse back/forward) replace the
|
||||||
|
single Up button; Alt+Up goes to the parent folder. Back leaves a search first.
|
||||||
|
- **Move to…** picks the destination from a folder browser instead of a typed path, and works on a
|
||||||
|
whole selection. Rename selects the name without its extension.
|
||||||
|
- **Drop to upload.** Files and folders dropped on My Files upload into the folder on screen. The
|
||||||
|
upload toast links to the Sync page.
|
||||||
|
|
||||||
## [1.11.1] — 2026-09-19
|
## [1.11.1] — 2026-09-19
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user