diff --git a/src/app.rs b/src/app.rs index 44d5880..cd4d005 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3004,7 +3004,12 @@ impl Application for App { .as_ref() .map_or_else(|| &tab.location, |x| &x.location); // Try to add text to end of location - if let Some(path) = location.path_opt() { + if let Location::Network(uri, ..) = location { + let mut uri_string = uri.clone(); + uri_string.push_str(&text); + tab.edit_location = + Some(location.with_uri(uri_string).into()); + } else if let Some(path) = location.path_opt() { let mut path_string = path.to_string_lossy().into_owned(); path_string.push_str(&text); diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 1fe8257..d5db451 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -16,6 +16,24 @@ use crate::{ const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri"; +fn resolve_uri(uri: &str) -> (String, gio::File) { + let file = gio::File::for_uri(uri); + // Resolve the target-uri if it exists + if let Ok(file_info) = file.query_info( + TARGET_URI_ATTRIBUTE, + gio::FileQueryInfoFlags::NONE, + gio::Cancellable::NONE, + ) { + if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { + let resolved_uri = String::from(resolved_uri); + let file = gio::File::for_uri(&resolved_uri); + return (resolved_uri, file); + } + } + + (uri.to_string(), file) +} + fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { if let Some(themed_icon) = icon.downcast_ref::() { for name in themed_icon.names() { @@ -83,19 +101,8 @@ fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems { } fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { - let mut file = gio::File::for_uri(uri); let force_dir = uri.starts_with("network:///"); - - // Resolve the target-uri if it exists - if let Ok(file_info) = file.query_info( - TARGET_URI_ATTRIBUTE, - gio::FileQueryInfoFlags::NONE, - gio::Cancellable::NONE, - ) { - if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { - file = gio::File::for_uri(resolved_uri.as_str()); - } - } + let (_, file) = resolve_uri(uri); // Read .hidden file if present let hidden_files: Box<[String]> = if let Some(path) = file.path() { @@ -210,6 +217,17 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { Ok(items) } +fn dir_info(uri: &str) -> Result<(String, String), glib::Error> { + let (resolved_uri, file) = resolve_uri(uri); + let info = file.query_info( + gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + gio::FileQueryInfoFlags::NONE, + gio::Cancellable::NONE, + )?; + + Ok((resolved_uri, info.display_name().into())) +} + fn mount_op(uri: String, event_tx: mpsc::UnboundedSender) -> gio::MountOperation { let mount_op = gio::MountOperation::new(); mount_op.connect_ask_password( @@ -270,6 +288,7 @@ enum Cmd { IconSizes, mpsc::Sender, String>>, ), + DirInfo(String, mpsc::Sender>), Unmount(MounterItem), } @@ -494,38 +513,27 @@ impl Gvfs { } ); } - Cmd::NetworkScan(mut uri, sizes, items_tx) => { - let original_uri = uri.clone(); - let mut file = gio::File::for_uri(&uri); - if let Ok(file_info) = file.query_info( - TARGET_URI_ATTRIBUTE, - gio::FileQueryInfoFlags::NONE, - gio::Cancellable::NONE, - ) { - if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { - uri = resolved_uri.into(); - file = gio::File::for_uri(&uri); - } - } + Cmd::NetworkScan(uri, sizes, items_tx) => { + let (resolved_uri, file) = resolve_uri(&uri); - let needs_mount = uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) { + let needs_mount = resolved_uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) { Ok(_) => false, Err(err) => matches!(err.kind::(), Some(gio::IOErrorEnum::NotMounted)) }; if needs_mount { - let mount_op = mount_op(uri.clone(), event_tx.clone()); + let mount_op = mount_op(resolved_uri.clone(), event_tx.clone()); let event_tx = event_tx.clone(); file.mount_enclosing_volume( gio::MountMountFlags::empty(), Some(&mount_op), gio::Cancellable::NONE, move |res| { - log::info!("network scan mounted {uri}: result {res:?}"); + log::info!("network scan mounted {resolved_uri}: result {res:?}"); // FIXME sometimes a uri can be mounted and then not recognized as mounted... // seems to be related to uri with a path - items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap(); - event_tx.send(Event::NetworkResult(uri, match res { + items_tx.blocking_send(network_scan(&uri, sizes)).unwrap(); + event_tx.send(Event::NetworkResult(resolved_uri, match res { Ok(()) => { Ok(true) }, @@ -537,9 +545,12 @@ impl Gvfs { } ); } else { - items_tx.send(network_scan(&original_uri, sizes)).await.unwrap(); + items_tx.send(network_scan(&uri, sizes)).await.unwrap(); } } + Cmd::DirInfo(uri, result_tx) => { + result_tx.send(dir_info(&uri)).await.unwrap(); + } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Mount = item.kind else { continue }; @@ -640,6 +651,14 @@ impl Mounter for Gvfs { items_rx.blocking_recv() } + fn dir_info(&self, uri: &str) -> Option<(String, String)> { + let (result_tx, mut result_rx) = mpsc::channel(1); + self.command_tx + .send(Cmd::DirInfo(uri.to_string(), result_tx)) + .unwrap(); + result_rx.blocking_recv().and_then(|res| res.ok()) + } + fn unmount(&self, item: MounterItem) -> Task<()> { let command_tx = self.command_tx.clone(); Task::future(async move { diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index 098b2b3..5aa6290 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -116,6 +116,7 @@ pub trait Mounter: Send + Sync { fn mount(&self, item: MounterItem) -> Task<()>; fn network_drive(&self, uri: String) -> Task<()>; fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option, String>>; + fn dir_info(&self, uri: &str) -> Option<(String, String)>; fn unmount(&self, item: MounterItem) -> Task<()>; fn subscription(&self) -> Subscription; } diff --git a/src/tab.rs b/src/tab.rs index b643ba2..0e36954 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1362,12 +1362,19 @@ pub struct EditLocation { impl EditLocation { pub fn resolve(&self) -> Option { - let Some(selected) = self.selected else { - return Some(self.location.clone()); - }; - let completions = self.completions.as_ref()?; - let completion = completions.get(selected)?; - Some(self.location.with_path(completion.1.clone())) + if let Location::Network(uri, _, path) = &self.location { + MOUNTERS + .values() + .find_map(|mounter| mounter.dir_info(uri)) + .map(|(uri, display_name)| Location::Network(uri, display_name, path.clone())) + } else { + let Some(selected) = self.selected else { + return Some(self.location.clone()); + }; + let completions = self.completions.as_ref()?; + let completion = completions.get(selected)?; + Some(self.location.with_path(completion.1.clone())) + } } pub fn select(&mut self, forwards: bool) { @@ -1430,7 +1437,15 @@ impl std::fmt::Display for Location { impl Location { pub fn normalize(&self) -> Self { - if let Some(mut path) = self.path_opt().cloned() { + if let Location::Network(uri, ..) = self { + if !uri.ends_with('/') { + let mut uri = uri.clone(); + uri.push('/'); + self.with_uri(uri) + } else { + self.clone() + } + } else if let Some(mut path) = self.path_opt().cloned() { // Add trailing slash if location is a path path.push(""); self.with_path(path) @@ -1482,12 +1497,19 @@ impl Location { Self::Search(_, term, show_hidden, time) => { Self::Search(path, term.clone(), *show_hidden, *time) } - Self::Network(id, name, path) => Self::Network(id.clone(), name.clone(), path.clone()), other => other.clone(), } } + pub fn with_uri(&self, uri: String) -> Self { + if let Self::Network(_, name, path) = self { + Self::Network(uri, name.clone(), path.clone()) + } else { + self.clone() + } + } + pub fn scan(&self, sizes: IconSizes) -> (Option, Vec) { let items = match self { Self::Desktop(path, display, desktop_config) => { @@ -3064,10 +3086,14 @@ impl Tab { { let mut remove = false; if let Some(last_location) = self.history.last() { - if let Some(last_path) = last_location.path_opt() { - if let Some(path) = location.path_opt() { - remove = last_path == path; - } + if let Location::Network(last_uri, ..) = last_location + && let Location::Network(uri, ..) = location + { + remove = last_uri == uri; + } else if let Some(last_path) = last_location.path_opt() + && let Some(path) = location.path_opt() + { + remove = last_path == path; } } if remove { @@ -3409,8 +3435,10 @@ impl Tab { } Message::EditLocationComplete(selected) => { if let Some(mut edit_location) = self.edit_location.take() { - edit_location.selected = Some(selected); - cd = edit_location.resolve(); + if !matches!(edit_location.location, Location::Network(..)) { + edit_location.selected = Some(selected); + cd = edit_location.resolve(); + } } } Message::EditLocationEnable => { @@ -4634,65 +4662,82 @@ impl Tab { .padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]); if let Some(edit_location) = &self.edit_location { - if let Some(location) = edit_location.resolve() { - //TODO: allow editing other locations - if let Some(path) = location.path_opt() { - row = row.push( - widget::button::custom( - widget::icon::from_name("window-close-symbolic").size(16), - ) - .on_press(Message::EditLocation(None)) - .padding(space_xxs) - .class(theme::Button::Icon), - ); - let text_input = widget::text_input("", path.to_string_lossy().into_owned()) + let mut text_input = None; + + //TODO: allow editing other locations + if let Location::Network(ref uri, ..) = edit_location.location { + let location = edit_location.location.clone(); + text_input = Some( + widget::text_input("", uri.clone()) + .id(self.edit_location_id.clone()) + .on_input(move |input| { + Message::EditLocation(Some(location.with_uri(input).into())) + }) + .on_submit(|_| Message::EditLocationSubmit) + .line_height(1.0), + ); + } else if let Some(resolved_location) = edit_location.resolve() + && let Some(path) = resolved_location.path_opt().cloned() + { + text_input = Some( + widget::text_input("", path.to_string_lossy().into_owned()) .id(self.edit_location_id.clone()) .on_input(move |input| { Message::EditLocation(Some( - location.with_path(PathBuf::from(input)).into(), + resolved_location.with_path(PathBuf::from(input)).into(), )) }) .on_submit(|_| Message::EditLocationSubmit) - .line_height(1.0); - let mut popover = - widget::popover(text_input).position(widget::popover::Position::Bottom); - if let Some(completions) = &edit_location.completions { - if !completions.is_empty() { - let mut column = - widget::column::with_capacity(completions.len()).padding(space_xxs); - for (i, (name, _path)) in completions.iter().enumerate() { - let selected = edit_location.selected == Some(i); - column = column.push( - widget::button::custom(widget::text::body(name)) - //TODO: match to design - .class(if selected { - theme::Button::Standard - } else { - theme::Button::HeaderBar - }) - .on_press(Message::EditLocationComplete(i)) - .padding(space_xxs) - .width(Length::Fill), - ); - } - popover = popover.popup( - widget::container(column) - .class(theme::Container::Dropdown) - //TODO: This is a hack to get the popover to be the right width - .max_width(size.width - 140.0), + .line_height(1.0), + ); + } + if let Some(text_input) = text_input { + row = row.push( + widget::button::custom( + widget::icon::from_name("window-close-symbolic").size(16), + ) + .on_press(Message::EditLocation(None)) + .padding(space_xxs) + .class(theme::Button::Icon), + ); + let mut popover = + widget::popover(text_input).position(widget::popover::Position::Bottom); + if let Some(completions) = &edit_location.completions { + if !completions.is_empty() { + let mut column = + widget::column::with_capacity(completions.len()).padding(space_xxs); + for (i, (name, _path)) in completions.iter().enumerate() { + let selected = edit_location.selected == Some(i); + column = column.push( + widget::button::custom(widget::text::body(name)) + //TODO: match to design + .class(if selected { + theme::Button::Standard + } else { + theme::Button::HeaderBar + }) + .on_press(Message::EditLocationComplete(i)) + .padding(space_xxs) + .width(Length::Fill), ); } + popover = popover.popup( + widget::container(column) + .class(theme::Container::Dropdown) + //TODO: This is a hack to get the popover to be the right width + .max_width(size.width - 140.0), + ); } - row = row.push(popover); - let mut column = widget::column::with_capacity(4).padding([0, space_s]); - column = column.push(row); - column = column.push(accent_rule); - if self.config.view == View::List && !condensed { - column = column.push(heading_row); - column = column.push(heading_rule); - } - return column.into(); } + row = row.push(popover); + let mut column = widget::column::with_capacity(4).padding([0, space_s]); + column = column.push(row); + column = column.push(accent_rule); + if self.config.view == View::List && !condensed { + column = column.push(heading_row); + column = column.push(heading_rule); + } + return column.into(); } } else if let Some(path) = self.location.path_opt() { row = row.push(