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.
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@@ -139,6 +196,7 @@ impl Default for AppConfig {
|
||||
open_with: None,
|
||||
prompt: None,
|
||||
proton_accent: None,
|
||||
files_view: FilesView::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,12 +542,29 @@ mod tests {
|
||||
open_with: None,
|
||||
prompt: None,
|
||||
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 decoded: AppConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0");
|
||||
assert_eq!(decoded.user_agent, "test-agent/1.0");
|
||||
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]
|
||||
|
||||
@@ -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); }
|
||||
.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); }
|
||||
.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-window { background-color: #111014; }
|
||||
|
||||
@@ -44,7 +44,7 @@ use gtk4::glib::BoxedAnyObject;
|
||||
|
||||
use pdfs_core::auth;
|
||||
|
||||
use pdfs_core::config::AppDirs;
|
||||
use pdfs_core::config::{AppDirs, FileSort, FilesView};
|
||||
|
||||
use pdfs_core::control::{
|
||||
ActivityEntry, ActivityKind, AlbumInfo, BookmarkInfo, ConflictInfo, ConflictKeep, DeviceInfo,
|
||||
@@ -394,7 +394,8 @@ fn build_window(app: &adw::Application) {
|
||||
},
|
||||
browser: BrowserState {
|
||||
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(),
|
||||
content: browser_widgets.content.clone(),
|
||||
status: browser_widgets.status.clone(),
|
||||
@@ -402,9 +403,10 @@ fn build_window(app: &adw::Application) {
|
||||
split: browser_widgets.split.clone(),
|
||||
path: RefCell::new(String::new()),
|
||||
search: browser_widgets.search.clone(),
|
||||
new_folder: browser_widgets.new_folder.clone(),
|
||||
upload: browser_widgets.upload.clone(),
|
||||
upload_folder: browser_widgets.upload_folder.clone(),
|
||||
actions: browser_widgets.actions.clone(),
|
||||
view_button: browser_widgets.view_button.clone(),
|
||||
view: Cell::new(FilesView::default()),
|
||||
listing: RefCell::new(Vec::new()),
|
||||
build_thumbnails: browser_widgets.build_thumbnails.clone(),
|
||||
thumbnail_build_row: browser_widgets.thumbnail_build_row.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_pin: browser_widgets.bulk_pin.clone(),
|
||||
bulk_unpin: browser_widgets.bulk_unpin.clone(),
|
||||
bulk_move: browser_widgets.bulk_move.clone(),
|
||||
empty_actions: browser_widgets.empty_actions.clone(),
|
||||
summary: browser_widgets.summary.clone(),
|
||||
zoom: browser_widgets.zoom.clone(),
|
||||
@@ -609,13 +612,7 @@ fn build_window(app: &adw::Application) {
|
||||
&browser_widgets.empty_upload,
|
||||
&browser_widgets.empty_new_folder,
|
||||
);
|
||||
wire_browser_actions(
|
||||
&ui,
|
||||
&browser_widgets.new_folder,
|
||||
&browser_widgets.upload,
|
||||
&browser_widgets.upload_folder,
|
||||
&browser_widgets.build_thumbnails,
|
||||
);
|
||||
wire_browser_actions(&ui, &browser_widgets.build_thumbnails);
|
||||
wire_details(&ui);
|
||||
wire_search(&ui);
|
||||
wire_gallery(
|
||||
@@ -1197,6 +1194,9 @@ fn show_shortcuts(window: &adw::ApplicationWindow) {
|
||||
&[
|
||||
("<Control>n", "New folder"),
|
||||
("<Control>u", "Upload files"),
|
||||
("<Alt>Left <Alt>Right", "Back / forward"),
|
||||
("<Alt>Up", "Parent folder"),
|
||||
("<Control>1 <Control>2", "Grid / list"),
|
||||
("F2", "Rename"),
|
||||
("Delete", "Move to Trash"),
|
||||
("Escape", "Clear the selection"),
|
||||
@@ -1260,6 +1260,7 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
|
||||
let ui = ui.clone();
|
||||
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");
|
||||
match key {
|
||||
// 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();
|
||||
}
|
||||
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::F2 if on_browser => {
|
||||
// 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.
|
||||
pub(crate) fn set_mounted(ui: &Rc<Ui>, mounted: bool) {
|
||||
*ui.mounted.borrow_mut() = mounted;
|
||||
ui.browser.new_folder.set_sensitive(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()),
|
||||
);
|
||||
sync_mounted_actions(ui, mounted);
|
||||
ui.gallery.upload.set_sensitive(mounted);
|
||||
ui.details.details.pin_row.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
|
||||
transfers on the Sync page and developer details in About. The app follows the system accent
|
||||
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
|
||||
|
||||
|
||||
Reference in new issue
Block a user