From 6a2bd1faf1efb9ee2773ef590de63b29eee1444b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 30 Jun 2025 19:23:40 -0400 Subject: [PATCH] fix: add eject button for context menus on mount point --- i18n/en/cosmic_files.ftl | 1 + src/app.rs | 58 +++++++++++++++++++++++++++++++++++----- src/dialog.rs | 17 +++++++++++- src/menu.rs | 24 +++++++++++------ src/mounter/gvfs.rs | 1 + src/tab.rs | 5 ++++ 6 files changed, 90 insertions(+), 16 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index d676382..4181a99 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -291,6 +291,7 @@ type-to-search-enter-path = Enters the path to the directory or file add-to-sidebar = Add to sidebar compress = Compress delete-permanently = Delete permanently +eject = Eject extract-here = Extract new-file = New file... new-folder = New folder... diff --git a/src/app.rs b/src/app.rs index 98df57b..e51d46c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -117,6 +117,7 @@ pub enum Action { Delete, EditHistory, EditLocation, + Eject, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(usize), @@ -184,6 +185,7 @@ impl Action { Action::EditLocation => { Message::TabMessage(entity_opt, tab::Message::EditLocationEnable) } + Action::Eject => Message::Eject, Action::EmptyTrash => Message::TabMessage(None, tab::Message::EmptyTrash), Action::ExtractHere => Message::ExtractHere(entity_opt), Action::ExtractTo => Message::ExtractTo(entity_opt), @@ -304,6 +306,7 @@ pub enum Message { DesktopViewOptions, DialogCancel, DialogComplete, + Eject, FileDialogMessage(DialogMessage), DialogPush(DialogPage), DialogUpdate(DialogPage), @@ -1044,17 +1047,36 @@ impl App { ) -> Task { log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}"); let icon_sizes = self.config.tab.icon_sizes; + let mounter_items = self.mounter_items.clone(); + Task::perform( async move { let location2 = location.clone(); match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, items)) => cosmic::action::app(Message::TabRescan( - entity, - location, - parent_item_opt, - items, - selection_paths, - )), + Ok((parent_item_opt, mut items)) => { + #[cfg(feature = "gvfs")] + { + let mounter_paths: Vec<_> = mounter_items + .iter() + .flat_map(|item| item.1.iter()) + .filter_map(|item| item.path()) + .collect(); + if !mounter_paths.is_empty() { + for item in &mut items { + item.is_mount_point = + item.path_opt().is_some_and(|p| mounter_paths.contains(p)); + } + } + } + + cosmic::action::app(Message::TabRescan( + entity, + location, + parent_item_opt, + items, + selection_paths, + )) + } Err(err) => { log::warn!("failed to rescan: {}", err); cosmic::action::none() @@ -4136,6 +4158,28 @@ impl Application for App { self.size = Some(size); self.handle_overlap(); } + Message::Eject => { + #[cfg(feature = "gvfs")] + { + let paths = self.selected_paths(None); + if let Some(p) = paths.first() { + { + for (k, mounter_items) in &self.mounter_items { + if let Some(mounter) = MOUNTERS.get(&k) { + if let Some(item) = mounter_items + .iter() + .find(|item| item.path().is_some_and(|path| path == *p)) + { + return mounter + .unmount(item.clone()) + .map(|_| cosmic::action::none()); + } + } + } + } + } + } + } #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Message::Focused(id) => { if let Some(w) = self.windows.get(&id) { diff --git a/src/dialog.rs b/src/dialog.rs index d75f100..7506e37 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -650,11 +650,26 @@ impl App { fn rescan_tab(&self) -> Task { let location = self.tab.location.clone(); let icon_sizes = self.tab.config.icon_sizes; + let mounter_items = self.mounter_items.clone(); Task::perform( async move { let location2 = location.clone(); match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, items)) => { + Ok((parent_item_opt, mut items)) => { + #[cfg(feature = "gvfs")] + { + let mounter_paths: Vec<_> = mounter_items + .iter() + .flat_map(|item| item.1.iter()) + .filter_map(|item| item.path()) + .collect(); + if !mounter_paths.is_empty() { + for item in &mut items { + item.is_mount_point = + item.path_opt().is_some_and(|p| mounter_paths.contains(p)); + } + } + } cosmic::action::app(Message::TabRescan(location, parent_item_opt, items)) } Err(err) => { diff --git a/src/menu.rs b/src/menu.rs index f9fc7b2..e1ee2a9 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -108,11 +108,13 @@ pub fn context_menu<'a>( let mut selected_trash_only = false; let mut selected_desktop_entry = None; let mut selected_types: Vec = vec![]; + let mut selected_mount_point = 0; if let Some(items) = tab.items_opt() { for item in items.iter() { if item.selected { selected += 1; if item.metadata.is_dir() { + selected_mount_point += item.is_mount_point as i32; selected_dir += 1; } match &item.location_opt { @@ -194,8 +196,10 @@ pub fn context_menu<'a>( .push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into()); } children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("rename"), Action::Rename).into()); - children.push(menu_item(fl!("cut"), Action::Cut).into()); + if selected_mount_point == 0 { + children.push(menu_item(fl!("rename"), Action::Rename).into()); + children.push(menu_item(fl!("cut"), Action::Cut).into()); + } children.push(menu_item(fl!("copy"), Action::Copy).into()); children.push(divider::horizontal::light().into()); @@ -235,12 +239,16 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); } children.push(divider::horizontal::light().into()); - if modifiers.shift() && !modifiers.control() { - children.push( - menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(), - ); - } else { - children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + if selected_mount_point == 0 { + if modifiers.shift() && !modifiers.control() { + children.push( + menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(), + ); + } else { + children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + } + } else if selected == 1 { + children.push(menu_item(fl!("eject"), Action::Eject).into()); } } else { //TODO: need better designs for menu with no selection diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 810b60d..a728ff6 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -109,6 +109,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { items.push(tab::Item { name, + is_mount_point: false, display_name, metadata, hidden: false, diff --git a/src/tab.rs b/src/tab.rs index 669b61d..81ae75e 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -668,6 +668,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS Item { name: file_name.clone().to_string(), display_name, + is_mount_point: false, metadata: ItemMetadata::GvfsPath { mtime, size_opt, @@ -809,6 +810,7 @@ pub fn item_from_entry( Item { name, display_name, + is_mount_point: false, metadata: ItemMetadata::Path { metadata, children_opt, @@ -1090,6 +1092,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { items.push(Item { name, display_name, + is_mount_point: false, metadata: ItemMetadata::Trash { metadata, entry }, hidden: false, location_opt: None, @@ -1270,6 +1273,7 @@ pub fn scan_desktop( items.push(Item { name, display_name, + is_mount_point: false, metadata, hidden: false, location_opt: Some(Location::Trash), @@ -1821,6 +1825,7 @@ impl ItemThumbnail { #[derive(Clone, Debug)] pub struct Item { pub name: String, + pub is_mount_point: bool, pub display_name: String, pub metadata: ItemMetadata, pub hidden: bool,