feat(tab): allow entering of network uri as path
This commit is contained in:
parent
e053db3bf9
commit
66733b7fda
3 changed files with 159 additions and 94 deletions
|
|
@ -16,6 +16,24 @@ use crate::{
|
||||||
|
|
||||||
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
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> {
|
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||||
for name in themed_icon.names() {
|
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> {
|
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:///");
|
let force_dir = uri.starts_with("network:///");
|
||||||
|
let (_, file) = resolve_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) {
|
|
||||||
file = gio::File::for_uri(resolved_uri.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read .hidden file if present
|
// Read .hidden file if present
|
||||||
let hidden_files: Box<[String]> = if let Some(path) = file.path() {
|
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)
|
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 {
|
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
|
||||||
let mount_op = gio::MountOperation::new();
|
let mount_op = gio::MountOperation::new();
|
||||||
mount_op.connect_ask_password(
|
mount_op.connect_ask_password(
|
||||||
|
|
@ -270,6 +288,7 @@ enum Cmd {
|
||||||
IconSizes,
|
IconSizes,
|
||||||
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
||||||
),
|
),
|
||||||
|
DirInfo(String, mpsc::Sender<Result<(String, String), glib::Error>>),
|
||||||
Unmount(MounterItem),
|
Unmount(MounterItem),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -494,38 +513,27 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Cmd::NetworkScan(mut uri, sizes, items_tx) => {
|
Cmd::NetworkScan(uri, sizes, items_tx) => {
|
||||||
let original_uri = uri.clone();
|
let (resolved_uri, file) = resolve_uri(&uri);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
Ok(_) => false,
|
||||||
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
|
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
|
||||||
};
|
};
|
||||||
|
|
||||||
if needs_mount {
|
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();
|
let event_tx = event_tx.clone();
|
||||||
file.mount_enclosing_volume(
|
file.mount_enclosing_volume(
|
||||||
gio::MountMountFlags::empty(),
|
gio::MountMountFlags::empty(),
|
||||||
Some(&mount_op),
|
Some(&mount_op),
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
move |res| {
|
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...
|
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
|
||||||
// seems to be related to uri with a path
|
// seems to be related to uri with a path
|
||||||
items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap();
|
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
|
||||||
event_tx.send(Event::NetworkResult(uri, match res {
|
event_tx.send(Event::NetworkResult(resolved_uri, match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
},
|
},
|
||||||
|
|
@ -537,9 +545,12 @@ impl Gvfs {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} 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) => {
|
Cmd::Unmount(mounter_item) => {
|
||||||
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
||||||
let ItemKind::Mount = item.kind else { continue };
|
let ItemKind::Mount = item.kind else { continue };
|
||||||
|
|
@ -640,6 +651,14 @@ impl Mounter for Gvfs {
|
||||||
items_rx.blocking_recv()
|
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<()> {
|
fn unmount(&self, item: MounterItem) -> Task<()> {
|
||||||
let command_tx = self.command_tx.clone();
|
let command_tx = self.command_tx.clone();
|
||||||
Task::future(async move {
|
Task::future(async move {
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ pub trait Mounter: Send + Sync {
|
||||||
fn mount(&self, item: MounterItem) -> Task<()>;
|
fn mount(&self, item: MounterItem) -> Task<()>;
|
||||||
fn network_drive(&self, uri: String) -> Task<()>;
|
fn network_drive(&self, uri: String) -> Task<()>;
|
||||||
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
|
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 unmount(&self, item: MounterItem) -> Task<()>;
|
||||||
fn subscription(&self) -> Subscription<MounterMessage>;
|
fn subscription(&self) -> Subscription<MounterMessage>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
171
src/tab.rs
171
src/tab.rs
|
|
@ -1362,12 +1362,19 @@ pub struct EditLocation {
|
||||||
|
|
||||||
impl EditLocation {
|
impl EditLocation {
|
||||||
pub fn resolve(&self) -> Option<Location> {
|
pub fn resolve(&self) -> Option<Location> {
|
||||||
let Some(selected) = self.selected else {
|
if let Location::Network(uri, _, path) = &self.location {
|
||||||
return Some(self.location.clone());
|
MOUNTERS
|
||||||
};
|
.values()
|
||||||
let completions = self.completions.as_ref()?;
|
.find_map(|mounter| mounter.dir_info(uri))
|
||||||
let completion = completions.get(selected)?;
|
.map(|(uri, display_name)| Location::Network(uri, display_name, path.clone()))
|
||||||
Some(self.location.with_path(completion.1.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) {
|
pub fn select(&mut self, forwards: bool) {
|
||||||
|
|
@ -1430,7 +1437,15 @@ impl std::fmt::Display for Location {
|
||||||
|
|
||||||
impl Location {
|
impl Location {
|
||||||
pub fn normalize(&self) -> Self {
|
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
|
// Add trailing slash if location is a path
|
||||||
path.push("");
|
path.push("");
|
||||||
self.with_path(path)
|
self.with_path(path)
|
||||||
|
|
@ -1482,12 +1497,19 @@ impl Location {
|
||||||
Self::Search(_, term, show_hidden, time) => {
|
Self::Search(_, term, show_hidden, time) => {
|
||||||
Self::Search(path, term.clone(), *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(),
|
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>) {
|
pub fn scan(&self, sizes: IconSizes) -> (Option<Item>, Vec<Item>) {
|
||||||
let items = match self {
|
let items = match self {
|
||||||
Self::Desktop(path, display, desktop_config) => {
|
Self::Desktop(path, display, desktop_config) => {
|
||||||
|
|
@ -3064,10 +3086,14 @@ impl Tab {
|
||||||
{
|
{
|
||||||
let mut remove = false;
|
let mut remove = false;
|
||||||
if let Some(last_location) = self.history.last() {
|
if let Some(last_location) = self.history.last() {
|
||||||
if let Some(last_path) = last_location.path_opt() {
|
if let Location::Network(last_uri, ..) = last_location
|
||||||
if let Some(path) = location.path_opt() {
|
&& let Location::Network(uri, ..) = location
|
||||||
remove = last_path == path;
|
{
|
||||||
}
|
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 {
|
if remove {
|
||||||
|
|
@ -3409,8 +3435,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Message::EditLocationComplete(selected) => {
|
Message::EditLocationComplete(selected) => {
|
||||||
if let Some(mut edit_location) = self.edit_location.take() {
|
if let Some(mut edit_location) = self.edit_location.take() {
|
||||||
edit_location.selected = Some(selected);
|
if !matches!(edit_location.location, Location::Network(..)) {
|
||||||
cd = edit_location.resolve();
|
edit_location.selected = Some(selected);
|
||||||
|
cd = edit_location.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::EditLocationEnable => {
|
Message::EditLocationEnable => {
|
||||||
|
|
@ -4634,65 +4662,82 @@ impl Tab {
|
||||||
.padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]);
|
.padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]);
|
||||||
|
|
||||||
if let Some(edit_location) = &self.edit_location {
|
if let Some(edit_location) = &self.edit_location {
|
||||||
if let Some(location) = edit_location.resolve() {
|
let mut text_input = None;
|
||||||
//TODO: allow editing other locations
|
|
||||||
if let Some(path) = location.path_opt() {
|
//TODO: allow editing other locations
|
||||||
row = row.push(
|
if let Location::Network(ref uri, ..) = edit_location.location {
|
||||||
widget::button::custom(
|
let location = edit_location.location.clone();
|
||||||
widget::icon::from_name("window-close-symbolic").size(16),
|
text_input = Some(
|
||||||
)
|
widget::text_input("", uri.clone())
|
||||||
.on_press(Message::EditLocation(None))
|
.id(self.edit_location_id.clone())
|
||||||
.padding(space_xxs)
|
.on_input(move |input| {
|
||||||
.class(theme::Button::Icon),
|
Message::EditLocation(Some(location.with_uri(input).into()))
|
||||||
);
|
})
|
||||||
let text_input = widget::text_input("", path.to_string_lossy().into_owned())
|
.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())
|
.id(self.edit_location_id.clone())
|
||||||
.on_input(move |input| {
|
.on_input(move |input| {
|
||||||
Message::EditLocation(Some(
|
Message::EditLocation(Some(
|
||||||
location.with_path(PathBuf::from(input)).into(),
|
resolved_location.with_path(PathBuf::from(input)).into(),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.on_submit(|_| Message::EditLocationSubmit)
|
.on_submit(|_| Message::EditLocationSubmit)
|
||||||
.line_height(1.0);
|
.line_height(1.0),
|
||||||
let mut popover =
|
);
|
||||||
widget::popover(text_input).position(widget::popover::Position::Bottom);
|
}
|
||||||
if let Some(completions) = &edit_location.completions {
|
if let Some(text_input) = text_input {
|
||||||
if !completions.is_empty() {
|
row = row.push(
|
||||||
let mut column =
|
widget::button::custom(
|
||||||
widget::column::with_capacity(completions.len()).padding(space_xxs);
|
widget::icon::from_name("window-close-symbolic").size(16),
|
||||||
for (i, (name, _path)) in completions.iter().enumerate() {
|
)
|
||||||
let selected = edit_location.selected == Some(i);
|
.on_press(Message::EditLocation(None))
|
||||||
column = column.push(
|
.padding(space_xxs)
|
||||||
widget::button::custom(widget::text::body(name))
|
.class(theme::Button::Icon),
|
||||||
//TODO: match to design
|
);
|
||||||
.class(if selected {
|
let mut popover =
|
||||||
theme::Button::Standard
|
widget::popover(text_input).position(widget::popover::Position::Bottom);
|
||||||
} else {
|
if let Some(completions) = &edit_location.completions {
|
||||||
theme::Button::HeaderBar
|
if !completions.is_empty() {
|
||||||
})
|
let mut column =
|
||||||
.on_press(Message::EditLocationComplete(i))
|
widget::column::with_capacity(completions.len()).padding(space_xxs);
|
||||||
.padding(space_xxs)
|
for (i, (name, _path)) in completions.iter().enumerate() {
|
||||||
.width(Length::Fill),
|
let selected = edit_location.selected == Some(i);
|
||||||
);
|
column = column.push(
|
||||||
}
|
widget::button::custom(widget::text::body(name))
|
||||||
popover = popover.popup(
|
//TODO: match to design
|
||||||
widget::container(column)
|
.class(if selected {
|
||||||
.class(theme::Container::Dropdown)
|
theme::Button::Standard
|
||||||
//TODO: This is a hack to get the popover to be the right width
|
} else {
|
||||||
.max_width(size.width - 140.0),
|
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() {
|
} else if let Some(path) = self.location.path_opt() {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue