From c3d64980424ae752281495c63e1edcbd70773503 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 13 Sep 2024 15:13:37 -0600 Subject: [PATCH] WIP: support for network browsing --- examples/gio-list.rs | 31 ++++++++ examples/{mount.rs => gio-mount.rs} | 0 src/app.rs | 64 ++++++++-------- src/dialog.rs | 6 +- src/lib.rs | 4 + src/menu.rs | 2 +- src/mounter/gvfs.rs | 105 +++++++++++++++++++++++++- src/mounter/mod.rs | 5 +- src/operation.rs | 6 +- src/tab.rs | 113 ++++++++++++++++++---------- 10 files changed, 251 insertions(+), 85 deletions(-) create mode 100644 examples/gio-list.rs rename examples/{mount.rs => gio-mount.rs} (100%) diff --git a/examples/gio-list.rs b/examples/gio-list.rs new file mode 100644 index 0000000..a83a0d6 --- /dev/null +++ b/examples/gio-list.rs @@ -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()); + } +} diff --git a/examples/mount.rs b/examples/gio-mount.rs similarity index 100% rename from examples/mount.rs rename to examples/gio-mount.rs diff --git a/src/app.rs b/src/app.rs index b5a7b34..6fa96e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -521,11 +521,14 @@ impl App { location: Location, selection_path: Option, ) -> Command { + 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) -> Element { 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::(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) { - children.push(item.property_view(IconSizes::default())); - } + //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::() { - let path = match tab.location { - Location::Path(ref path) => Some(path), - Location::Search(ref path, _) => Some(path), - _ => None, - } - .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) { - children.push(item.property_view(IconSizes::default())); - } + 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_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,11 +2388,14 @@ 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)) - .await + match tokio::task::spawn_blocking(move || { + Location::Trash.scan(mounters, icon_sizes) + }) + .await { Ok(items) => { for path in &*recently_trashed { @@ -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); diff --git a/src/dialog.rs b/src/dialog.rs index 7dd6dcd..30d203a 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -376,10 +376,12 @@ struct App { impl App { fn rescan_tab(&self) -> Command { 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)); } } diff --git a/src/lib.rs b/src/lib.rs index f23d24e..0f201d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,10 @@ mod spawn_detached; use tab::Location; pub mod tab; +pub(crate) fn err_str(err: T) -> String { + err.to_string() +} + pub fn home_dir() -> PathBuf { match dirs::home_dir() { Some(home) => home, diff --git a/src/menu.rs b/src/menu.rs index 86b1d08..f3b22d6 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -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) => { diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 7ad7b4c..367eee6 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -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 { if let Some(themed_icon) = icon.downcast_ref::() { @@ -21,10 +26,97 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { None } +fn network_scan(uri: &str, sizes: IconSizes) -> Result, 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, 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, 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( diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index bca9622..a17dc1f 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -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), } -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, String>>; fn unmount(&self, item: MounterItem) -> Command<()>; fn subscription(&self) -> subscription::Subscription; } diff --git a/src/operation.rs b/src/operation.rs index b5ac4e7..15dabcc 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -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(err: T) -> String { - err.to_string() -} - fn handle_replace( msg_tx: &Arc>>, file_from: PathBuf, diff --git a/src/tab.rs b/src/tab.rs index 0252f25..b854444 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -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 metadata.modified().ok(), - ItemMetadata::Trash { .. } => None, + _ => None, }; // Sort with latest modified first @@ -750,9 +751,17 @@ 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![] +pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec { + 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 { + pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec { 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,14 +1607,18 @@ 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 { - commands.push(Command::OpenFile(path.clone())); + 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 path for item {:?}", clicked_item); + log::warn!("no location for item {:?}", clicked_item); } } else { log::warn!("no item for click index {:?}", click_i_opt); @@ -1990,12 +2009,14 @@ 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 { - commands.push(Command::OpenFile(path.clone())); + 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() { - format!("{} items", children) + //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(),