Implement network drive connection, part of #202

This commit is contained in:
Jeremy Soller 2024-09-12 15:54:54 -06:00
parent f41730978c
commit 0d8fd00dd3
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
8 changed files with 542 additions and 86 deletions

46
Cargo.lock generated
View file

@ -1213,7 +1213,7 @@ dependencies = [
[[package]]
name = "cosmic-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"atomicwrites",
"cosmic-config-derive",
@ -1232,7 +1232,7 @@ dependencies = [
[[package]]
name = "cosmic-config-derive"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"quote",
"syn 1.0.109",
@ -1339,7 +1339,7 @@ dependencies = [
[[package]]
name = "cosmic-theme"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"almost",
"cosmic-config",
@ -2782,7 +2782,7 @@ dependencies = [
[[package]]
name = "iced"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"dnd",
"iced_accessibility",
@ -2801,7 +2801,7 @@ dependencies = [
[[package]]
name = "iced_accessibility"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"accesskit",
"accesskit_unix",
@ -2811,7 +2811,7 @@ dependencies = [
[[package]]
name = "iced_core"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"bitflags 2.6.0",
"dnd",
@ -2833,7 +2833,7 @@ dependencies = [
[[package]]
name = "iced_futures"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"futures",
"iced_core",
@ -2846,7 +2846,7 @@ dependencies = [
[[package]]
name = "iced_graphics"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"bitflags 2.6.0",
"bytemuck",
@ -2870,7 +2870,7 @@ dependencies = [
[[package]]
name = "iced_renderer"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"iced_graphics",
"iced_tiny_skia",
@ -2882,7 +2882,7 @@ dependencies = [
[[package]]
name = "iced_runtime"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"dnd",
"iced_accessibility",
@ -2896,7 +2896,7 @@ dependencies = [
[[package]]
name = "iced_sctk"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"enum-repr",
"float-cmp",
@ -2923,7 +2923,7 @@ dependencies = [
[[package]]
name = "iced_style"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"iced_core",
"once_cell",
@ -2933,7 +2933,7 @@ dependencies = [
[[package]]
name = "iced_tiny_skia"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"bytemuck",
"cosmic-text",
@ -2950,7 +2950,7 @@ dependencies = [
[[package]]
name = "iced_wgpu"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"as-raw-xcb-connection",
"bitflags 2.6.0",
@ -2979,7 +2979,7 @@ dependencies = [
[[package]]
name = "iced_widget"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"dnd",
"iced_accessibility",
@ -2997,7 +2997,7 @@ dependencies = [
[[package]]
name = "iced_winit"
version = "0.12.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"dnd",
"iced_accessibility",
@ -3513,7 +3513,7 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libcosmic"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
dependencies = [
"apply",
"ashpd 0.9.1",
@ -3601,7 +3601,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
"redox_syscall 0.5.3",
"redox_syscall 0.5.4",
]
[[package]]
@ -3736,9 +3736,9 @@ dependencies = [
[[package]]
name = "mac-notification-sys"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64"
checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91"
dependencies = [
"cc",
"dirs-next",
@ -4436,7 +4436,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.3",
"redox_syscall 0.5.4",
"smallvec",
"windows-targets 0.52.6",
]
@ -4847,9 +4847,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
dependencies = [
"bitflags 2.6.0",
]

View file

@ -4,6 +4,7 @@ empty-folder-hidden = Empty folder (has hidden items)
no-results = No results found
filesystem = Filesystem
home = Home
networks = Networks
notification-in-progress = File operations are in progress.
trash = Trash
recents = Recents
@ -63,7 +64,6 @@ apply-to-all = Apply to all
keep-both = Keep both
skip = Skip
## Metadata Dialog
owner = Owner
group = Group
@ -77,6 +77,22 @@ execute = Execute
## About
git-description = Git commit {$hash} on {$date}
## Add Network Drive
add-network-drive = Add network drive
connect = Connect
connect-anonymously = Connect anonymously
connecting = Connecting...
domain = Domain
enter-server-address = Enter server address
network-drive-description =
Server addresses include a protocol prefix and address.
Examples: ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-error = Unable to access network drive
password = Password
remember-password = Remember password
try-again = Try again
username = Username
## Operations
edit-history = Edit history
history = History

View file

@ -63,7 +63,9 @@ use crate::{
key_bind::key_binds,
localize::LANGUAGE_SORTER,
menu, mime_app, mime_icon,
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters},
mounter::{
mounters, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters,
},
operation::{Operation, ReplaceResult},
spawn_detached::spawn_detached,
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
@ -246,6 +248,7 @@ pub enum Message {
DialogComplete,
DialogPush(DialogPage),
DialogUpdate(DialogPage),
DialogUpdateComplete(DialogPage),
EditLocation(Option<Entity>),
ExtractHere(Option<Entity>),
Key(Modifiers, Key),
@ -257,6 +260,10 @@ pub enum Message {
NavBarClose(Entity),
NavBarContext(Entity),
NavMenuAction(NavMenuAction),
NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkDriveInput(String),
NetworkDriveSubmit,
NetworkResult(MounterKey, String, Result<bool, String>),
NewItem(Option<Entity>, bool),
#[cfg(feature = "notify")]
Notification(Arc<Mutex<notify_rust::NotificationHandle>>),
@ -314,6 +321,7 @@ pub enum Message {
pub enum ContextPage {
About,
EditHistory,
NetworkDrive,
OpenWith,
Properties(Option<ContextItem>),
Settings,
@ -324,6 +332,7 @@ impl ContextPage {
match self {
Self::About => String::new(),
Self::EditHistory => fl!("edit-history"),
Self::NetworkDrive => fl!("add-network-drive"),
Self::OpenWith => fl!("open-with"),
Self::Properties(..) => String::default(),
Self::Settings => fl!("settings"),
@ -367,6 +376,17 @@ pub enum DialogPage {
},
EmptyTrash,
FailedOperation(u64),
NetworkAuth {
mounter_key: MounterKey,
uri: String,
auth: MounterAuth,
auth_tx: mpsc::Sender<MounterAuth>,
},
NetworkError {
mounter_key: MounterKey,
uri: String,
error: String,
},
NewItem {
parent: PathBuf,
name: String,
@ -433,6 +453,8 @@ pub struct App {
modifiers: Modifiers,
mounters: Mounters,
mounter_items: HashMap<MounterKey, MounterItems>,
network_drive_connecting: Option<(MounterKey, String)>,
network_drive_input: String,
#[cfg(feature = "notify")]
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
pending_operation_id: u64,
@ -635,6 +657,7 @@ impl App {
});
}
}
nav_model = nav_model.insert(|b| {
b.text(fl!("trash"))
.icon(widget::icon::icon(tab::trash_icon_symbolic(16)))
@ -642,6 +665,17 @@ impl App {
.divider_above()
});
nav_model = nav_model.insert(|b| {
b.text(fl!("networks"))
.icon(widget::icon::icon(
widget::icon::from_name("network-workgroup-symbolic")
.size(16)
.handle(),
))
.data(Location::Networks)
.divider_above()
});
// Collect all mounter items
let mut nav_items = Vec::new();
for (key, items) in self.mounter_items.iter() {
@ -794,6 +828,31 @@ impl App {
.into()
}
fn network_drive(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_m, .. } = theme::active().cosmic().spacing;
let mut text_input =
widget::text_input(fl!("enter-server-address"), &self.network_drive_input);
let button = if self.network_drive_connecting.is_some() {
widget::button::standard(fl!("connecting"))
} else {
text_input = text_input
.on_input(Message::NetworkDriveInput)
.on_submit(Message::NetworkDriveSubmit);
widget::button::standard(fl!("connect")).on_press(Message::NetworkDriveSubmit)
};
widget::column::with_children(vec![
text_input.into(),
widget::text(fl!("network-drive-description")).into(),
widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
button.into(),
])
.into(),
])
.spacing(space_m)
.into()
}
fn open_with(&self) -> Element<Message> {
let mut children = Vec::new();
let entity = self.tab_model.active();
@ -1118,6 +1177,8 @@ impl Application for App {
modifiers: Modifiers::empty(),
mounters: mounters(),
mounter_items: HashMap::new(),
network_drive_connecting: None,
network_drive_input: String::new(),
#[cfg(feature = "notify")]
notification_opt: None,
pending_operation_id: 0,
@ -1422,6 +1483,31 @@ impl Application for App {
DialogPage::FailedOperation(id) => {
log::warn!("TODO: retry operation {}", id);
}
DialogPage::NetworkAuth {
mounter_key,
uri,
auth,
auth_tx,
} => {
return Command::perform(
async move {
auth_tx.send(auth).await.unwrap();
message::none()
},
|x| x,
);
}
DialogPage::NetworkError {
mounter_key,
uri,
error,
} => {
//TODO: re-use mounter_key?
return Command::batch([
self.update(Message::NetworkDriveInput(uri)),
self.update(Message::NetworkDriveSubmit),
]);
}
DialogPage::NewItem { parent, name, dir } => {
let path = parent.join(name);
self.operation(if dir {
@ -1450,6 +1536,12 @@ impl Application for App {
self.dialog_pages[0] = dialog_page;
}
}
Message::DialogUpdateComplete(dialog_page) => {
return Command::batch([
self.update(Message::DialogUpdate(dialog_page)),
self.update(Message::DialogComplete),
]);
}
Message::EditLocation(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(location) = self.tab_model.data::<Tab>(entity).and_then(|tab| {
@ -1569,6 +1661,55 @@ impl Application for App {
return Command::batch(commands);
}
Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {
self.dialog_pages.push_back(DialogPage::NetworkAuth {
mounter_key,
uri,
auth,
auth_tx,
});
}
Message::NetworkDriveInput(input) => {
self.network_drive_input = input;
}
Message::NetworkDriveSubmit => {
//TODO: know which mounter to use for network drives
for (mounter_key, mounter) in self.mounters.iter() {
self.network_drive_connecting =
Some((*mounter_key, self.network_drive_input.clone()));
return mounter
.network_drive(self.network_drive_input.clone())
.map(|_| message::none());
}
log::warn!(
"no mounter found for connecting to {:?}",
self.network_drive_input
);
}
Message::NetworkResult(mounter_key, uri, res) => {
if self.network_drive_connecting == Some((mounter_key, uri.clone())) {
self.network_drive_connecting = None;
}
match res {
Ok(true) => {
log::info!("connected to {:?}", uri);
if matches!(self.context_page, ContextPage::NetworkDrive) {
self.core.window.show_context = false;
}
}
Ok(false) => {
log::info!("cancelled connection to {:?}", uri);
}
Err(error) => {
log::warn!("failed to connect to {:?}: {}", uri, error);
self.dialog_pages.push_back(DialogPage::NetworkError {
mounter_key,
uri,
error,
});
}
}
}
Message::NewItem(entity_opt, dir) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
@ -2090,6 +2231,12 @@ impl Application for App {
tab::Command::Action(action) => {
commands.push(self.update(action.message(Some(entity))));
}
tab::Command::AddNetworkDrive => {
let context_page = ContextPage::NetworkDrive;
self.context_page = context_page;
self.core.window.show_context = true;
self.set_context_title(context_page.title());
}
tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => {
self.activate_nav_model_location(&tab_path);
@ -2544,6 +2691,7 @@ impl Application for App {
Some(match self.context_page {
ContextPage::About => self.about(),
ContextPage::EditHistory => self.edit_history(),
ContextPage::NetworkDrive => self.network_drive(),
ContextPage::OpenWith => self.open_with(),
ContextPage::Properties(entity) => self.properties(entity),
ContextPage::Settings => self.settings(),
@ -2556,7 +2704,9 @@ impl Application for App {
None => return None,
};
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let cosmic_theme::Spacing {
space_xxs, space_s, ..
} = theme::active().cosmic().spacing;
let dialog = match dialog_page {
DialogPage::Compress {
@ -2658,6 +2808,125 @@ impl Application for App {
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
}
DialogPage::NetworkAuth {
mounter_key,
uri,
auth,
auth_tx,
} => {
//TODO: use URI!
let mut controls = Vec::with_capacity(4);
if let Some(username) = &auth.username_opt {
//TODO: what should submit do?
controls.push(
widget::text_input(fl!("username"), username)
.on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key,
uri: uri.clone(),
auth: MounterAuth {
username_opt: Some(value),
..auth.clone()
},
auth_tx: auth_tx.clone(),
})
})
.into(),
);
}
if let Some(domain) = &auth.domain_opt {
//TODO: what should submit do?
controls.push(
widget::text_input(fl!("domain"), domain)
.on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key,
uri: uri.clone(),
auth: MounterAuth {
domain_opt: Some(value),
..auth.clone()
},
auth_tx: auth_tx.clone(),
})
})
.into(),
);
}
if let Some(password) = &auth.password_opt {
//TODO: what should submit do?
//TODO: button for showing password
controls.push(
widget::secure_input(fl!("password"), password, None, true)
.on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key,
uri: uri.clone(),
auth: MounterAuth {
password_opt: Some(value),
..auth.clone()
},
auth_tx: auth_tx.clone(),
})
})
.into(),
);
}
if let Some(remember) = &auth.remember_opt {
//TODO: what should submit do?
//TODO: button for showing password
controls.push(
widget::checkbox(fl!("remember-password"), *remember, move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key,
uri: uri.clone(),
auth: MounterAuth {
remember_opt: Some(value),
..auth.clone()
},
auth_tx: auth_tx.clone(),
})
})
.into(),
);
}
let mut parts = auth.message.splitn(2, '\n');
let title = parts.next().unwrap_or_default();
let body = parts.next().unwrap_or_default();
widget::dialog(title)
.body(body)
.control(widget::column::with_children(controls).spacing(space_s))
.primary_action(
widget::button::suggested(fl!("connect")).on_press(Message::DialogComplete),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
.tertiary_action(widget::button::text(fl!("connect-anonymously")).on_press(
Message::DialogUpdateComplete(DialogPage::NetworkAuth {
mounter_key: *mounter_key,
uri: uri.clone(),
auth: MounterAuth {
anonymous_opt: Some(true),
..auth.clone()
},
auth_tx: auth_tx.clone(),
}),
))
}
DialogPage::NetworkError {
mounter_key,
uri,
error,
} => widget::dialog(fl!("network-drive-error"))
.body(error)
.icon(widget::icon::from_name("dialog-error").size(64))
.primary_action(
widget::button::standard(fl!("try-again")).on_press(Message::DialogComplete),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
),
DialogPage::NewItem { parent, name, dir } => {
let mut dialog = widget::dialog(if *dir {
fl!("create-new-folder")
@ -3148,11 +3417,17 @@ impl Application for App {
for (key, mounter) in self.mounters.iter() {
let key = *key;
subscriptions.push(
mounter
.subscription()
.map(move |items| Message::MounterItems(key, items)),
);
subscriptions.push(mounter.subscription().map(move |mounter_message| {
match mounter_message {
MounterMessage::Items(items) => Message::MounterItems(key, items),
MounterMessage::NetworkAuth(uri, auth, auth_tx) => {
Message::NetworkAuth(key, uri, auth, auth_tx)
}
MounterMessage::NetworkResult(uri, res) => {
Message::NetworkResult(key, uri, res)
}
}
}));
}
if !self.pending_operations.is_empty() {

View file

@ -44,7 +44,7 @@ use crate::{
fl, home_dir,
localize::LANGUAGE_SORTER,
menu,
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters},
mounter::{mounters, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters},
tab::{self, ItemMetadata, Location, Tab},
};
@ -1329,11 +1329,15 @@ impl Application for App {
for (key, mounter) in self.mounters.iter() {
let key = *key;
subscriptions.push(
mounter
.subscription()
.map(move |items| Message::MounterItems(key, items)),
);
subscriptions.push(mounter.subscription().map(move |mounter_message| {
match mounter_message {
MounterMessage::Items(items) => Message::MounterItems(key, items),
_ => {
log::warn!("{:?} not supported in dialog mode", mounter_message);
Message::None
}
}
}));
}
Subscription::batch(subscriptions)

View file

@ -197,6 +197,9 @@ pub fn context_menu<'a>(
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
}
}
(_, Location::Networks) => {
//TODO: networks context menu?
}
(_, Location::Trash) => {
if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());

View file

@ -6,7 +6,7 @@ use gio::{glib, prelude::*};
use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
use tokio::sync::{mpsc, Mutex};
use super::{Mounter, MounterItem, MounterItems};
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
@ -24,12 +24,15 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
enum Cmd {
Rescan,
Mount(MounterItem),
NetworkDrive(String),
Unmount(MounterItem),
}
enum Event {
Changed,
Items(MounterItems),
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkResult(String, Result<bool, String>),
}
#[derive(Clone, Debug)]
@ -190,6 +193,80 @@ impl Gvfs {
);
}
}
Cmd::NetworkDrive(uri) => {
let mount_op = gio::MountOperation::new();
{
let event_tx = event_tx.clone();
let uri = uri.clone();
mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| {
let auth = MounterAuth {
message: message.to_string(),
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
Some(default_user.to_string())
} else {
None
},
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
Some(default_domain.to_string())
} else {
None
},
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
Some(String::new())
} else {
None
},
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
Some(false)
} else {
None
},
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
Some(false)
} else {
None
}
};
let (auth_tx, mut auth_rx) = mpsc::channel(1);
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap();
//TODO: async recv?
if let Some(auth) = auth_rx.blocking_recv() {
if auth.anonymous_opt == Some(true) {
mount_op.set_anonymous(true);
} else {
mount_op.set_username(auth.username_opt.as_deref());
mount_op.set_domain(auth.domain_opt.as_deref());
mount_op.set_password(auth.password_opt.as_deref());
if auth.remember_opt == Some(true) {
mount_op.set_password_save(gio::PasswordSave::Permanently);
}
}
mount_op.reply(gio::MountOperationResult::Handled);
} else {
mount_op.reply(gio::MountOperationResult::Aborted);
}
});
}
let file = gio::File::for_uri(&uri);
let event_tx = event_tx.clone();
file.mount_enclosing_volume(
gio::MountMountFlags::empty(),
Some(&mount_op),
gio::Cancellable::NONE,
move |res| {
log::info!("network drive {}: result {:?}", uri, res);
event_tx.send(Event::NetworkResult(uri, match res {
Ok(()) => Ok(true),
Err(err) => match err.kind::<gio::IOErrorEnum>() {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{}", err))
}
})).unwrap();
}
);
}
Cmd::Unmount(mounter_item) => {
let MounterItem::Gvfs(item) = mounter_item else { continue };
let ItemKind::Mount = item.kind else { continue };
@ -242,6 +319,17 @@ impl Mounter for Gvfs {
)
}
fn network_drive(&self, uri: String) -> Command<()> {
let command_tx = self.command_tx.clone();
Command::perform(
async move {
command_tx.send(Cmd::NetworkDrive(uri)).unwrap();
()
},
|x| x,
)
}
fn unmount(&self, item: MounterItem) -> Command<()> {
let command_tx = self.command_tx.clone();
Command::perform(
@ -253,17 +341,23 @@ impl Mounter for Gvfs {
)
}
fn subscription(&self) -> subscription::Subscription<MounterItems> {
fn subscription(&self) -> subscription::Subscription<MounterMessage> {
let command_tx = self.command_tx.clone();
let event_rx = self.event_rx.clone();
subscription::channel(TypeId::of::<Self>(), 1, |mut output| async move {
command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.lock().await.recv().await {
match event {
Event::Changed => {
command_tx.send(Cmd::Rescan).unwrap();
}
Event::Items(items) => output.send(items).await.unwrap(),
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
Event::Items(items) => output.send(MounterMessage::Items(items)).await.unwrap(),
Event::NetworkAuth(uri, auth, auth_tx) => output
.send(MounterMessage::NetworkAuth(uri, auth, auth_tx))
.await
.unwrap(),
Event::NetworkResult(uri, res) => output
.send(MounterMessage::NetworkResult(uri, res))
.await
.unwrap(),
}
}
pending().await

View file

@ -1,9 +1,40 @@
use cosmic::{iced::subscription, widget, Command};
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
use tokio::sync::mpsc;
#[cfg(feature = "gvfs")]
mod gvfs;
#[derive(Clone)]
pub struct MounterAuth {
pub message: String,
pub username_opt: Option<String>,
pub domain_opt: Option<String>,
pub password_opt: Option<String>,
pub remember_opt: Option<bool>,
pub anonymous_opt: Option<bool>,
}
// Custom debug for MounterAuth to hide password
impl fmt::Debug for MounterAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MounterAuth")
.field("username_opt", &self.username_opt)
.field("domain_opt", &self.domain_opt)
.field(
"password_opt",
if self.password_opt.is_some() {
&"Some(*)"
} else {
&"None"
},
)
.field("remember_opt", &self.remember_opt)
.field("anonymous_opt", &self.anonymous_opt)
.finish()
}
}
#[derive(Clone, Debug)]
pub enum MounterItem {
#[cfg(feature = "gvfs")]
@ -48,11 +79,19 @@ impl MounterItem {
pub type MounterItems = Vec<MounterItem>;
#[derive(Clone, Debug)]
pub enum MounterMessage {
Items(MounterItems),
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkResult(String, Result<bool, String>),
}
pub trait Mounter {
//TODO: send result
fn mount(&self, item: MounterItem) -> Command<()>;
fn network_drive(&self, uri: String) -> Command<()>;
fn unmount(&self, item: MounterItem) -> Command<()>;
fn subscription(&self) -> subscription::Subscription<MounterItems>;
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]

View file

@ -724,12 +724,18 @@ 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![]
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Location {
Path(PathBuf),
Search(PathBuf, String),
Trash,
Recents,
Networks,
}
impl std::fmt::Display for Location {
@ -739,6 +745,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"),
}
}
}
@ -750,6 +757,7 @@ impl Location {
Self::Search(path, term) => scan_search(path, term, sizes),
Self::Trash => scan_trash(sizes),
Self::Recents => scan_recents(sizes),
Self::Networks => scan_networks(sizes),
}
}
}
@ -757,6 +765,7 @@ impl Location {
#[derive(Debug)]
pub enum Command {
Action(Action),
AddNetworkDrive,
ChangeLocation(String, Location, Option<PathBuf>),
DropFiles(PathBuf, ClipboardPaste),
EmptyTrash,
@ -770,6 +779,7 @@ pub enum Command {
#[derive(Clone, Debug)]
pub enum Message {
AddNetworkDrive,
Click(Option<usize>),
DoubleClick(Option<usize>),
ClickRelease(Option<usize>),
@ -1227,7 +1237,6 @@ impl Tab {
}
pub fn title(&self) -> String {
//TODO: better title
match &self.location {
Location::Path(path) => {
let (name, _) = folder_name(path);
@ -1244,6 +1253,9 @@ impl Tab {
Location::Recents => {
fl!("recents")
}
Location::Networks => {
fl!("networks")
}
}
}
@ -1518,6 +1530,9 @@ impl Tab {
let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
match message {
Message::AddNetworkDrive => {
commands.push(Command::AddNetworkDrive);
}
Message::ClickRelease(click_i_opt) => {
if click_i_opt == self.clicked.take() {
return commands;
@ -1931,13 +1946,9 @@ impl Tab {
commands.push(Command::OpenFile(path.clone()));
}
}
Location::Search(_path, _term) => {
_ => {
cd = Some(location);
}
Location::Trash => {
cd = Some(location);
}
Location::Recents => cd = Some(location),
}
}
Message::LocationUp => {
@ -2086,6 +2097,9 @@ impl Tab {
Location::Recents => {
log::warn!("Copy to recents is not supported.");
}
Location::Networks => {
log::warn!("Copy to networks is not supported.");
}
};
}
Message::Drop(None) => {
@ -2170,8 +2184,7 @@ impl Tab {
if match &location {
Location::Path(path) => path.is_dir(),
Location::Search(path, _term) => path.is_dir(),
Location::Trash => true,
Location::Recents => true,
_ => true,
} {
let prev_path = if let Location::Path(path) = &self.location {
Some(path.clone())
@ -2531,14 +2544,8 @@ impl Tab {
children.reverse();
}
Location::Trash => {
let mut row = widget::row::with_capacity(2)
.align_items(Alignment::Center)
.spacing(space_xxxs);
row = row.push(widget::icon::icon(trash_icon_symbolic(16)).size(16));
row = row.push(widget::text::heading(fl!("trash")));
children.push(
widget::button(row)
widget::button(widget::text::heading(fl!("trash")))
.padding(space_xxxs)
.on_press(Message::Location(Location::Trash))
.style(theme::Button::Text)
@ -2546,20 +2553,23 @@ impl Tab {
);
}
Location::Recents => {
let mut row = widget::row::with_capacity(2)
.align_items(Alignment::Center)
.spacing(space_xxxs);
row = row.push(widget::icon::from_name("document-open-recent-symbolic").size(16));
row = row.push(widget::text::heading(fl!("recents")));
children.push(
widget::button(row)
widget::button(widget::text::heading(fl!("recents")))
.padding(space_xxxs)
.on_press(Message::Location(Location::Recents))
.style(theme::Button::Text)
.into(),
);
}
Location::Networks => {
children.push(
widget::button(widget::text::heading(fl!("networks")))
.padding(space_xxxs)
.on_press(Message::Location(Location::Networks))
.style(theme::Button::Text)
.into(),
);
}
}
for child in children {
@ -3273,6 +3283,12 @@ impl Tab {
// Update cached size
self.size_opt.set(Some(size));
let cosmic_theme::Spacing {
space_xxs,
space_xs,
..
} = theme::active().cosmic().spacing;
let location_view_opt = if matches!(self.mode, Mode::Desktop) {
None
} else {
@ -3350,27 +3366,36 @@ impl Tab {
} else {
tab_column = tab_column.push(popover);
}
if let Location::Trash = self.location {
if let Some(items) = self.items_opt() {
if !items.is_empty() {
let cosmic_theme::Spacing {
space_xxs,
space_xs,
..
} = theme::active().cosmic().spacing;
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::button::standard(fl!("empty-trash"))
.on_press(Message::EmptyTrash)
.into(),
]))
.padding([space_xxs, space_xs])
.layer(cosmic_theme::Layer::Primary),
);
match &self.location {
Location::Trash => {
if let Some(items) = self.items_opt() {
if !items.is_empty() {
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::button::standard(fl!("empty-trash"))
.on_press(Message::EmptyTrash)
.into(),
]))
.padding([space_xxs, space_xs])
.layer(cosmic_theme::Layer::Primary),
);
}
}
}
Location::Networks => {
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::button::standard(fl!("add-network-drive"))
.on_press(Message::AddNetworkDrive)
.into(),
]))
.padding([space_xxs, space_xs])
.layer(cosmic_theme::Layer::Primary),
);
}
_ => {}
}
let mut tab_view = widget::container(tab_column)
.height(Length::Fill)