WIP: support for network browsing

This commit is contained in:
Jeremy Soller 2024-09-13 15:13:37 -06:00
parent cd71c895f9
commit c3d6498042
10 changed files with 251 additions and 85 deletions

31
examples/gio-list.rs Normal file
View file

@ -0,0 +1,31 @@
use gio::prelude::*;
use std::env;
fn main() {
let uri = env::args().nth(1).expect("no uri provided");
let file = gio::File::for_uri(&uri);
for entry_res in file
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
.unwrap()
{
let entry = entry_res.unwrap();
println!("{:?}", entry.display_name());
for attribute in entry.list_attributes(None) {
println!(
" {:?}: {:?}",
attribute,
entry.attribute_as_string(&attribute)
);
}
//TODO: what is the best way to resolve shortcuts?
let child = if let Some(target_uri) =
entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
{
gio::File::for_uri(&target_uri)
} else {
file.child(entry.name())
};
println!("{:?}", child.uri());
}
}

View file

@ -521,11 +521,14 @@ impl App {
location: Location,
selection_path: Option<PathBuf>,
) -> Command<Message> {
let mounters = self.mounters.clone();
let icon_sizes = self.config.tab.icon_sizes;
Command::perform(
async move {
let location2 = location.clone();
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
match tokio::task::spawn_blocking(move || location2.scan(mounters, icon_sizes))
.await
{
Ok(items) => {
message::app(Message::TabRescan(entity, location, items, selection_path))
}
@ -672,7 +675,10 @@ impl App {
.size(16)
.handle(),
))
.data(Location::Networks)
.data(Location::Network(
"network:///".to_string(),
fl!("networks"),
))
.divider_above()
});
@ -941,49 +947,35 @@ impl App {
fn properties(&self, entity: Option<ContextItem>) -> Element<Message> {
match entity {
None => self.tab_properties(self.tab_model.active()),
Some(ContextItem::TabBar(entity)) => self.tab_properties(entity),
Some(ContextItem::NavBar(item)) => {
let mut children = Vec::new();
let mut children = Vec::with_capacity(1);
if let Some(location) = self.nav_model.data::<Location>(item) {
if let Location::Path(path) = location {
let parent = path.parent().unwrap_or(path);
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
if item.path_opt() == Some(path) {
//TODO: this should be done once, not when generating the view!
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
children.push(item.property_view(IconSizes::default()));
}
}
};
}
widget::settings::view_column(children).into()
}
Some(ContextItem::BreadCrumbs(index)) => {
let mut children = Vec::new();
let mut children = Vec::with_capacity(1);
if let Some(tab) = self.tab_model.active_data::<Tab>() {
let path = match tab.location {
Location::Path(ref path) => Some(path),
Location::Search(ref path, _) => Some(path),
_ => None,
}
let path_opt = tab
.location
.path_opt()
.and_then(|path| path.ancestors().nth(index))
.map(|path| path.to_path_buf());
if let Some(ref path) = path {
let parent = path.parent().unwrap_or(path);
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
if item.path_opt() == Some(path) {
if let Some(ref path) = path_opt {
//TODO: this should be done once, not when generating the view!
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
children.push(item.property_view(IconSizes::default()));
}
}
};
}
widget::settings::view_column(children).into()
}
}
@ -2396,10 +2388,13 @@ impl Application for App {
self.toasts.remove(id);
let mut paths = Vec::with_capacity(recently_trashed.len());
let mounters = self.mounters.clone();
let icon_sizes = self.config.tab.icon_sizes;
return cosmic::command::future(async move {
match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes))
match tokio::task::spawn_blocking(move || {
Location::Trash.scan(mounters, icon_sizes)
})
.await
{
Ok(items) => {
@ -3549,6 +3544,7 @@ pub(crate) mod test_utils {
use crate::{
config::{IconSizes, TabConfig},
mounter::MounterMap,
tab::Item,
};
@ -3711,7 +3707,7 @@ pub(crate) mod test_utils {
// New tab with items
let location = Location::Path(path.to_owned());
let items = location.scan(IconSizes::default());
let items = location.scan(Mounters::new(MounterMap::new()), IconSizes::default());
let mut tab = Tab::new(location, TabConfig::default());
tab.set_items(items);

View file

@ -376,10 +376,12 @@ struct App {
impl App {
fn rescan_tab(&self) -> Command<Message> {
let location = self.tab.location.clone();
let mounters = self.mounters.clone();
let icon_sizes = self.tab.config.icon_sizes;
Command::perform(
async move {
match tokio::task::spawn_blocking(move || location.scan(icon_sizes)).await {
match tokio::task::spawn_blocking(move || location.scan(mounters, icon_sizes)).await
{
Ok(items) => message::app(Message::TabRescan(items)),
Err(err) => {
log::warn!("failed to rescan: {}", err);
@ -895,7 +897,7 @@ impl Application for App {
}
}
}
DialogPage::Replace { filename } => {
DialogPage::Replace { .. } => {
return self.update(Message::Save(true));
}
}

View file

@ -25,6 +25,10 @@ mod spawn_detached;
use tab::Location;
pub mod tab;
pub(crate) fn err_str<T: ToString>(err: T) -> String {
err.to_string()
}
pub fn home_dir() -> PathBuf {
match dirs::home_dir() {
Some(home) => home,

View file

@ -197,7 +197,7 @@ pub fn context_menu<'a>(
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
}
}
(_, Location::Networks) => {
(_, Location::Network(_, _)) => {
//TODO: networks context menu?
}
(_, Location::Trash) => {

View file

@ -3,10 +3,15 @@ use cosmic::{
widget, Command,
};
use gio::{glib, prelude::*};
use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
use std::{any::TypeId, cell::Cell, future::pending, path::PathBuf, sync::Arc};
use tokio::sync::{mpsc, Mutex};
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::{
config::IconSizes,
err_str,
tab::{self, ItemMetadata, ItemThumbnail, Location},
};
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
@ -21,10 +26,97 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
None
}
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
let mut items = Vec::new();
let file = gio::File::for_uri(&uri);
for info_res in file
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
.map_err(err_str)?
{
let info = info_res.map_err(err_str)?;
println!("{:?}", info.display_name());
for attribute in info.list_attributes(None) {
println!(
" {:?}: {:?}: {:?}",
attribute,
info.attribute_type(&attribute),
info.attribute_as_string(&attribute)
);
}
let name = info.name().to_string_lossy().to_string();
let display_name = info.display_name().to_string();
//TODO: what is the best way to resolve shortcuts?
let location = Location::Network(
if let Some(target_uri) = info.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
{
target_uri.to_string()
} else {
file.child(info.name()).uri().to_string()
},
display_name.clone(),
);
//TODO: support dir or file
let metadata = ItemMetadata::SimpleDir { entries: 0 };
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
let file_icon = |size| {
info.icon()
.as_ref()
.and_then(|icon| gio_icon_to_path(icon, size))
.map(|path| widget::icon::from_path(path))
.unwrap_or(
widget::icon::from_name(if metadata.is_dir() {
"folder"
} else {
"text-x-generic"
})
.size(size)
.handle(),
)
};
(
//TODO: get mime from content_type?
"inode/directory".parse().unwrap(),
file_icon(sizes.grid()),
file_icon(sizes.list()),
file_icon(sizes.list_condensed()),
)
};
items.push(tab::Item {
name,
display_name,
metadata,
hidden: false,
location_opt: Some(location),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(),
pos_opt: Cell::new(None),
rect_opt: Cell::new(None),
selected: false,
overlaps_drag_rect: false,
});
}
Ok(items)
}
enum Cmd {
Rescan,
Mount(MounterItem),
NetworkDrive(String),
NetworkScan(
String,
IconSizes,
mpsc::Sender<Result<Vec<tab::Item>, String>>,
),
Unmount(MounterItem),
}
@ -267,6 +359,9 @@ impl Gvfs {
}
);
}
Cmd::NetworkScan(uri, sizes, items_tx) => {
items_tx.send(network_scan(&uri, sizes)).await.unwrap();
}
Cmd::Unmount(mounter_item) => {
let MounterItem::Gvfs(item) = mounter_item else { continue };
let ItemKind::Mount = item.kind else { continue };
@ -330,6 +425,14 @@ impl Mounter for Gvfs {
)
}
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {
let (items_tx, mut items_rx) = mpsc::channel(1);
self.command_tx
.send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))
.unwrap();
items_rx.blocking_recv()
}
fn unmount(&self, item: MounterItem) -> Command<()> {
let command_tx = self.command_tx.clone();
Command::perform(

View file

@ -2,6 +2,8 @@ use cosmic::{iced::subscription, widget, Command};
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
use tokio::sync::mpsc;
use crate::{config::IconSizes, tab};
#[cfg(feature = "gvfs")]
mod gvfs;
@ -86,10 +88,11 @@ pub enum MounterMessage {
NetworkResult(String, Result<bool, String>),
}
pub trait Mounter {
pub trait Mounter: Send + Sync {
//TODO: send result
fn mount(&self, item: MounterItem) -> Command<()>;
fn network_drive(&self, uri: String) -> Command<()>;
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
fn unmount(&self, item: MounterItem) -> Command<()>;
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
}

View file

@ -15,15 +15,11 @@ use walkdir::WalkDir;
use crate::{
app::{ArchiveType, DialogPage, Message},
config::IconSizes,
fl,
err_str, fl,
mime_icon::mime_for_path,
tab,
};
fn err_str<T: ToString>(err: T) -> String {
err.to_string()
}
fn handle_replace(
msg_tx: &Arc<Mutex<Sender<Message>>>,
file_from: PathBuf,

View file

@ -63,6 +63,7 @@ use crate::{
menu,
mime_app::{mime_apps, MimeApp},
mime_icon::{mime_for_path, mime_icon},
mounter::Mounters,
mouse_area,
};
use unix_permissions_ext::UNIXPermissionsExt;
@ -561,7 +562,7 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
items.par_sort_unstable_by(|a, b| {
let get_modified = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
ItemMetadata::Trash { .. } => None,
_ => None,
};
// Sort with latest modified first
@ -750,9 +751,17 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
recents.into_iter().take(50).map(|(item, _)| item).collect()
}
pub fn scan_networks(sizes: IconSizes) -> Vec<Item> {
//TODO: network folder items
vec![]
pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
for (key, mounter) in mounters.iter() {
match mounter.network_scan(uri, sizes) {
Some(Ok(items)) => return items,
Some(Err(err)) => {
log::warn!("failed to scan networks: {}", err);
}
None => {}
}
}
Vec::new()
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -761,7 +770,7 @@ pub enum Location {
Search(PathBuf, String),
Trash,
Recents,
Networks,
Network(String, String),
}
impl std::fmt::Display for Location {
@ -771,7 +780,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"),
Self::Network(uri, _) => write!(f, "{}", uri),
}
}
}
@ -785,13 +794,13 @@ impl Location {
}
}
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
match self {
Self::Path(path) => scan_path(path, sizes),
Self::Search(path, term) => scan_search(path, term, sizes),
Self::Trash => scan_trash(sizes),
Self::Recents => scan_recents(sizes),
Self::Networks => scan_networks(sizes),
Self::Network(uri, _) => scan_network(uri, mounters, sizes),
}
}
}
@ -881,6 +890,12 @@ pub enum ItemMetadata {
metadata: trash::TrashItemMetadata,
entry: trash::TrashItem,
},
SimpleDir {
entries: u64,
},
SimpleFile {
size: u64,
},
}
impl ItemMetadata {
@ -891,6 +906,8 @@ impl ItemMetadata {
trash::TrashItemSize::Entries(_) => true,
trash::TrashItemSize::Bytes(_) => false,
},
Self::SimpleDir { .. } => true,
Self::SimpleFile { .. } => false,
}
}
}
@ -1087,8 +1104,8 @@ impl Item {
);
}
}
ItemMetadata::Trash { .. } => {
//TODO: trash metadata
_ => {
//TODO: other metadata types
}
}
@ -1127,8 +1144,8 @@ impl Item {
)));
}
}
ItemMetadata::Trash { .. } => {
//TODO: trash metadata
_ => {
//TODO: other metadata
}
}
@ -1278,9 +1295,7 @@ impl Tab {
Location::Recents => {
fl!("recents")
}
Location::Networks => {
fl!("networks")
}
Location::Network(_uri, display_name) => display_name.clone(),
}
}
@ -1592,15 +1607,19 @@ impl Tab {
.as_ref()
.and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i)))
{
if let Some(Location::Path(path)) = &clicked_item.location_opt {
if let Some(location) = &clicked_item.location_opt {
if clicked_item.metadata.is_dir() {
cd = Some(Location::Path(path.clone()));
cd = Some(location.clone());
} else {
if let Location::Path(path) = location {
commands.push(Command::OpenFile(path.clone()));
}
} else {
log::warn!("no path for item {:?}", clicked_item);
}
}
} else {
log::warn!("no location for item {:?}", clicked_item);
}
} else {
log::warn!("no item for click index {:?}", click_i_opt);
}
@ -1990,13 +2009,15 @@ impl Tab {
if let Some(ref mut items) = self.items_opt {
for item in items.iter() {
if item.selected {
if let Some(Location::Path(path)) = &item.location_opt {
if path.is_dir() {
if let Some(location) = &item.location_opt {
if item.metadata.is_dir() {
//TODO: allow opening multiple tabs?
cd = Some(Location::Path(path.clone()));
cd = Some(location.clone());
} else {
if let Location::Path(path) = location {
commands.push(Command::OpenFile(path.clone()));
}
}
} else {
//TODO: open properties?
}
@ -2112,20 +2133,11 @@ impl Tab {
}
commands.push(Command::DropFiles(to, from))
}
Location::Search(_, _) => {
log::warn!(" Copy/cut to search not supported.");
}
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
commands.push(Command::MoveToTrash(from.paths))
}
Location::Trash => {
log::warn!("Copy to trash is not supported.");
}
Location::Recents => {
log::warn!("Copy to recents is not supported.");
}
Location::Networks => {
log::warn!("Copy to networks is not supported.");
_ => {
log::warn!("{:?} to {:?} is not supported.", from.kind, to);
}
};
}
@ -2254,6 +2266,8 @@ impl Tab {
trash::TrashItemSize::Entries(entries) => (true, entries as u64),
trash::TrashItemSize::Bytes(bytes) => (false, bytes),
},
ItemMetadata::SimpleDir { entries } => (true, *entries),
ItemMetadata::SimpleFile { size } => (false, *size),
};
let (a_is_entry, a_size) = get_size(a.1);
let (b_is_entry, b_size) = get_size(b.1);
@ -2287,7 +2301,7 @@ impl Tab {
items.sort_by(|a, b| {
let get_modified = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
ItemMetadata::Trash { .. } => None,
_ => None,
};
let a_modified = get_modified(a.1);
@ -2588,11 +2602,14 @@ impl Tab {
.into(),
);
}
Location::Networks => {
Location::Network(uri, display_name) => {
children.push(
widget::button(widget::text::heading(fl!("networks")))
widget::button(widget::text::heading(display_name))
.padding(space_xxxs)
.on_press(Message::Location(Location::Networks))
.on_press(Message::Location(Location::Network(
uri.clone(),
display_name.clone(),
)))
.style(theme::Button::Text)
.into(),
);
@ -3067,13 +3084,18 @@ impl Tab {
Ok(time) => format_time(time).to_string(),
Err(_) => String::new(),
},
ItemMetadata::Trash { .. } => String::new(),
_ => String::new(),
};
let size_text = match &item.metadata {
ItemMetadata::Path { metadata, children } => {
if metadata.is_dir() {
//TODO: translate
if *children == 1 {
format!("{} item", children)
} else {
format!("{} items", children)
}
} else {
format_size(metadata.len())
}
@ -3089,6 +3111,15 @@ impl Tab {
}
trash::TrashItemSize::Bytes(bytes) => format_size(bytes),
},
ItemMetadata::SimpleDir { entries } => {
//TODO: translate
if *entries == 1 {
format!("{} item", entries)
} else {
format!("{} items", entries)
}
}
ItemMetadata::SimpleFile { size } => format_size(*size),
};
let row = if condensed {
@ -3410,7 +3441,7 @@ impl Tab {
}
}
}
Location::Networks => {
Location::Network(uri, display_name) if uri == "network:///" => {
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),