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]]
|
||||
name = "cosmic-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"atomicwrites",
|
||||
"cosmic-config-derive",
|
||||
|
|
@ -1232,7 +1232,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
@ -1339,7 +1339,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"almost",
|
||||
"cosmic-config",
|
||||
|
|
@ -2782,7 +2782,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2801,7 +2801,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_accessibility"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_unix",
|
||||
|
|
@ -2811,7 +2811,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_core"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"dnd",
|
||||
|
|
@ -2833,7 +2833,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_futures"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_core",
|
||||
|
|
@ -2846,7 +2846,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_graphics"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
|
|
@ -2870,7 +2870,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_renderer"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"iced_graphics",
|
||||
"iced_tiny_skia",
|
||||
|
|
@ -2882,7 +2882,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_runtime"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2896,7 +2896,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_sctk"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"enum-repr",
|
||||
"float-cmp",
|
||||
|
|
@ -2923,7 +2923,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_style"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"iced_core",
|
||||
"once_cell",
|
||||
|
|
@ -2933,7 +2933,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_tiny_skia"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
|
|
@ -2950,7 +2950,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_wgpu"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"bitflags 2.6.0",
|
||||
|
|
@ -2979,7 +2979,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_widget"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2997,7 +2997,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_winit"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -3513,7 +3513,7 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#c497c227ce806dec84cf66ffcae07745374b1ef8"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#f942977703404e43ade60080f14d61e7aa078733"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"ashpd 0.9.1",
|
||||
|
|
@ -3601,7 +3601,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
|||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
"redox_syscall 0.5.3",
|
||||
"redox_syscall 0.5.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3736,9 +3736,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64"
|
||||
checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dirs-next",
|
||||
|
|
@ -4436,7 +4436,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.3",
|
||||
"redox_syscall 0.5.4",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
|
@ -4847,9 +4847,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ empty-folder-hidden = Empty folder (has hidden items)
|
|||
no-results = No results found
|
||||
filesystem = Filesystem
|
||||
home = Home
|
||||
networks = Networks
|
||||
notification-in-progress = File operations are in progress.
|
||||
trash = Trash
|
||||
recents = Recents
|
||||
|
|
@ -63,7 +64,6 @@ apply-to-all = Apply to all
|
|||
keep-both = Keep both
|
||||
skip = Skip
|
||||
|
||||
|
||||
## Metadata Dialog
|
||||
owner = Owner
|
||||
group = Group
|
||||
|
|
@ -77,6 +77,22 @@ execute = Execute
|
|||
## About
|
||||
git-description = Git commit {$hash} on {$date}
|
||||
|
||||
## Add Network Drive
|
||||
add-network-drive = Add network drive
|
||||
connect = Connect
|
||||
connect-anonymously = Connect anonymously
|
||||
connecting = Connecting...
|
||||
domain = Domain
|
||||
enter-server-address = Enter server address
|
||||
network-drive-description =
|
||||
Server addresses include a protocol prefix and address.
|
||||
Examples: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
network-drive-error = Unable to access network drive
|
||||
password = Password
|
||||
remember-password = Remember password
|
||||
try-again = Try again
|
||||
username = Username
|
||||
|
||||
## Operations
|
||||
edit-history = Edit history
|
||||
history = History
|
||||
|
|
|
|||
289
src/app.rs
289
src/app.rs
|
|
@ -63,7 +63,9 @@ use crate::{
|
|||
key_bind::key_binds,
|
||||
localize::LANGUAGE_SORTER,
|
||||
menu, mime_app, mime_icon,
|
||||
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters},
|
||||
mounter::{
|
||||
mounters, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters,
|
||||
},
|
||||
operation::{Operation, ReplaceResult},
|
||||
spawn_detached::spawn_detached,
|
||||
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
||||
|
|
@ -246,6 +248,7 @@ pub enum Message {
|
|||
DialogComplete,
|
||||
DialogPush(DialogPage),
|
||||
DialogUpdate(DialogPage),
|
||||
DialogUpdateComplete(DialogPage),
|
||||
EditLocation(Option<Entity>),
|
||||
ExtractHere(Option<Entity>),
|
||||
Key(Modifiers, Key),
|
||||
|
|
@ -257,6 +260,10 @@ pub enum Message {
|
|||
NavBarClose(Entity),
|
||||
NavBarContext(Entity),
|
||||
NavMenuAction(NavMenuAction),
|
||||
NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender<MounterAuth>),
|
||||
NetworkDriveInput(String),
|
||||
NetworkDriveSubmit,
|
||||
NetworkResult(MounterKey, String, Result<bool, String>),
|
||||
NewItem(Option<Entity>, bool),
|
||||
#[cfg(feature = "notify")]
|
||||
Notification(Arc<Mutex<notify_rust::NotificationHandle>>),
|
||||
|
|
@ -314,6 +321,7 @@ pub enum Message {
|
|||
pub enum ContextPage {
|
||||
About,
|
||||
EditHistory,
|
||||
NetworkDrive,
|
||||
OpenWith,
|
||||
Properties(Option<ContextItem>),
|
||||
Settings,
|
||||
|
|
@ -324,6 +332,7 @@ impl ContextPage {
|
|||
match self {
|
||||
Self::About => String::new(),
|
||||
Self::EditHistory => fl!("edit-history"),
|
||||
Self::NetworkDrive => fl!("add-network-drive"),
|
||||
Self::OpenWith => fl!("open-with"),
|
||||
Self::Properties(..) => String::default(),
|
||||
Self::Settings => fl!("settings"),
|
||||
|
|
@ -367,6 +376,17 @@ pub enum DialogPage {
|
|||
},
|
||||
EmptyTrash,
|
||||
FailedOperation(u64),
|
||||
NetworkAuth {
|
||||
mounter_key: MounterKey,
|
||||
uri: String,
|
||||
auth: MounterAuth,
|
||||
auth_tx: mpsc::Sender<MounterAuth>,
|
||||
},
|
||||
NetworkError {
|
||||
mounter_key: MounterKey,
|
||||
uri: String,
|
||||
error: String,
|
||||
},
|
||||
NewItem {
|
||||
parent: PathBuf,
|
||||
name: String,
|
||||
|
|
@ -433,6 +453,8 @@ pub struct App {
|
|||
modifiers: Modifiers,
|
||||
mounters: Mounters,
|
||||
mounter_items: HashMap<MounterKey, MounterItems>,
|
||||
network_drive_connecting: Option<(MounterKey, String)>,
|
||||
network_drive_input: String,
|
||||
#[cfg(feature = "notify")]
|
||||
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
|
||||
pending_operation_id: u64,
|
||||
|
|
@ -635,6 +657,7 @@ impl App {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
nav_model = nav_model.insert(|b| {
|
||||
b.text(fl!("trash"))
|
||||
.icon(widget::icon::icon(tab::trash_icon_symbolic(16)))
|
||||
|
|
@ -642,6 +665,17 @@ impl App {
|
|||
.divider_above()
|
||||
});
|
||||
|
||||
nav_model = nav_model.insert(|b| {
|
||||
b.text(fl!("networks"))
|
||||
.icon(widget::icon::icon(
|
||||
widget::icon::from_name("network-workgroup-symbolic")
|
||||
.size(16)
|
||||
.handle(),
|
||||
))
|
||||
.data(Location::Networks)
|
||||
.divider_above()
|
||||
});
|
||||
|
||||
// Collect all mounter items
|
||||
let mut nav_items = Vec::new();
|
||||
for (key, items) in self.mounter_items.iter() {
|
||||
|
|
@ -794,6 +828,31 @@ impl App {
|
|||
.into()
|
||||
}
|
||||
|
||||
fn network_drive(&self) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_m, .. } = theme::active().cosmic().spacing;
|
||||
let mut text_input =
|
||||
widget::text_input(fl!("enter-server-address"), &self.network_drive_input);
|
||||
let button = if self.network_drive_connecting.is_some() {
|
||||
widget::button::standard(fl!("connecting"))
|
||||
} else {
|
||||
text_input = text_input
|
||||
.on_input(Message::NetworkDriveInput)
|
||||
.on_submit(Message::NetworkDriveSubmit);
|
||||
widget::button::standard(fl!("connect")).on_press(Message::NetworkDriveSubmit)
|
||||
};
|
||||
widget::column::with_children(vec![
|
||||
text_input.into(),
|
||||
widget::text(fl!("network-drive-description")).into(),
|
||||
widget::row::with_children(vec![
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
button.into(),
|
||||
])
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_m)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn open_with(&self) -> Element<Message> {
|
||||
let mut children = Vec::new();
|
||||
let entity = self.tab_model.active();
|
||||
|
|
@ -1118,6 +1177,8 @@ impl Application for App {
|
|||
modifiers: Modifiers::empty(),
|
||||
mounters: mounters(),
|
||||
mounter_items: HashMap::new(),
|
||||
network_drive_connecting: None,
|
||||
network_drive_input: String::new(),
|
||||
#[cfg(feature = "notify")]
|
||||
notification_opt: None,
|
||||
pending_operation_id: 0,
|
||||
|
|
@ -1422,6 +1483,31 @@ impl Application for App {
|
|||
DialogPage::FailedOperation(id) => {
|
||||
log::warn!("TODO: retry operation {}", id);
|
||||
}
|
||||
DialogPage::NetworkAuth {
|
||||
mounter_key,
|
||||
uri,
|
||||
auth,
|
||||
auth_tx,
|
||||
} => {
|
||||
return Command::perform(
|
||||
async move {
|
||||
auth_tx.send(auth).await.unwrap();
|
||||
message::none()
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
DialogPage::NetworkError {
|
||||
mounter_key,
|
||||
uri,
|
||||
error,
|
||||
} => {
|
||||
//TODO: re-use mounter_key?
|
||||
return Command::batch([
|
||||
self.update(Message::NetworkDriveInput(uri)),
|
||||
self.update(Message::NetworkDriveSubmit),
|
||||
]);
|
||||
}
|
||||
DialogPage::NewItem { parent, name, dir } => {
|
||||
let path = parent.join(name);
|
||||
self.operation(if dir {
|
||||
|
|
@ -1450,6 +1536,12 @@ impl Application for App {
|
|||
self.dialog_pages[0] = dialog_page;
|
||||
}
|
||||
}
|
||||
Message::DialogUpdateComplete(dialog_page) => {
|
||||
return Command::batch([
|
||||
self.update(Message::DialogUpdate(dialog_page)),
|
||||
self.update(Message::DialogComplete),
|
||||
]);
|
||||
}
|
||||
Message::EditLocation(entity_opt) => {
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
if let Some(location) = self.tab_model.data::<Tab>(entity).and_then(|tab| {
|
||||
|
|
@ -1569,6 +1661,55 @@ impl Application for App {
|
|||
|
||||
return Command::batch(commands);
|
||||
}
|
||||
Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {
|
||||
self.dialog_pages.push_back(DialogPage::NetworkAuth {
|
||||
mounter_key,
|
||||
uri,
|
||||
auth,
|
||||
auth_tx,
|
||||
});
|
||||
}
|
||||
Message::NetworkDriveInput(input) => {
|
||||
self.network_drive_input = input;
|
||||
}
|
||||
Message::NetworkDriveSubmit => {
|
||||
//TODO: know which mounter to use for network drives
|
||||
for (mounter_key, mounter) in self.mounters.iter() {
|
||||
self.network_drive_connecting =
|
||||
Some((*mounter_key, self.network_drive_input.clone()));
|
||||
return mounter
|
||||
.network_drive(self.network_drive_input.clone())
|
||||
.map(|_| message::none());
|
||||
}
|
||||
log::warn!(
|
||||
"no mounter found for connecting to {:?}",
|
||||
self.network_drive_input
|
||||
);
|
||||
}
|
||||
Message::NetworkResult(mounter_key, uri, res) => {
|
||||
if self.network_drive_connecting == Some((mounter_key, uri.clone())) {
|
||||
self.network_drive_connecting = None;
|
||||
}
|
||||
match res {
|
||||
Ok(true) => {
|
||||
log::info!("connected to {:?}", uri);
|
||||
if matches!(self.context_page, ContextPage::NetworkDrive) {
|
||||
self.core.window.show_context = false;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log::info!("cancelled connection to {:?}", uri);
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("failed to connect to {:?}: {}", uri, error);
|
||||
self.dialog_pages.push_back(DialogPage::NetworkError {
|
||||
mounter_key,
|
||||
uri,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::NewItem(entity_opt, dir) => {
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||
|
|
@ -2090,6 +2231,12 @@ impl Application for App {
|
|||
tab::Command::Action(action) => {
|
||||
commands.push(self.update(action.message(Some(entity))));
|
||||
}
|
||||
tab::Command::AddNetworkDrive => {
|
||||
let context_page = ContextPage::NetworkDrive;
|
||||
self.context_page = context_page;
|
||||
self.core.window.show_context = true;
|
||||
self.set_context_title(context_page.title());
|
||||
}
|
||||
tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => {
|
||||
self.activate_nav_model_location(&tab_path);
|
||||
|
||||
|
|
@ -2544,6 +2691,7 @@ impl Application for App {
|
|||
Some(match self.context_page {
|
||||
ContextPage::About => self.about(),
|
||||
ContextPage::EditHistory => self.edit_history(),
|
||||
ContextPage::NetworkDrive => self.network_drive(),
|
||||
ContextPage::OpenWith => self.open_with(),
|
||||
ContextPage::Properties(entity) => self.properties(entity),
|
||||
ContextPage::Settings => self.settings(),
|
||||
|
|
@ -2556,7 +2704,9 @@ impl Application for App {
|
|||
None => return None,
|
||||
};
|
||||
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs, space_s, ..
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let dialog = match dialog_page {
|
||||
DialogPage::Compress {
|
||||
|
|
@ -2658,6 +2808,125 @@ impl Application for App {
|
|||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
)
|
||||
}
|
||||
DialogPage::NetworkAuth {
|
||||
mounter_key,
|
||||
uri,
|
||||
auth,
|
||||
auth_tx,
|
||||
} => {
|
||||
//TODO: use URI!
|
||||
let mut controls = Vec::with_capacity(4);
|
||||
if let Some(username) = &auth.username_opt {
|
||||
//TODO: what should submit do?
|
||||
controls.push(
|
||||
widget::text_input(fl!("username"), username)
|
||||
.on_input(move |value| {
|
||||
Message::DialogUpdate(DialogPage::NetworkAuth {
|
||||
mounter_key: *mounter_key,
|
||||
uri: uri.clone(),
|
||||
auth: MounterAuth {
|
||||
username_opt: Some(value),
|
||||
..auth.clone()
|
||||
},
|
||||
auth_tx: auth_tx.clone(),
|
||||
})
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(domain) = &auth.domain_opt {
|
||||
//TODO: what should submit do?
|
||||
controls.push(
|
||||
widget::text_input(fl!("domain"), domain)
|
||||
.on_input(move |value| {
|
||||
Message::DialogUpdate(DialogPage::NetworkAuth {
|
||||
mounter_key: *mounter_key,
|
||||
uri: uri.clone(),
|
||||
auth: MounterAuth {
|
||||
domain_opt: Some(value),
|
||||
..auth.clone()
|
||||
},
|
||||
auth_tx: auth_tx.clone(),
|
||||
})
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(password) = &auth.password_opt {
|
||||
//TODO: what should submit do?
|
||||
//TODO: button for showing password
|
||||
controls.push(
|
||||
widget::secure_input(fl!("password"), password, None, true)
|
||||
.on_input(move |value| {
|
||||
Message::DialogUpdate(DialogPage::NetworkAuth {
|
||||
mounter_key: *mounter_key,
|
||||
uri: uri.clone(),
|
||||
auth: MounterAuth {
|
||||
password_opt: Some(value),
|
||||
..auth.clone()
|
||||
},
|
||||
auth_tx: auth_tx.clone(),
|
||||
})
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(remember) = &auth.remember_opt {
|
||||
//TODO: what should submit do?
|
||||
//TODO: button for showing password
|
||||
controls.push(
|
||||
widget::checkbox(fl!("remember-password"), *remember, move |value| {
|
||||
Message::DialogUpdate(DialogPage::NetworkAuth {
|
||||
mounter_key: *mounter_key,
|
||||
uri: uri.clone(),
|
||||
auth: MounterAuth {
|
||||
remember_opt: Some(value),
|
||||
..auth.clone()
|
||||
},
|
||||
auth_tx: auth_tx.clone(),
|
||||
})
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut parts = auth.message.splitn(2, '\n');
|
||||
let title = parts.next().unwrap_or_default();
|
||||
let body = parts.next().unwrap_or_default();
|
||||
widget::dialog(title)
|
||||
.body(body)
|
||||
.control(widget::column::with_children(controls).spacing(space_s))
|
||||
.primary_action(
|
||||
widget::button::suggested(fl!("connect")).on_press(Message::DialogComplete),
|
||||
)
|
||||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
)
|
||||
.tertiary_action(widget::button::text(fl!("connect-anonymously")).on_press(
|
||||
Message::DialogUpdateComplete(DialogPage::NetworkAuth {
|
||||
mounter_key: *mounter_key,
|
||||
uri: uri.clone(),
|
||||
auth: MounterAuth {
|
||||
anonymous_opt: Some(true),
|
||||
..auth.clone()
|
||||
},
|
||||
auth_tx: auth_tx.clone(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
DialogPage::NetworkError {
|
||||
mounter_key,
|
||||
uri,
|
||||
error,
|
||||
} => widget::dialog(fl!("network-drive-error"))
|
||||
.body(error)
|
||||
.icon(widget::icon::from_name("dialog-error").size(64))
|
||||
.primary_action(
|
||||
widget::button::standard(fl!("try-again")).on_press(Message::DialogComplete),
|
||||
)
|
||||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
),
|
||||
DialogPage::NewItem { parent, name, dir } => {
|
||||
let mut dialog = widget::dialog(if *dir {
|
||||
fl!("create-new-folder")
|
||||
|
|
@ -3148,11 +3417,17 @@ impl Application for App {
|
|||
|
||||
for (key, mounter) in self.mounters.iter() {
|
||||
let key = *key;
|
||||
subscriptions.push(
|
||||
mounter
|
||||
.subscription()
|
||||
.map(move |items| Message::MounterItems(key, items)),
|
||||
);
|
||||
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
||||
match mounter_message {
|
||||
MounterMessage::Items(items) => Message::MounterItems(key, items),
|
||||
MounterMessage::NetworkAuth(uri, auth, auth_tx) => {
|
||||
Message::NetworkAuth(key, uri, auth, auth_tx)
|
||||
}
|
||||
MounterMessage::NetworkResult(uri, res) => {
|
||||
Message::NetworkResult(key, uri, res)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if !self.pending_operations.is_empty() {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ use crate::{
|
|||
fl, home_dir,
|
||||
localize::LANGUAGE_SORTER,
|
||||
menu,
|
||||
mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters},
|
||||
mounter::{mounters, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters},
|
||||
tab::{self, ItemMetadata, Location, Tab},
|
||||
};
|
||||
|
||||
|
|
@ -1329,11 +1329,15 @@ impl Application for App {
|
|||
|
||||
for (key, mounter) in self.mounters.iter() {
|
||||
let key = *key;
|
||||
subscriptions.push(
|
||||
mounter
|
||||
.subscription()
|
||||
.map(move |items| Message::MounterItems(key, items)),
|
||||
);
|
||||
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
||||
match mounter_message {
|
||||
MounterMessage::Items(items) => Message::MounterItems(key, items),
|
||||
_ => {
|
||||
log::warn!("{:?} not supported in dialog mode", mounter_message);
|
||||
Message::None
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Subscription::batch(subscriptions)
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@ pub fn context_menu<'a>(
|
|||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
(_, Location::Networks) => {
|
||||
//TODO: networks context menu?
|
||||
}
|
||||
(_, Location::Trash) => {
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use gio::{glib, prelude::*};
|
|||
use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use super::{Mounter, MounterItem, MounterItems};
|
||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||
|
||||
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||
|
|
@ -24,12 +24,15 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
|||
enum Cmd {
|
||||
Rescan,
|
||||
Mount(MounterItem),
|
||||
NetworkDrive(String),
|
||||
Unmount(MounterItem),
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Changed,
|
||||
Items(MounterItems),
|
||||
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
|
||||
NetworkResult(String, Result<bool, String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -190,6 +193,80 @@ impl Gvfs {
|
|||
);
|
||||
}
|
||||
}
|
||||
Cmd::NetworkDrive(uri) => {
|
||||
let mount_op = gio::MountOperation::new();
|
||||
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
let uri = uri.clone();
|
||||
mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| {
|
||||
let auth = MounterAuth {
|
||||
message: message.to_string(),
|
||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
||||
Some(default_user.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
||||
Some(default_domain.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
||||
Some(String::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap();
|
||||
//TODO: async recv?
|
||||
if let Some(auth) = auth_rx.blocking_recv() {
|
||||
if auth.anonymous_opt == Some(true) {
|
||||
mount_op.set_anonymous(true);
|
||||
} else {
|
||||
mount_op.set_username(auth.username_opt.as_deref());
|
||||
mount_op.set_domain(auth.domain_opt.as_deref());
|
||||
mount_op.set_password(auth.password_opt.as_deref());
|
||||
if auth.remember_opt == Some(true) {
|
||||
mount_op.set_password_save(gio::PasswordSave::Permanently);
|
||||
}
|
||||
}
|
||||
mount_op.reply(gio::MountOperationResult::Handled);
|
||||
} else {
|
||||
mount_op.reply(gio::MountOperationResult::Aborted);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let file = gio::File::for_uri(&uri);
|
||||
let event_tx = event_tx.clone();
|
||||
file.mount_enclosing_volume(
|
||||
gio::MountMountFlags::empty(),
|
||||
Some(&mount_op),
|
||||
gio::Cancellable::NONE,
|
||||
move |res| {
|
||||
log::info!("network drive {}: result {:?}", uri, res);
|
||||
event_tx.send(Event::NetworkResult(uri, match res {
|
||||
Ok(()) => Ok(true),
|
||||
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
||||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||
_ => Err(format!("{}", err))
|
||||
}
|
||||
})).unwrap();
|
||||
}
|
||||
);
|
||||
}
|
||||
Cmd::Unmount(mounter_item) => {
|
||||
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
||||
let ItemKind::Mount = item.kind else { continue };
|
||||
|
|
@ -242,6 +319,17 @@ impl Mounter for Gvfs {
|
|||
)
|
||||
}
|
||||
|
||||
fn network_drive(&self, uri: String) -> Command<()> {
|
||||
let command_tx = self.command_tx.clone();
|
||||
Command::perform(
|
||||
async move {
|
||||
command_tx.send(Cmd::NetworkDrive(uri)).unwrap();
|
||||
()
|
||||
},
|
||||
|x| x,
|
||||
)
|
||||
}
|
||||
|
||||
fn unmount(&self, item: MounterItem) -> Command<()> {
|
||||
let command_tx = self.command_tx.clone();
|
||||
Command::perform(
|
||||
|
|
@ -253,17 +341,23 @@ impl Mounter for Gvfs {
|
|||
)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> subscription::Subscription<MounterItems> {
|
||||
fn subscription(&self) -> subscription::Subscription<MounterMessage> {
|
||||
let command_tx = self.command_tx.clone();
|
||||
let event_rx = self.event_rx.clone();
|
||||
subscription::channel(TypeId::of::<Self>(), 1, |mut output| async move {
|
||||
command_tx.send(Cmd::Rescan).unwrap();
|
||||
while let Some(event) = event_rx.lock().await.recv().await {
|
||||
match event {
|
||||
Event::Changed => {
|
||||
command_tx.send(Cmd::Rescan).unwrap();
|
||||
}
|
||||
Event::Items(items) => output.send(items).await.unwrap(),
|
||||
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
|
||||
Event::Items(items) => output.send(MounterMessage::Items(items)).await.unwrap(),
|
||||
Event::NetworkAuth(uri, auth, auth_tx) => output
|
||||
.send(MounterMessage::NetworkAuth(uri, auth, auth_tx))
|
||||
.await
|
||||
.unwrap(),
|
||||
Event::NetworkResult(uri, res) => output
|
||||
.send(MounterMessage::NetworkResult(uri, res))
|
||||
.await
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
pending().await
|
||||
|
|
|
|||
|
|
@ -1,9 +1,40 @@
|
|||
use cosmic::{iced::subscription, widget, Command};
|
||||
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
||||
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[cfg(feature = "gvfs")]
|
||||
mod gvfs;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MounterAuth {
|
||||
pub message: String,
|
||||
pub username_opt: Option<String>,
|
||||
pub domain_opt: Option<String>,
|
||||
pub password_opt: Option<String>,
|
||||
pub remember_opt: Option<bool>,
|
||||
pub anonymous_opt: Option<bool>,
|
||||
}
|
||||
|
||||
// Custom debug for MounterAuth to hide password
|
||||
impl fmt::Debug for MounterAuth {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MounterAuth")
|
||||
.field("username_opt", &self.username_opt)
|
||||
.field("domain_opt", &self.domain_opt)
|
||||
.field(
|
||||
"password_opt",
|
||||
if self.password_opt.is_some() {
|
||||
&"Some(*)"
|
||||
} else {
|
||||
&"None"
|
||||
},
|
||||
)
|
||||
.field("remember_opt", &self.remember_opt)
|
||||
.field("anonymous_opt", &self.anonymous_opt)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MounterItem {
|
||||
#[cfg(feature = "gvfs")]
|
||||
|
|
@ -48,11 +79,19 @@ impl MounterItem {
|
|||
|
||||
pub type MounterItems = Vec<MounterItem>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MounterMessage {
|
||||
Items(MounterItems),
|
||||
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
|
||||
NetworkResult(String, Result<bool, String>),
|
||||
}
|
||||
|
||||
pub trait Mounter {
|
||||
//TODO: send result
|
||||
fn mount(&self, item: MounterItem) -> Command<()>;
|
||||
fn network_drive(&self, uri: String) -> Command<()>;
|
||||
fn unmount(&self, item: MounterItem) -> Command<()>;
|
||||
fn subscription(&self) -> subscription::Subscription<MounterItems>;
|
||||
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
|
|
|
|||
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()
|
||||
}
|
||||
|
||||
pub fn scan_networks(sizes: IconSizes) -> Vec<Item> {
|
||||
//TODO: network folder items
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Location {
|
||||
Path(PathBuf),
|
||||
Search(PathBuf, String),
|
||||
Trash,
|
||||
Recents,
|
||||
Networks,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Location {
|
||||
|
|
@ -739,6 +745,7 @@ impl std::fmt::Display for Location {
|
|||
Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
|
||||
Self::Trash => write!(f, "trash"),
|
||||
Self::Recents => write!(f, "recents"),
|
||||
Self::Networks => write!(f, "networks"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -750,6 +757,7 @@ impl Location {
|
|||
Self::Search(path, term) => scan_search(path, term, sizes),
|
||||
Self::Trash => scan_trash(sizes),
|
||||
Self::Recents => scan_recents(sizes),
|
||||
Self::Networks => scan_networks(sizes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -757,6 +765,7 @@ impl Location {
|
|||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
Action(Action),
|
||||
AddNetworkDrive,
|
||||
ChangeLocation(String, Location, Option<PathBuf>),
|
||||
DropFiles(PathBuf, ClipboardPaste),
|
||||
EmptyTrash,
|
||||
|
|
@ -770,6 +779,7 @@ pub enum Command {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
AddNetworkDrive,
|
||||
Click(Option<usize>),
|
||||
DoubleClick(Option<usize>),
|
||||
ClickRelease(Option<usize>),
|
||||
|
|
@ -1227,7 +1237,6 @@ impl Tab {
|
|||
}
|
||||
|
||||
pub fn title(&self) -> String {
|
||||
//TODO: better title
|
||||
match &self.location {
|
||||
Location::Path(path) => {
|
||||
let (name, _) = folder_name(path);
|
||||
|
|
@ -1244,6 +1253,9 @@ impl Tab {
|
|||
Location::Recents => {
|
||||
fl!("recents")
|
||||
}
|
||||
Location::Networks => {
|
||||
fl!("networks")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1518,6 +1530,9 @@ impl Tab {
|
|||
let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
|
||||
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
|
||||
match message {
|
||||
Message::AddNetworkDrive => {
|
||||
commands.push(Command::AddNetworkDrive);
|
||||
}
|
||||
Message::ClickRelease(click_i_opt) => {
|
||||
if click_i_opt == self.clicked.take() {
|
||||
return commands;
|
||||
|
|
@ -1931,13 +1946,9 @@ impl Tab {
|
|||
commands.push(Command::OpenFile(path.clone()));
|
||||
}
|
||||
}
|
||||
Location::Search(_path, _term) => {
|
||||
_ => {
|
||||
cd = Some(location);
|
||||
}
|
||||
Location::Trash => {
|
||||
cd = Some(location);
|
||||
}
|
||||
Location::Recents => cd = Some(location),
|
||||
}
|
||||
}
|
||||
Message::LocationUp => {
|
||||
|
|
@ -2086,6 +2097,9 @@ impl Tab {
|
|||
Location::Recents => {
|
||||
log::warn!("Copy to recents is not supported.");
|
||||
}
|
||||
Location::Networks => {
|
||||
log::warn!("Copy to networks is not supported.");
|
||||
}
|
||||
};
|
||||
}
|
||||
Message::Drop(None) => {
|
||||
|
|
@ -2170,8 +2184,7 @@ impl Tab {
|
|||
if match &location {
|
||||
Location::Path(path) => path.is_dir(),
|
||||
Location::Search(path, _term) => path.is_dir(),
|
||||
Location::Trash => true,
|
||||
Location::Recents => true,
|
||||
_ => true,
|
||||
} {
|
||||
let prev_path = if let Location::Path(path) = &self.location {
|
||||
Some(path.clone())
|
||||
|
|
@ -2531,14 +2544,8 @@ impl Tab {
|
|||
children.reverse();
|
||||
}
|
||||
Location::Trash => {
|
||||
let mut row = widget::row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(space_xxxs);
|
||||
row = row.push(widget::icon::icon(trash_icon_symbolic(16)).size(16));
|
||||
row = row.push(widget::text::heading(fl!("trash")));
|
||||
|
||||
children.push(
|
||||
widget::button(row)
|
||||
widget::button(widget::text::heading(fl!("trash")))
|
||||
.padding(space_xxxs)
|
||||
.on_press(Message::Location(Location::Trash))
|
||||
.style(theme::Button::Text)
|
||||
|
|
@ -2546,20 +2553,23 @@ impl Tab {
|
|||
);
|
||||
}
|
||||
Location::Recents => {
|
||||
let mut row = widget::row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(space_xxxs);
|
||||
row = row.push(widget::icon::from_name("document-open-recent-symbolic").size(16));
|
||||
row = row.push(widget::text::heading(fl!("recents")));
|
||||
|
||||
children.push(
|
||||
widget::button(row)
|
||||
widget::button(widget::text::heading(fl!("recents")))
|
||||
.padding(space_xxxs)
|
||||
.on_press(Message::Location(Location::Recents))
|
||||
.style(theme::Button::Text)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Location::Networks => {
|
||||
children.push(
|
||||
widget::button(widget::text::heading(fl!("networks")))
|
||||
.padding(space_xxxs)
|
||||
.on_press(Message::Location(Location::Networks))
|
||||
.style(theme::Button::Text)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for child in children {
|
||||
|
|
@ -3273,6 +3283,12 @@ impl Tab {
|
|||
// Update cached size
|
||||
self.size_opt.set(Some(size));
|
||||
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs,
|
||||
space_xs,
|
||||
..
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let location_view_opt = if matches!(self.mode, Mode::Desktop) {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -3350,27 +3366,36 @@ impl Tab {
|
|||
} else {
|
||||
tab_column = tab_column.push(popover);
|
||||
}
|
||||
if let Location::Trash = self.location {
|
||||
if let Some(items) = self.items_opt() {
|
||||
if !items.is_empty() {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs,
|
||||
space_xs,
|
||||
..
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children(vec![
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::button::standard(fl!("empty-trash"))
|
||||
.on_press(Message::EmptyTrash)
|
||||
.into(),
|
||||
]))
|
||||
.padding([space_xxs, space_xs])
|
||||
.layer(cosmic_theme::Layer::Primary),
|
||||
);
|
||||
match &self.location {
|
||||
Location::Trash => {
|
||||
if let Some(items) = self.items_opt() {
|
||||
if !items.is_empty() {
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children(vec![
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::button::standard(fl!("empty-trash"))
|
||||
.on_press(Message::EmptyTrash)
|
||||
.into(),
|
||||
]))
|
||||
.padding([space_xxs, space_xs])
|
||||
.layer(cosmic_theme::Layer::Primary),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Location::Networks => {
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children(vec![
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::button::standard(fl!("add-network-drive"))
|
||||
.on_press(Message::AddNetworkDrive)
|
||||
.into(),
|
||||
]))
|
||||
.padding([space_xxs, space_xs])
|
||||
.layer(cosmic_theme::Layer::Primary),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut tab_view = widget::container(tab_column)
|
||||
.height(Length::Fill)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue