Merge pull request #1390 from Cheong-Lau/enter-network-path
feat(tab): allow entering of network uri as path
This commit is contained in:
commit
3baaf4b452
4 changed files with 165 additions and 95 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<PathBuf> {
|
||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||
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<Vec<tab::Item>, 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<Vec<tab::Item>, 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<Event>) -> gio::MountOperation {
|
||||
let mount_op = gio::MountOperation::new();
|
||||
mount_op.connect_ask_password(
|
||||
|
|
@ -270,6 +288,7 @@ enum Cmd {
|
|||
IconSizes,
|
||||
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
||||
),
|
||||
DirInfo(String, mpsc::Sender<Result<(String, String), glib::Error>>),
|
||||
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::<gio::IOErrorEnum>(), 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 {
|
||||
|
|
|
|||
|
|
@ -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<Result<Vec<tab::Item>, String>>;
|
||||
fn dir_info(&self, uri: &str) -> Option<(String, String)>;
|
||||
fn unmount(&self, item: MounterItem) -> Task<()>;
|
||||
fn subscription(&self) -> Subscription<MounterMessage>;
|
||||
}
|
||||
|
|
|
|||
171
src/tab.rs
171
src/tab.rs
|
|
@ -1362,12 +1362,19 @@ pub struct EditLocation {
|
|||
|
||||
impl EditLocation {
|
||||
pub fn resolve(&self) -> Option<Location> {
|
||||
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<Item>, Vec<Item>) {
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue