Implement network drive connection, part of #202
This commit is contained in:
parent
f41730978c
commit
0d8fd00dd3
8 changed files with 542 additions and 86 deletions
46
Cargo.lock
generated
46
Cargo.lock
generated
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
289
src/app.rs
289
src/app.rs
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
107
src/tab.rs
107
src/tab.rs
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue