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

View file

@ -4,6 +4,7 @@ empty-folder-hidden = Empty folder (has hidden items)
no-results = No results found no-results = No results found
filesystem = Filesystem filesystem = Filesystem
home = Home home = Home
networks = Networks
notification-in-progress = File operations are in progress. notification-in-progress = File operations are in progress.
trash = Trash trash = Trash
recents = Recents recents = Recents
@ -63,7 +64,6 @@ apply-to-all = Apply to all
keep-both = Keep both keep-both = Keep both
skip = Skip skip = Skip
## Metadata Dialog ## Metadata Dialog
owner = Owner owner = Owner
group = Group group = Group
@ -77,6 +77,22 @@ execute = Execute
## About ## About
git-description = Git commit {$hash} on {$date} 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 ## Operations
edit-history = Edit history edit-history = Edit history
history = History history = History

View file

@ -63,7 +63,9 @@ use crate::{
key_bind::key_binds, key_bind::key_binds,
localize::LANGUAGE_SORTER, localize::LANGUAGE_SORTER,
menu, mime_app, mime_icon, menu, mime_app, mime_icon,
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters}, mounter::{
mounters, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters,
},
operation::{Operation, ReplaceResult}, operation::{Operation, ReplaceResult},
spawn_detached::spawn_detached, spawn_detached::spawn_detached,
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
@ -246,6 +248,7 @@ pub enum Message {
DialogComplete, DialogComplete,
DialogPush(DialogPage), DialogPush(DialogPage),
DialogUpdate(DialogPage), DialogUpdate(DialogPage),
DialogUpdateComplete(DialogPage),
EditLocation(Option<Entity>), EditLocation(Option<Entity>),
ExtractHere(Option<Entity>), ExtractHere(Option<Entity>),
Key(Modifiers, Key), Key(Modifiers, Key),
@ -257,6 +260,10 @@ pub enum Message {
NavBarClose(Entity), NavBarClose(Entity),
NavBarContext(Entity), NavBarContext(Entity),
NavMenuAction(NavMenuAction), NavMenuAction(NavMenuAction),
NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkDriveInput(String),
NetworkDriveSubmit,
NetworkResult(MounterKey, String, Result<bool, String>),
NewItem(Option<Entity>, bool), NewItem(Option<Entity>, bool),
#[cfg(feature = "notify")] #[cfg(feature = "notify")]
Notification(Arc<Mutex<notify_rust::NotificationHandle>>), Notification(Arc<Mutex<notify_rust::NotificationHandle>>),
@ -314,6 +321,7 @@ pub enum Message {
pub enum ContextPage { pub enum ContextPage {
About, About,
EditHistory, EditHistory,
NetworkDrive,
OpenWith, OpenWith,
Properties(Option<ContextItem>), Properties(Option<ContextItem>),
Settings, Settings,
@ -324,6 +332,7 @@ impl ContextPage {
match self { match self {
Self::About => String::new(), Self::About => String::new(),
Self::EditHistory => fl!("edit-history"), Self::EditHistory => fl!("edit-history"),
Self::NetworkDrive => fl!("add-network-drive"),
Self::OpenWith => fl!("open-with"), Self::OpenWith => fl!("open-with"),
Self::Properties(..) => String::default(), Self::Properties(..) => String::default(),
Self::Settings => fl!("settings"), Self::Settings => fl!("settings"),
@ -367,6 +376,17 @@ pub enum DialogPage {
}, },
EmptyTrash, EmptyTrash,
FailedOperation(u64), FailedOperation(u64),
NetworkAuth {
mounter_key: MounterKey,
uri: String,
auth: MounterAuth,
auth_tx: mpsc::Sender<MounterAuth>,
},
NetworkError {
mounter_key: MounterKey,
uri: String,
error: String,
},
NewItem { NewItem {
parent: PathBuf, parent: PathBuf,
name: String, name: String,
@ -433,6 +453,8 @@ pub struct App {
modifiers: Modifiers, modifiers: Modifiers,
mounters: Mounters, mounters: Mounters,
mounter_items: HashMap<MounterKey, MounterItems>, mounter_items: HashMap<MounterKey, MounterItems>,
network_drive_connecting: Option<(MounterKey, String)>,
network_drive_input: String,
#[cfg(feature = "notify")] #[cfg(feature = "notify")]
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>, notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
pending_operation_id: u64, pending_operation_id: u64,
@ -635,6 +657,7 @@ impl App {
}); });
} }
} }
nav_model = nav_model.insert(|b| { nav_model = nav_model.insert(|b| {
b.text(fl!("trash")) b.text(fl!("trash"))
.icon(widget::icon::icon(tab::trash_icon_symbolic(16))) .icon(widget::icon::icon(tab::trash_icon_symbolic(16)))
@ -642,6 +665,17 @@ impl App {
.divider_above() .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 // Collect all mounter items
let mut nav_items = Vec::new(); let mut nav_items = Vec::new();
for (key, items) in self.mounter_items.iter() { for (key, items) in self.mounter_items.iter() {
@ -794,6 +828,31 @@ impl App {
.into() .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> { fn open_with(&self) -> Element<Message> {
let mut children = Vec::new(); let mut children = Vec::new();
let entity = self.tab_model.active(); let entity = self.tab_model.active();
@ -1118,6 +1177,8 @@ impl Application for App {
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
mounters: mounters(), mounters: mounters(),
mounter_items: HashMap::new(), mounter_items: HashMap::new(),
network_drive_connecting: None,
network_drive_input: String::new(),
#[cfg(feature = "notify")] #[cfg(feature = "notify")]
notification_opt: None, notification_opt: None,
pending_operation_id: 0, pending_operation_id: 0,
@ -1422,6 +1483,31 @@ impl Application for App {
DialogPage::FailedOperation(id) => { DialogPage::FailedOperation(id) => {
log::warn!("TODO: retry operation {}", 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 } => { DialogPage::NewItem { parent, name, dir } => {
let path = parent.join(name); let path = parent.join(name);
self.operation(if dir { self.operation(if dir {
@ -1450,6 +1536,12 @@ impl Application for App {
self.dialog_pages[0] = dialog_page; 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) => { Message::EditLocation(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(location) = self.tab_model.data::<Tab>(entity).and_then(|tab| { 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); 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) => { Message::NewItem(entity_opt, dir) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
@ -2090,6 +2231,12 @@ impl Application for App {
tab::Command::Action(action) => { tab::Command::Action(action) => {
commands.push(self.update(action.message(Some(entity)))); 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) => { tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => {
self.activate_nav_model_location(&tab_path); self.activate_nav_model_location(&tab_path);
@ -2544,6 +2691,7 @@ impl Application for App {
Some(match self.context_page { Some(match self.context_page {
ContextPage::About => self.about(), ContextPage::About => self.about(),
ContextPage::EditHistory => self.edit_history(), ContextPage::EditHistory => self.edit_history(),
ContextPage::NetworkDrive => self.network_drive(),
ContextPage::OpenWith => self.open_with(), ContextPage::OpenWith => self.open_with(),
ContextPage::Properties(entity) => self.properties(entity), ContextPage::Properties(entity) => self.properties(entity),
ContextPage::Settings => self.settings(), ContextPage::Settings => self.settings(),
@ -2556,7 +2704,9 @@ impl Application for App {
None => return None, 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 { let dialog = match dialog_page {
DialogPage::Compress { DialogPage::Compress {
@ -2658,6 +2808,125 @@ impl Application for App {
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), 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 } => { DialogPage::NewItem { parent, name, dir } => {
let mut dialog = widget::dialog(if *dir { let mut dialog = widget::dialog(if *dir {
fl!("create-new-folder") fl!("create-new-folder")
@ -3148,11 +3417,17 @@ impl Application for App {
for (key, mounter) in self.mounters.iter() { for (key, mounter) in self.mounters.iter() {
let key = *key; let key = *key;
subscriptions.push( subscriptions.push(mounter.subscription().map(move |mounter_message| {
mounter match mounter_message {
.subscription() MounterMessage::Items(items) => Message::MounterItems(key, items),
.map(move |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() { if !self.pending_operations.is_empty() {

View file

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

View file

@ -197,6 +197,9 @@ pub fn context_menu<'a>(
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
} }
} }
(_, Location::Networks) => {
//TODO: networks context menu?
}
(_, Location::Trash) => { (_, Location::Trash) => {
if tab.mode.multiple() { if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); 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 std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
use tokio::sync::{mpsc, Mutex}; 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> { 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>() {
@ -24,12 +24,15 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
enum Cmd { enum Cmd {
Rescan, Rescan,
Mount(MounterItem), Mount(MounterItem),
NetworkDrive(String),
Unmount(MounterItem), Unmount(MounterItem),
} }
enum Event { enum Event {
Changed, Changed,
Items(MounterItems), Items(MounterItems),
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkResult(String, Result<bool, String>),
} }
#[derive(Clone, Debug)] #[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) => { 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 };
@ -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<()> { fn unmount(&self, item: MounterItem) -> Command<()> {
let command_tx = self.command_tx.clone(); let command_tx = self.command_tx.clone();
Command::perform( 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 command_tx = self.command_tx.clone();
let event_rx = self.event_rx.clone(); let event_rx = self.event_rx.clone();
subscription::channel(TypeId::of::<Self>(), 1, |mut output| async move { subscription::channel(TypeId::of::<Self>(), 1, |mut output| async move {
command_tx.send(Cmd::Rescan).unwrap(); command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.lock().await.recv().await { while let Some(event) = event_rx.lock().await.recv().await {
match event { match event {
Event::Changed => { Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
command_tx.send(Cmd::Rescan).unwrap(); Event::Items(items) => output.send(MounterMessage::Items(items)).await.unwrap(),
} Event::NetworkAuth(uri, auth, auth_tx) => output
Event::Items(items) => output.send(items).await.unwrap(), .send(MounterMessage::NetworkAuth(uri, auth, auth_tx))
.await
.unwrap(),
Event::NetworkResult(uri, res) => output
.send(MounterMessage::NetworkResult(uri, res))
.await
.unwrap(),
} }
} }
pending().await pending().await

View file

@ -1,9 +1,40 @@
use cosmic::{iced::subscription, widget, Command}; 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")] #[cfg(feature = "gvfs")]
mod 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)] #[derive(Clone, Debug)]
pub enum MounterItem { pub enum MounterItem {
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
@ -48,11 +79,19 @@ impl MounterItem {
pub type MounterItems = Vec<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 { pub trait Mounter {
//TODO: send result //TODO: send result
fn mount(&self, item: MounterItem) -> Command<()>; fn mount(&self, item: MounterItem) -> Command<()>;
fn network_drive(&self, uri: String) -> Command<()>;
fn unmount(&self, item: MounterItem) -> 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)] #[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() 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum Location { pub enum Location {
Path(PathBuf), Path(PathBuf),
Search(PathBuf, String), Search(PathBuf, String),
Trash, Trash,
Recents, Recents,
Networks,
} }
impl std::fmt::Display for Location { 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::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
Self::Trash => write!(f, "trash"), Self::Trash => write!(f, "trash"),
Self::Recents => write!(f, "recents"), 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::Search(path, term) => scan_search(path, term, sizes),
Self::Trash => scan_trash(sizes), Self::Trash => scan_trash(sizes),
Self::Recents => scan_recents(sizes), Self::Recents => scan_recents(sizes),
Self::Networks => scan_networks(sizes),
} }
} }
} }
@ -757,6 +765,7 @@ impl Location {
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
Action(Action), Action(Action),
AddNetworkDrive,
ChangeLocation(String, Location, Option<PathBuf>), ChangeLocation(String, Location, Option<PathBuf>),
DropFiles(PathBuf, ClipboardPaste), DropFiles(PathBuf, ClipboardPaste),
EmptyTrash, EmptyTrash,
@ -770,6 +779,7 @@ pub enum Command {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
AddNetworkDrive,
Click(Option<usize>), Click(Option<usize>),
DoubleClick(Option<usize>), DoubleClick(Option<usize>),
ClickRelease(Option<usize>), ClickRelease(Option<usize>),
@ -1227,7 +1237,6 @@ impl Tab {
} }
pub fn title(&self) -> String { pub fn title(&self) -> String {
//TODO: better title
match &self.location { match &self.location {
Location::Path(path) => { Location::Path(path) => {
let (name, _) = folder_name(path); let (name, _) = folder_name(path);
@ -1244,6 +1253,9 @@ impl Tab {
Location::Recents => { Location::Recents => {
fl!("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_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
match message { match message {
Message::AddNetworkDrive => {
commands.push(Command::AddNetworkDrive);
}
Message::ClickRelease(click_i_opt) => { Message::ClickRelease(click_i_opt) => {
if click_i_opt == self.clicked.take() { if click_i_opt == self.clicked.take() {
return commands; return commands;
@ -1931,13 +1946,9 @@ impl Tab {
commands.push(Command::OpenFile(path.clone())); commands.push(Command::OpenFile(path.clone()));
} }
} }
Location::Search(_path, _term) => { _ => {
cd = Some(location); cd = Some(location);
} }
Location::Trash => {
cd = Some(location);
}
Location::Recents => cd = Some(location),
} }
} }
Message::LocationUp => { Message::LocationUp => {
@ -2086,6 +2097,9 @@ impl Tab {
Location::Recents => { Location::Recents => {
log::warn!("Copy to recents is not supported."); log::warn!("Copy to recents is not supported.");
} }
Location::Networks => {
log::warn!("Copy to networks is not supported.");
}
}; };
} }
Message::Drop(None) => { Message::Drop(None) => {
@ -2170,8 +2184,7 @@ impl Tab {
if match &location { if match &location {
Location::Path(path) => path.is_dir(), Location::Path(path) => path.is_dir(),
Location::Search(path, _term) => path.is_dir(), Location::Search(path, _term) => path.is_dir(),
Location::Trash => true, _ => true,
Location::Recents => true,
} { } {
let prev_path = if let Location::Path(path) = &self.location { let prev_path = if let Location::Path(path) = &self.location {
Some(path.clone()) Some(path.clone())
@ -2531,14 +2544,8 @@ impl Tab {
children.reverse(); children.reverse();
} }
Location::Trash => { 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( children.push(
widget::button(row) widget::button(widget::text::heading(fl!("trash")))
.padding(space_xxxs) .padding(space_xxxs)
.on_press(Message::Location(Location::Trash)) .on_press(Message::Location(Location::Trash))
.style(theme::Button::Text) .style(theme::Button::Text)
@ -2546,20 +2553,23 @@ impl Tab {
); );
} }
Location::Recents => { 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( children.push(
widget::button(row) widget::button(widget::text::heading(fl!("recents")))
.padding(space_xxxs) .padding(space_xxxs)
.on_press(Message::Location(Location::Recents)) .on_press(Message::Location(Location::Recents))
.style(theme::Button::Text) .style(theme::Button::Text)
.into(), .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 { for child in children {
@ -3273,6 +3283,12 @@ impl Tab {
// Update cached size // Update cached size
self.size_opt.set(Some(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) { let location_view_opt = if matches!(self.mode, Mode::Desktop) {
None None
} else { } else {
@ -3350,27 +3366,36 @@ impl Tab {
} else { } else {
tab_column = tab_column.push(popover); tab_column = tab_column.push(popover);
} }
if let Location::Trash = self.location { match &self.location {
if let Some(items) = self.items_opt() { Location::Trash => {
if !items.is_empty() { if let Some(items) = self.items_opt() {
let cosmic_theme::Spacing { if !items.is_empty() {
space_xxs, tab_column = tab_column.push(
space_xs, widget::layer_container(widget::row::with_children(vec![
.. widget::horizontal_space(Length::Fill).into(),
} = theme::active().cosmic().spacing; widget::button::standard(fl!("empty-trash"))
.on_press(Message::EmptyTrash)
tab_column = tab_column.push( .into(),
widget::layer_container(widget::row::with_children(vec![ ]))
widget::horizontal_space(Length::Fill).into(), .padding([space_xxs, space_xs])
widget::button::standard(fl!("empty-trash")) .layer(cosmic_theme::Layer::Primary),
.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) let mut tab_view = widget::container(tab_column)
.height(Length::Fill) .height(Length::Fill)