From 0d8fd00dd33399bc822f8baef520b843c34ac87a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 12 Sep 2024 15:54:54 -0600 Subject: [PATCH] Implement network drive connection, part of #202 --- Cargo.lock | 46 +++---- i18n/en/cosmic_files.ftl | 18 ++- src/app.rs | 289 ++++++++++++++++++++++++++++++++++++++- src/dialog.rs | 16 ++- src/menu.rs | 3 + src/mounter/gvfs.rs | 106 +++++++++++++- src/mounter/mod.rs | 43 +++++- src/tab.rs | 107 +++++++++------ 8 files changed, 542 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adc140c..fe3771e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,7 +1213,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1232,7 +1232,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "quote", "syn 1.0.109", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "almost", "cosmic-config", @@ -2782,7 +2782,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "dnd", "iced_accessibility", @@ -2801,7 +2801,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "accesskit", "accesskit_unix", @@ -2811,7 +2811,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "bitflags 2.6.0", "dnd", @@ -2833,7 +2833,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "futures", "iced_core", @@ -2846,7 +2846,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -2870,7 +2870,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2882,7 +2882,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "dnd", "iced_accessibility", @@ -2896,7 +2896,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "enum-repr", "float-cmp", @@ -2923,7 +2923,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "iced_core", "once_cell", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "bytemuck", "cosmic-text", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "as-raw-xcb-connection", "bitflags 2.6.0", @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "dnd", "iced_accessibility", @@ -2997,7 +2997,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "dnd", "iced_accessibility", @@ -3513,7 +3513,7 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8" +source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733" dependencies = [ "apply", "ashpd 0.9.1", @@ -3601,7 +3601,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.4", ] [[package]] @@ -3736,9 +3736,9 @@ dependencies = [ [[package]] name = "mac-notification-sys" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91" dependencies = [ "cc", "dirs-next", @@ -4436,7 +4436,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.4", "smallvec", "windows-targets 0.52.6", ] @@ -4847,9 +4847,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ "bitflags 2.6.0", ] diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 9407db7..abf4b69 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -4,6 +4,7 @@ empty-folder-hidden = Empty folder (has hidden items) no-results = No results found filesystem = Filesystem home = Home +networks = Networks notification-in-progress = File operations are in progress. trash = Trash recents = Recents @@ -63,7 +64,6 @@ apply-to-all = Apply to all keep-both = Keep both skip = Skip - ## Metadata Dialog owner = Owner group = Group @@ -77,6 +77,22 @@ execute = Execute ## About git-description = Git commit {$hash} on {$date} +## Add Network Drive +add-network-drive = Add network drive +connect = Connect +connect-anonymously = Connect anonymously +connecting = Connecting... +domain = Domain +enter-server-address = Enter server address +network-drive-description = + Server addresses include a protocol prefix and address. + Examples: ssh://192.168.0.1, ftp://[2001:db8::1] +network-drive-error = Unable to access network drive +password = Password +remember-password = Remember password +try-again = Try again +username = Username + ## Operations edit-history = Edit history history = History diff --git a/src/app.rs b/src/app.rs index 71d5d54..bb7722a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -63,7 +63,9 @@ use crate::{ key_bind::key_binds, localize::LANGUAGE_SORTER, menu, mime_app, mime_icon, - mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters}, + mounter::{ + mounters, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters, + }, operation::{Operation, ReplaceResult}, spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, @@ -246,6 +248,7 @@ pub enum Message { DialogComplete, DialogPush(DialogPage), DialogUpdate(DialogPage), + DialogUpdateComplete(DialogPage), EditLocation(Option), ExtractHere(Option), Key(Modifiers, Key), @@ -257,6 +260,10 @@ pub enum Message { NavBarClose(Entity), NavBarContext(Entity), NavMenuAction(NavMenuAction), + NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender), + NetworkDriveInput(String), + NetworkDriveSubmit, + NetworkResult(MounterKey, String, Result), NewItem(Option, bool), #[cfg(feature = "notify")] Notification(Arc>), @@ -314,6 +321,7 @@ pub enum Message { pub enum ContextPage { About, EditHistory, + NetworkDrive, OpenWith, Properties(Option), Settings, @@ -324,6 +332,7 @@ impl ContextPage { match self { Self::About => String::new(), Self::EditHistory => fl!("edit-history"), + Self::NetworkDrive => fl!("add-network-drive"), Self::OpenWith => fl!("open-with"), Self::Properties(..) => String::default(), Self::Settings => fl!("settings"), @@ -367,6 +376,17 @@ pub enum DialogPage { }, EmptyTrash, FailedOperation(u64), + NetworkAuth { + mounter_key: MounterKey, + uri: String, + auth: MounterAuth, + auth_tx: mpsc::Sender, + }, + NetworkError { + mounter_key: MounterKey, + uri: String, + error: String, + }, NewItem { parent: PathBuf, name: String, @@ -433,6 +453,8 @@ pub struct App { modifiers: Modifiers, mounters: Mounters, mounter_items: HashMap, + network_drive_connecting: Option<(MounterKey, String)>, + network_drive_input: String, #[cfg(feature = "notify")] notification_opt: Option>>, pending_operation_id: u64, @@ -635,6 +657,7 @@ impl App { }); } } + nav_model = nav_model.insert(|b| { b.text(fl!("trash")) .icon(widget::icon::icon(tab::trash_icon_symbolic(16))) @@ -642,6 +665,17 @@ impl App { .divider_above() }); + nav_model = nav_model.insert(|b| { + b.text(fl!("networks")) + .icon(widget::icon::icon( + widget::icon::from_name("network-workgroup-symbolic") + .size(16) + .handle(), + )) + .data(Location::Networks) + .divider_above() + }); + // Collect all mounter items let mut nav_items = Vec::new(); for (key, items) in self.mounter_items.iter() { @@ -794,6 +828,31 @@ impl App { .into() } + fn network_drive(&self) -> Element { + let cosmic_theme::Spacing { space_m, .. } = theme::active().cosmic().spacing; + let mut text_input = + widget::text_input(fl!("enter-server-address"), &self.network_drive_input); + let button = if self.network_drive_connecting.is_some() { + widget::button::standard(fl!("connecting")) + } else { + text_input = text_input + .on_input(Message::NetworkDriveInput) + .on_submit(Message::NetworkDriveSubmit); + widget::button::standard(fl!("connect")).on_press(Message::NetworkDriveSubmit) + }; + widget::column::with_children(vec![ + text_input.into(), + widget::text(fl!("network-drive-description")).into(), + widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + button.into(), + ]) + .into(), + ]) + .spacing(space_m) + .into() + } + fn open_with(&self) -> Element { let mut children = Vec::new(); let entity = self.tab_model.active(); @@ -1118,6 +1177,8 @@ impl Application for App { modifiers: Modifiers::empty(), mounters: mounters(), mounter_items: HashMap::new(), + network_drive_connecting: None, + network_drive_input: String::new(), #[cfg(feature = "notify")] notification_opt: None, pending_operation_id: 0, @@ -1422,6 +1483,31 @@ impl Application for App { DialogPage::FailedOperation(id) => { log::warn!("TODO: retry operation {}", id); } + DialogPage::NetworkAuth { + mounter_key, + uri, + auth, + auth_tx, + } => { + return Command::perform( + async move { + auth_tx.send(auth).await.unwrap(); + message::none() + }, + |x| x, + ); + } + DialogPage::NetworkError { + mounter_key, + uri, + error, + } => { + //TODO: re-use mounter_key? + return Command::batch([ + self.update(Message::NetworkDriveInput(uri)), + self.update(Message::NetworkDriveSubmit), + ]); + } DialogPage::NewItem { parent, name, dir } => { let path = parent.join(name); self.operation(if dir { @@ -1450,6 +1536,12 @@ impl Application for App { self.dialog_pages[0] = dialog_page; } } + Message::DialogUpdateComplete(dialog_page) => { + return Command::batch([ + self.update(Message::DialogUpdate(dialog_page)), + self.update(Message::DialogComplete), + ]); + } Message::EditLocation(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(location) = self.tab_model.data::(entity).and_then(|tab| { @@ -1569,6 +1661,55 @@ impl Application for App { return Command::batch(commands); } + Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => { + self.dialog_pages.push_back(DialogPage::NetworkAuth { + mounter_key, + uri, + auth, + auth_tx, + }); + } + Message::NetworkDriveInput(input) => { + self.network_drive_input = input; + } + Message::NetworkDriveSubmit => { + //TODO: know which mounter to use for network drives + for (mounter_key, mounter) in self.mounters.iter() { + self.network_drive_connecting = + Some((*mounter_key, self.network_drive_input.clone())); + return mounter + .network_drive(self.network_drive_input.clone()) + .map(|_| message::none()); + } + log::warn!( + "no mounter found for connecting to {:?}", + self.network_drive_input + ); + } + Message::NetworkResult(mounter_key, uri, res) => { + if self.network_drive_connecting == Some((mounter_key, uri.clone())) { + self.network_drive_connecting = None; + } + match res { + Ok(true) => { + log::info!("connected to {:?}", uri); + if matches!(self.context_page, ContextPage::NetworkDrive) { + self.core.window.show_context = false; + } + } + Ok(false) => { + log::info!("cancelled connection to {:?}", uri); + } + Err(error) => { + log::warn!("failed to connect to {:?}: {}", uri, error); + self.dialog_pages.push_back(DialogPage::NetworkError { + mounter_key, + uri, + error, + }); + } + } + } Message::NewItem(entity_opt, dir) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { @@ -2090,6 +2231,12 @@ impl Application for App { tab::Command::Action(action) => { commands.push(self.update(action.message(Some(entity)))); } + tab::Command::AddNetworkDrive => { + let context_page = ContextPage::NetworkDrive; + self.context_page = context_page; + self.core.window.show_context = true; + self.set_context_title(context_page.title()); + } tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => { self.activate_nav_model_location(&tab_path); @@ -2544,6 +2691,7 @@ impl Application for App { Some(match self.context_page { ContextPage::About => self.about(), ContextPage::EditHistory => self.edit_history(), + ContextPage::NetworkDrive => self.network_drive(), ContextPage::OpenWith => self.open_with(), ContextPage::Properties(entity) => self.properties(entity), ContextPage::Settings => self.settings(), @@ -2556,7 +2704,9 @@ impl Application for App { None => return None, }; - let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { + space_xxs, space_s, .. + } = theme::active().cosmic().spacing; let dialog = match dialog_page { DialogPage::Compress { @@ -2658,6 +2808,125 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } + DialogPage::NetworkAuth { + mounter_key, + uri, + auth, + auth_tx, + } => { + //TODO: use URI! + let mut controls = Vec::with_capacity(4); + if let Some(username) = &auth.username_opt { + //TODO: what should submit do? + controls.push( + widget::text_input(fl!("username"), username) + .on_input(move |value| { + Message::DialogUpdate(DialogPage::NetworkAuth { + mounter_key: *mounter_key, + uri: uri.clone(), + auth: MounterAuth { + username_opt: Some(value), + ..auth.clone() + }, + auth_tx: auth_tx.clone(), + }) + }) + .into(), + ); + } + if let Some(domain) = &auth.domain_opt { + //TODO: what should submit do? + controls.push( + widget::text_input(fl!("domain"), domain) + .on_input(move |value| { + Message::DialogUpdate(DialogPage::NetworkAuth { + mounter_key: *mounter_key, + uri: uri.clone(), + auth: MounterAuth { + domain_opt: Some(value), + ..auth.clone() + }, + auth_tx: auth_tx.clone(), + }) + }) + .into(), + ); + } + if let Some(password) = &auth.password_opt { + //TODO: what should submit do? + //TODO: button for showing password + controls.push( + widget::secure_input(fl!("password"), password, None, true) + .on_input(move |value| { + Message::DialogUpdate(DialogPage::NetworkAuth { + mounter_key: *mounter_key, + uri: uri.clone(), + auth: MounterAuth { + password_opt: Some(value), + ..auth.clone() + }, + auth_tx: auth_tx.clone(), + }) + }) + .into(), + ); + } + if let Some(remember) = &auth.remember_opt { + //TODO: what should submit do? + //TODO: button for showing password + controls.push( + widget::checkbox(fl!("remember-password"), *remember, move |value| { + Message::DialogUpdate(DialogPage::NetworkAuth { + mounter_key: *mounter_key, + uri: uri.clone(), + auth: MounterAuth { + remember_opt: Some(value), + ..auth.clone() + }, + auth_tx: auth_tx.clone(), + }) + }) + .into(), + ); + } + + let mut parts = auth.message.splitn(2, '\n'); + let title = parts.next().unwrap_or_default(); + let body = parts.next().unwrap_or_default(); + widget::dialog(title) + .body(body) + .control(widget::column::with_children(controls).spacing(space_s)) + .primary_action( + widget::button::suggested(fl!("connect")).on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .tertiary_action(widget::button::text(fl!("connect-anonymously")).on_press( + Message::DialogUpdateComplete(DialogPage::NetworkAuth { + mounter_key: *mounter_key, + uri: uri.clone(), + auth: MounterAuth { + anonymous_opt: Some(true), + ..auth.clone() + }, + auth_tx: auth_tx.clone(), + }), + )) + } + DialogPage::NetworkError { + mounter_key, + uri, + error, + } => widget::dialog(fl!("network-drive-error")) + .body(error) + .icon(widget::icon::from_name("dialog-error").size(64)) + .primary_action( + widget::button::standard(fl!("try-again")).on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ), DialogPage::NewItem { parent, name, dir } => { let mut dialog = widget::dialog(if *dir { fl!("create-new-folder") @@ -3148,11 +3417,17 @@ impl Application for App { for (key, mounter) in self.mounters.iter() { let key = *key; - subscriptions.push( - mounter - .subscription() - .map(move |items| Message::MounterItems(key, items)), - ); + subscriptions.push(mounter.subscription().map(move |mounter_message| { + match mounter_message { + MounterMessage::Items(items) => Message::MounterItems(key, items), + MounterMessage::NetworkAuth(uri, auth, auth_tx) => { + Message::NetworkAuth(key, uri, auth, auth_tx) + } + MounterMessage::NetworkResult(uri, res) => { + Message::NetworkResult(key, uri, res) + } + } + })); } if !self.pending_operations.is_empty() { diff --git a/src/dialog.rs b/src/dialog.rs index 64ee627..3440760 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -44,7 +44,7 @@ use crate::{ fl, home_dir, localize::LANGUAGE_SORTER, menu, - mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters}, + mounter::{mounters, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters}, tab::{self, ItemMetadata, Location, Tab}, }; @@ -1329,11 +1329,15 @@ impl Application for App { for (key, mounter) in self.mounters.iter() { let key = *key; - subscriptions.push( - mounter - .subscription() - .map(move |items| Message::MounterItems(key, items)), - ); + subscriptions.push(mounter.subscription().map(move |mounter_message| { + match mounter_message { + MounterMessage::Items(items) => Message::MounterItems(key, items), + _ => { + log::warn!("{:?} not supported in dialog mode", mounter_message); + Message::None + } + } + })); } Subscription::batch(subscriptions) diff --git a/src/menu.rs b/src/menu.rs index acf6903..86b1d08 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -197,6 +197,9 @@ pub fn context_menu<'a>( children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); } } + (_, Location::Networks) => { + //TODO: networks context menu? + } (_, Location::Trash) => { if tab.mode.multiple() { children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 17bdc79..7ad7b4c 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -6,7 +6,7 @@ use gio::{glib, prelude::*}; use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc}; use tokio::sync::{mpsc, Mutex}; -use super::{Mounter, MounterItem, MounterItems}; +use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { if let Some(themed_icon) = icon.downcast_ref::() { @@ -24,12 +24,15 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { enum Cmd { Rescan, Mount(MounterItem), + NetworkDrive(String), Unmount(MounterItem), } enum Event { Changed, Items(MounterItems), + NetworkAuth(String, MounterAuth, mpsc::Sender), + NetworkResult(String, Result), } #[derive(Clone, Debug)] @@ -190,6 +193,80 @@ impl Gvfs { ); } } + Cmd::NetworkDrive(uri) => { + let mount_op = gio::MountOperation::new(); + + { + let event_tx = event_tx.clone(); + let uri = uri.clone(); + mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| { + let auth = MounterAuth { + message: message.to_string(), + username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { + Some(default_user.to_string()) + } else { + None + }, + domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { + Some(default_domain.to_string()) + } else { + None + }, + password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { + Some(String::new()) + } else { + None + }, + remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) { + Some(false) + } else { + None + }, + anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) { + Some(false) + } else { + None + } + }; + let (auth_tx, mut auth_rx) = mpsc::channel(1); + event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap(); + //TODO: async recv? + if let Some(auth) = auth_rx.blocking_recv() { + if auth.anonymous_opt == Some(true) { + mount_op.set_anonymous(true); + } else { + mount_op.set_username(auth.username_opt.as_deref()); + mount_op.set_domain(auth.domain_opt.as_deref()); + mount_op.set_password(auth.password_opt.as_deref()); + if auth.remember_opt == Some(true) { + mount_op.set_password_save(gio::PasswordSave::Permanently); + } + } + mount_op.reply(gio::MountOperationResult::Handled); + } else { + mount_op.reply(gio::MountOperationResult::Aborted); + } + }); + } + + let file = gio::File::for_uri(&uri); + let event_tx = event_tx.clone(); + file.mount_enclosing_volume( + gio::MountMountFlags::empty(), + Some(&mount_op), + gio::Cancellable::NONE, + move |res| { + log::info!("network drive {}: result {:?}", uri, res); + event_tx.send(Event::NetworkResult(uri, match res { + Ok(()) => Ok(true), + Err(err) => match err.kind::() { + Some(gio::IOErrorEnum::FailedHandled) => Ok(false), + _ => Err(format!("{}", err)) + } + })).unwrap(); + } + ); + } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Mount = item.kind else { continue }; @@ -242,6 +319,17 @@ impl Mounter for Gvfs { ) } + fn network_drive(&self, uri: String) -> Command<()> { + let command_tx = self.command_tx.clone(); + Command::perform( + async move { + command_tx.send(Cmd::NetworkDrive(uri)).unwrap(); + () + }, + |x| x, + ) + } + fn unmount(&self, item: MounterItem) -> Command<()> { let command_tx = self.command_tx.clone(); Command::perform( @@ -253,17 +341,23 @@ impl Mounter for Gvfs { ) } - fn subscription(&self) -> subscription::Subscription { + fn subscription(&self) -> subscription::Subscription { let command_tx = self.command_tx.clone(); let event_rx = self.event_rx.clone(); subscription::channel(TypeId::of::(), 1, |mut output| async move { command_tx.send(Cmd::Rescan).unwrap(); while let Some(event) = event_rx.lock().await.recv().await { match event { - Event::Changed => { - command_tx.send(Cmd::Rescan).unwrap(); - } - Event::Items(items) => output.send(items).await.unwrap(), + Event::Changed => command_tx.send(Cmd::Rescan).unwrap(), + Event::Items(items) => output.send(MounterMessage::Items(items)).await.unwrap(), + Event::NetworkAuth(uri, auth, auth_tx) => output + .send(MounterMessage::NetworkAuth(uri, auth, auth_tx)) + .await + .unwrap(), + Event::NetworkResult(uri, res) => output + .send(MounterMessage::NetworkResult(uri, res)) + .await + .unwrap(), } } pending().await diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index 79f90ad..bca9622 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -1,9 +1,40 @@ use cosmic::{iced::subscription, widget, Command}; -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; +use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc}; +use tokio::sync::mpsc; #[cfg(feature = "gvfs")] mod gvfs; +#[derive(Clone)] +pub struct MounterAuth { + pub message: String, + pub username_opt: Option, + pub domain_opt: Option, + pub password_opt: Option, + pub remember_opt: Option, + pub anonymous_opt: Option, +} + +// Custom debug for MounterAuth to hide password +impl fmt::Debug for MounterAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MounterAuth") + .field("username_opt", &self.username_opt) + .field("domain_opt", &self.domain_opt) + .field( + "password_opt", + if self.password_opt.is_some() { + &"Some(*)" + } else { + &"None" + }, + ) + .field("remember_opt", &self.remember_opt) + .field("anonymous_opt", &self.anonymous_opt) + .finish() + } +} + #[derive(Clone, Debug)] pub enum MounterItem { #[cfg(feature = "gvfs")] @@ -48,11 +79,19 @@ impl MounterItem { pub type MounterItems = Vec; +#[derive(Clone, Debug)] +pub enum MounterMessage { + Items(MounterItems), + NetworkAuth(String, MounterAuth, mpsc::Sender), + NetworkResult(String, Result), +} + pub trait Mounter { //TODO: send result fn mount(&self, item: MounterItem) -> Command<()>; + fn network_drive(&self, uri: String) -> Command<()>; fn unmount(&self, item: MounterItem) -> Command<()>; - fn subscription(&self) -> subscription::Subscription; + fn subscription(&self) -> subscription::Subscription; } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/src/tab.rs b/src/tab.rs index e4a0463..18293e0 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -724,12 +724,18 @@ pub fn scan_recents(sizes: IconSizes) -> Vec { recents.into_iter().take(50).map(|(item, _)| item).collect() } +pub fn scan_networks(sizes: IconSizes) -> Vec { + //TODO: network folder items + vec![] +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Location { Path(PathBuf), Search(PathBuf, String), Trash, Recents, + Networks, } impl std::fmt::Display for Location { @@ -739,6 +745,7 @@ impl std::fmt::Display for Location { Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term), Self::Trash => write!(f, "trash"), Self::Recents => write!(f, "recents"), + Self::Networks => write!(f, "networks"), } } } @@ -750,6 +757,7 @@ impl Location { Self::Search(path, term) => scan_search(path, term, sizes), Self::Trash => scan_trash(sizes), Self::Recents => scan_recents(sizes), + Self::Networks => scan_networks(sizes), } } } @@ -757,6 +765,7 @@ impl Location { #[derive(Debug)] pub enum Command { Action(Action), + AddNetworkDrive, ChangeLocation(String, Location, Option), DropFiles(PathBuf, ClipboardPaste), EmptyTrash, @@ -770,6 +779,7 @@ pub enum Command { #[derive(Clone, Debug)] pub enum Message { + AddNetworkDrive, Click(Option), DoubleClick(Option), ClickRelease(Option), @@ -1227,7 +1237,6 @@ impl Tab { } pub fn title(&self) -> String { - //TODO: better title match &self.location { Location::Path(path) => { let (name, _) = folder_name(path); @@ -1244,6 +1253,9 @@ impl Tab { Location::Recents => { fl!("recents") } + Location::Networks => { + fl!("networks") + } } } @@ -1518,6 +1530,9 @@ impl Tab { let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); match message { + Message::AddNetworkDrive => { + commands.push(Command::AddNetworkDrive); + } Message::ClickRelease(click_i_opt) => { if click_i_opt == self.clicked.take() { return commands; @@ -1931,13 +1946,9 @@ impl Tab { commands.push(Command::OpenFile(path.clone())); } } - Location::Search(_path, _term) => { + _ => { cd = Some(location); } - Location::Trash => { - cd = Some(location); - } - Location::Recents => cd = Some(location), } } Message::LocationUp => { @@ -2086,6 +2097,9 @@ impl Tab { Location::Recents => { log::warn!("Copy to recents is not supported."); } + Location::Networks => { + log::warn!("Copy to networks is not supported."); + } }; } Message::Drop(None) => { @@ -2170,8 +2184,7 @@ impl Tab { if match &location { Location::Path(path) => path.is_dir(), Location::Search(path, _term) => path.is_dir(), - Location::Trash => true, - Location::Recents => true, + _ => true, } { let prev_path = if let Location::Path(path) = &self.location { Some(path.clone()) @@ -2531,14 +2544,8 @@ impl Tab { children.reverse(); } Location::Trash => { - let mut row = widget::row::with_capacity(2) - .align_items(Alignment::Center) - .spacing(space_xxxs); - row = row.push(widget::icon::icon(trash_icon_symbolic(16)).size(16)); - row = row.push(widget::text::heading(fl!("trash"))); - children.push( - widget::button(row) + widget::button(widget::text::heading(fl!("trash"))) .padding(space_xxxs) .on_press(Message::Location(Location::Trash)) .style(theme::Button::Text) @@ -2546,20 +2553,23 @@ impl Tab { ); } Location::Recents => { - let mut row = widget::row::with_capacity(2) - .align_items(Alignment::Center) - .spacing(space_xxxs); - row = row.push(widget::icon::from_name("document-open-recent-symbolic").size(16)); - row = row.push(widget::text::heading(fl!("recents"))); - children.push( - widget::button(row) + widget::button(widget::text::heading(fl!("recents"))) .padding(space_xxxs) .on_press(Message::Location(Location::Recents)) .style(theme::Button::Text) .into(), ); } + Location::Networks => { + children.push( + widget::button(widget::text::heading(fl!("networks"))) + .padding(space_xxxs) + .on_press(Message::Location(Location::Networks)) + .style(theme::Button::Text) + .into(), + ); + } } for child in children { @@ -3273,6 +3283,12 @@ impl Tab { // Update cached size self.size_opt.set(Some(size)); + let cosmic_theme::Spacing { + space_xxs, + space_xs, + .. + } = theme::active().cosmic().spacing; + let location_view_opt = if matches!(self.mode, Mode::Desktop) { None } else { @@ -3350,27 +3366,36 @@ impl Tab { } else { tab_column = tab_column.push(popover); } - if let Location::Trash = self.location { - if let Some(items) = self.items_opt() { - if !items.is_empty() { - let cosmic_theme::Spacing { - space_xxs, - space_xs, - .. - } = theme::active().cosmic().spacing; - - tab_column = tab_column.push( - widget::layer_container(widget::row::with_children(vec![ - widget::horizontal_space(Length::Fill).into(), - widget::button::standard(fl!("empty-trash")) - .on_press(Message::EmptyTrash) - .into(), - ])) - .padding([space_xxs, space_xs]) - .layer(cosmic_theme::Layer::Primary), - ); + match &self.location { + Location::Trash => { + if let Some(items) = self.items_opt() { + if !items.is_empty() { + tab_column = tab_column.push( + widget::layer_container(widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + widget::button::standard(fl!("empty-trash")) + .on_press(Message::EmptyTrash) + .into(), + ])) + .padding([space_xxs, space_xs]) + .layer(cosmic_theme::Layer::Primary), + ); + } } } + Location::Networks => { + tab_column = tab_column.push( + widget::layer_container(widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + widget::button::standard(fl!("add-network-drive")) + .on_press(Message::AddNetworkDrive) + .into(), + ])) + .padding([space_xxs, space_xs]) + .layer(cosmic_theme::Layer::Primary), + ); + } + _ => {} } let mut tab_view = widget::container(tab_column) .height(Length::Fill)