mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 14:31:18 -07:00
Notifications
This commit is contained in:
@@ -27,7 +27,8 @@
|
||||
*/
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
||||
use crate::system::auto_update::{Update, UpdateStatus};
|
||||
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||
use crate::system::notifications::Notification;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### protocol_opt_to_enum
|
||||
@@ -155,6 +156,51 @@ impl AuthActivity {
|
||||
|
||||
// -- update install
|
||||
|
||||
/// ### check_for_updates
|
||||
///
|
||||
/// If enabled in configuration, check for updates from Github
|
||||
pub(super) fn check_for_updates(&mut self) {
|
||||
debug!("Check for updates...");
|
||||
// Check version only if unset in the store
|
||||
let ctx = self.context_mut();
|
||||
if !ctx.store().isset(super::STORE_KEY_LATEST_VERSION) {
|
||||
debug!("Version is not set in storage");
|
||||
if ctx.config().get_check_for_updates() {
|
||||
debug!("Check for updates is enabled");
|
||||
// Send request
|
||||
match Update::is_new_version_available() {
|
||||
Ok(Some(Release { version, body })) => {
|
||||
// If some, store version and release notes
|
||||
info!("Latest version is: {}", version);
|
||||
if ctx.config().get_notifications() {
|
||||
// Notify new version available
|
||||
Notification::update_available(version.as_str());
|
||||
}
|
||||
// Store info
|
||||
ctx.store_mut()
|
||||
.set_string(super::STORE_KEY_LATEST_VERSION, version);
|
||||
ctx.store_mut()
|
||||
.set_string(super::STORE_KEY_RELEASE_NOTES, body);
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
|
||||
// Just set flag as check
|
||||
ctx.store_mut().set(super::STORE_KEY_LATEST_VERSION);
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
error!("Failed to get latest version: {}", err);
|
||||
self.mount_error(
|
||||
format!("Could not check for new updates: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Check for updates is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### install_update
|
||||
///
|
||||
/// Install latest termscp version via GUI
|
||||
@@ -173,9 +219,17 @@ impl AuthActivity {
|
||||
match result {
|
||||
Ok(UpdateStatus::AlreadyUptodate) => self.mount_info("termscp is already up to date!"),
|
||||
Ok(UpdateStatus::UpdateInstalled(ver)) => {
|
||||
if self.config().get_notifications() {
|
||||
Notification::update_installed(ver.as_str());
|
||||
}
|
||||
self.mount_info(format!("termscp has been updated to version {}!", ver))
|
||||
}
|
||||
Err(err) => self.mount_error(format!("Could not install update: {}", err)),
|
||||
Err(err) => {
|
||||
if self.config().get_notifications() {
|
||||
Notification::update_failed(err.to_string());
|
||||
}
|
||||
self.mount_error(format!("Could not install update: {}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ mod view;
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::auto_update::{Release, Update as TermscpUpdate};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event;
|
||||
@@ -110,45 +110,6 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_create
|
||||
///
|
||||
/// If enabled in configuration, check for updates from Github
|
||||
fn check_for_updates(&mut self) {
|
||||
debug!("Check for updates...");
|
||||
// Check version only if unset in the store
|
||||
let ctx: &mut Context = self.context_mut();
|
||||
if !ctx.store().isset(STORE_KEY_LATEST_VERSION) {
|
||||
debug!("Version is not set in storage");
|
||||
if ctx.config().get_check_for_updates() {
|
||||
debug!("Check for updates is enabled");
|
||||
// Send request
|
||||
match TermscpUpdate::is_new_version_available() {
|
||||
Ok(Some(Release { version, body })) => {
|
||||
// If some, store version and release notes
|
||||
info!("Latest version is: {}", version);
|
||||
ctx.store_mut()
|
||||
.set_string(STORE_KEY_LATEST_VERSION, version);
|
||||
ctx.store_mut().set_string(STORE_KEY_RELEASE_NOTES, body);
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
|
||||
// Just set flag as check
|
||||
ctx.store_mut().set(STORE_KEY_LATEST_VERSION);
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
error!("Failed to get latest version: {}", err);
|
||||
self.mount_error(
|
||||
format!("Could not check for new updates: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Check for updates is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### context
|
||||
///
|
||||
/// Returns a reference to context
|
||||
@@ -163,6 +124,13 @@ impl AuthActivity {
|
||||
self.context.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// ### config
|
||||
///
|
||||
/// Returns config client reference
|
||||
fn config(&self) -> &ConfigClient {
|
||||
self.context().config()
|
||||
}
|
||||
|
||||
/// ### theme
|
||||
///
|
||||
/// Returns a reference to theme
|
||||
|
||||
@@ -87,6 +87,13 @@ impl TransferStates {
|
||||
pub fn aborted(&self) -> bool {
|
||||
self.aborted
|
||||
}
|
||||
|
||||
/// ### full_size
|
||||
///
|
||||
/// Returns the size of the entire transfer
|
||||
pub fn full_size(&self) -> usize {
|
||||
self.full.total
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProgressStates {
|
||||
@@ -305,6 +312,8 @@ mod test {
|
||||
assert_eq!(states.aborted(), true);
|
||||
states.reset();
|
||||
assert_eq!(states.aborted(), false);
|
||||
states.full.total = 1024;
|
||||
assert_eq!(states.full_size(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,12 +22,15 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
|
||||
use crate::filetransfer::ProtocolParams;
|
||||
use crate::system::environment;
|
||||
use crate::system::notifications::Notification;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
use crate::utils::path;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tuirealm::Update;
|
||||
@@ -146,4 +149,86 @@ impl FileTransferActivity {
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_connection_msg
|
||||
///
|
||||
/// Get connection message to show to client
|
||||
pub(super) fn get_connection_msg(params: &ProtocolParams) -> String {
|
||||
match params {
|
||||
ProtocolParams::Generic(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
format!("Connecting to {}:{}…", params.address, params.port)
|
||||
}
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {} ({})",
|
||||
params.bucket_name, params.region
|
||||
);
|
||||
format!("Connecting to {}…", params.bucket_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### notify_transfer_completed
|
||||
///
|
||||
/// Send notification regarding transfer completed
|
||||
/// The notification is sent only when these conditions are satisfied:
|
||||
///
|
||||
/// - notifications are enabled
|
||||
/// - transfer size is greater or equal than notification threshold
|
||||
pub(super) fn notify_transfer_completed(&self, payload: &TransferPayload) {
|
||||
if self.config().get_notifications()
|
||||
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||
{
|
||||
Notification::transfer_completed(self.transfer_completed_msg(payload));
|
||||
}
|
||||
}
|
||||
|
||||
/// ### notify_transfer_error
|
||||
///
|
||||
/// Send notification regarding transfer error
|
||||
/// The notification is sent only when these conditions are satisfied:
|
||||
///
|
||||
/// - notifications are enabled
|
||||
/// - transfer size is greater or equal than notification threshold
|
||||
pub(super) fn notify_transfer_error(&self, msg: &str) {
|
||||
if self.config().get_notifications()
|
||||
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||
{
|
||||
Notification::transfer_error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_completed_msg(&self, payload: &TransferPayload) -> String {
|
||||
let transfer_stats = format!(
|
||||
"took {} seconds; at {}/s",
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
);
|
||||
match payload {
|
||||
TransferPayload::File(file) => {
|
||||
format!(
|
||||
"File \"{}\" has been successfully transferred ({})",
|
||||
file.name, transfer_stats
|
||||
)
|
||||
}
|
||||
TransferPayload::Any(entry) => {
|
||||
format!(
|
||||
"\"{}\" has been successfully transferred ({})",
|
||||
entry.get_name(),
|
||||
transfer_stats
|
||||
)
|
||||
}
|
||||
TransferPayload::Many(entries) => {
|
||||
format!(
|
||||
"{} files has been successfully transferred ({})",
|
||||
entries.len(),
|
||||
transfer_stats
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(self) mod view;
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::FsEntry;
|
||||
@@ -239,28 +239,6 @@ impl FileTransferActivity {
|
||||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
|
||||
/// ### get_connection_msg
|
||||
///
|
||||
/// Get connection message to show to client
|
||||
fn get_connection_msg(params: &ProtocolParams) -> String {
|
||||
match params {
|
||||
ProtocolParams::Generic(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
format!("Connecting to {}:{}…", params.address, params.port)
|
||||
}
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {} ({})",
|
||||
params.bucket_name, params.region
|
||||
);
|
||||
format!("Connecting to {}…", params.bucket_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -206,17 +206,27 @@ impl FileTransferActivity {
|
||||
dst_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// Use different method based on payload
|
||||
match payload {
|
||||
TransferPayload::Any(entry) => {
|
||||
self.filetransfer_send_any(&entry, curr_remote_path, dst_name)
|
||||
let result = match payload {
|
||||
TransferPayload::Any(ref entry) => {
|
||||
self.filetransfer_send_any(entry, curr_remote_path, dst_name)
|
||||
}
|
||||
TransferPayload::File(file) => {
|
||||
self.filetransfer_send_file(&file, curr_remote_path, dst_name)
|
||||
TransferPayload::File(ref file) => {
|
||||
self.filetransfer_send_file(file, curr_remote_path, dst_name)
|
||||
}
|
||||
TransferPayload::Many(entries) => {
|
||||
TransferPayload::Many(ref entries) => {
|
||||
self.filetransfer_send_many(entries, curr_remote_path)
|
||||
}
|
||||
};
|
||||
// Notify
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
self.notify_transfer_completed(&payload);
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify_transfer_error(e.as_str());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_file
|
||||
@@ -268,10 +278,10 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
|
||||
// Send recurse
|
||||
self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_many
|
||||
@@ -279,7 +289,7 @@ impl FileTransferActivity {
|
||||
/// Send many entries to remote
|
||||
fn filetransfer_send_many(
|
||||
&mut self,
|
||||
entries: Vec<FsEntry>,
|
||||
entries: &[FsEntry],
|
||||
curr_remote_path: &Path,
|
||||
) -> Result<(), String> {
|
||||
// Reset states
|
||||
@@ -293,12 +303,14 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
|
||||
// Send recurse
|
||||
entries
|
||||
let result = entries
|
||||
.iter()
|
||||
.for_each(|x| self.filetransfer_send_recurse(x, curr_remote_path, None));
|
||||
.map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
|
||||
.find(|x| x.is_err())
|
||||
.unwrap_or(Ok(()));
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
|
||||
fn filetransfer_send_recurse(
|
||||
@@ -306,7 +318,7 @@ impl FileTransferActivity {
|
||||
entry: &FsEntry,
|
||||
curr_remote_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
// Write popup
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
@@ -320,44 +332,42 @@ impl FileTransferActivity {
|
||||
};
|
||||
remote_path.push(remote_file_name);
|
||||
// Match entry
|
||||
match entry {
|
||||
let result: Result<(), String> = match entry {
|
||||
FsEntry::File(file) => {
|
||||
if let Err(err) = self.filetransfer_send_one(file, remote_path.as_path(), file_name)
|
||||
{
|
||||
// Log error
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Failed to upload file {}: {}", file.name, err),
|
||||
);
|
||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||
if matches!(
|
||||
err,
|
||||
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
|
||||
) {
|
||||
// Stat file on remote and remove it if exists
|
||||
match self.client.stat(remote_path.as_path()) {
|
||||
Err(err) => self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
remote_path.display(),
|
||||
err
|
||||
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
|
||||
Err(err) => {
|
||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||
if matches!(
|
||||
err,
|
||||
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
|
||||
) {
|
||||
// Stat file on remote and remove it if exists
|
||||
match self.client.stat(remote_path.as_path()) {
|
||||
Err(err) => self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
remote_path.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
),
|
||||
Ok(entry) => {
|
||||
if let Err(err) = self.client.remove(&entry) {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
remote_path.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
Ok(entry) => {
|
||||
if let Err(err) = self.client.remove(&entry) {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
remote_path.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err.to_string())
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
@@ -387,7 +397,7 @@ impl FileTransferActivity {
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
return Err(err.to_string());
|
||||
}
|
||||
}
|
||||
// Get files in dir
|
||||
@@ -400,8 +410,13 @@ impl FileTransferActivity {
|
||||
break;
|
||||
}
|
||||
// Send entry; name is always None after first call
|
||||
self.filetransfer_send_recurse(entry, remote_path.as_path(), None);
|
||||
if let Err(err) =
|
||||
self.filetransfer_send_recurse(entry, remote_path.as_path(), None)
|
||||
{
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -412,10 +427,11 @@ impl FileTransferActivity {
|
||||
err
|
||||
),
|
||||
);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Scan dir on remote
|
||||
self.reload_remote_dir();
|
||||
// If aborted; show popup
|
||||
@@ -426,6 +442,7 @@ impl FileTransferActivity {
|
||||
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_file
|
||||
@@ -613,11 +630,23 @@ impl FileTransferActivity {
|
||||
local_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
match payload {
|
||||
TransferPayload::Any(entry) => self.filetransfer_recv_any(&entry, local_path, dst_name),
|
||||
TransferPayload::File(file) => self.filetransfer_recv_file(&file, local_path),
|
||||
TransferPayload::Many(entries) => self.filetransfer_recv_many(entries, local_path),
|
||||
let result = match payload {
|
||||
TransferPayload::Any(ref entry) => {
|
||||
self.filetransfer_recv_any(entry, local_path, dst_name)
|
||||
}
|
||||
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path),
|
||||
TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path),
|
||||
};
|
||||
// Notify
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
self.notify_transfer_completed(&payload);
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify_transfer_error(e.as_str());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_recv_any
|
||||
@@ -639,10 +668,10 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
|
||||
// Receive
|
||||
self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
||||
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_recv_file
|
||||
@@ -669,7 +698,7 @@ impl FileTransferActivity {
|
||||
/// Send many entries to remote
|
||||
fn filetransfer_recv_many(
|
||||
&mut self,
|
||||
entries: Vec<FsEntry>,
|
||||
entries: &[FsEntry],
|
||||
curr_remote_path: &Path,
|
||||
) -> Result<(), String> {
|
||||
// Reset states
|
||||
@@ -683,12 +712,14 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
|
||||
// Send recurse
|
||||
entries
|
||||
let result = entries
|
||||
.iter()
|
||||
.for_each(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None));
|
||||
.map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
|
||||
.find(|x| x.is_err())
|
||||
.unwrap_or(Ok(()));
|
||||
// Umount progress bar
|
||||
self.umount_progress_bar();
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
|
||||
fn filetransfer_recv_recurse(
|
||||
@@ -696,14 +727,14 @@ impl FileTransferActivity {
|
||||
entry: &FsEntry,
|
||||
local_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
// Write popup
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Match entry
|
||||
match entry {
|
||||
let result: Result<(), String> = match entry {
|
||||
FsEntry::File(file) => {
|
||||
// Get local file
|
||||
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
||||
@@ -716,10 +747,6 @@ impl FileTransferActivity {
|
||||
if let Err(err) =
|
||||
self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
|
||||
{
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not download file {}: {}", file.name, err),
|
||||
);
|
||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||
if matches!(
|
||||
err,
|
||||
@@ -749,6 +776,9 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err.to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
@@ -798,12 +828,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
// Receive entry; name is always None after first call
|
||||
// Local path becomes local_dir_path
|
||||
self.filetransfer_recv_recurse(
|
||||
if let Err(err) = self.filetransfer_recv_recurse(
|
||||
entry,
|
||||
local_dir_path.as_path(),
|
||||
None,
|
||||
);
|
||||
) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -814,6 +847,7 @@ impl FileTransferActivity {
|
||||
err
|
||||
),
|
||||
);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -826,10 +860,11 @@ impl FileTransferActivity {
|
||||
err
|
||||
),
|
||||
);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Reload directory on local
|
||||
self.reload_local_dir();
|
||||
// if aborted; show alert
|
||||
@@ -843,6 +878,7 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// ### filetransfer_recv_one
|
||||
|
||||
@@ -58,6 +58,8 @@ const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLA
|
||||
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
||||
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
|
||||
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
|
||||
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
|
||||
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
|
||||
// -- ssh keys
|
||||
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
|
||||
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
|
||||
|
||||
@@ -40,11 +40,13 @@ use super::{
|
||||
COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME,
|
||||
COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
|
||||
COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE,
|
||||
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
||||
COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
|
||||
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
|
||||
COMPONENT_RADIO_NOTIFICATIONS_ENABLED, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR,
|
||||
COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::ui::keymap::*;
|
||||
use crate::utils::parser::parse_color;
|
||||
@@ -103,10 +105,26 @@ impl SetupActivity {
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_TEXT_EDITOR);
|
||||
None
|
||||
}
|
||||
// Input field <UP>
|
||||
(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
|
||||
None
|
||||
@@ -136,7 +154,7 @@ impl SetupActivity {
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
||||
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
|
||||
None
|
||||
}
|
||||
// Error <ENTER> or <ESC>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
use super::{Context, SetupActivity};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
@@ -143,8 +144,8 @@ impl SetupActivity {
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label("File formatter syntax (local)", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
@@ -153,12 +154,35 @@ impl SetupActivity {
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||
.with_label("File formatter syntax (remote)", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightRed)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||
.with_title("Enable notifications?", Alignment::Left)
|
||||
.with_options(&[String::from("Yes"), String::from("No")])
|
||||
.rewind(true)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||
Box::new(Bytes::new(
|
||||
BytesPropsBuilder::default()
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_label("Notifications: minimum transfer size", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Load values
|
||||
self.load_input_values();
|
||||
}
|
||||
@@ -173,7 +197,7 @@ impl SetupActivity {
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Length(21), // Main body
|
||||
Constraint::Length(18), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
@@ -182,8 +206,13 @@ impl SetupActivity {
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
// Make chunks
|
||||
// Make chunks (two columns)
|
||||
let ui_cfg_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
// Column 1
|
||||
let ui_cfg_chunks_col1 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
@@ -193,31 +222,65 @@ impl SetupActivity {
|
||||
Constraint::Length(3), // Updates tab
|
||||
Constraint::Length(3), // Prompt file replace
|
||||
Constraint::Length(3), // Group dirs
|
||||
Constraint::Length(3), // Local Format input
|
||||
Constraint::Length(3), // Remote Format input
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
.split(ui_cfg_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
|
||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
f,
|
||||
ui_cfg_chunks_col1[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
f,
|
||||
ui_cfg_chunks_col1[2],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
|
||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||
f,
|
||||
ui_cfg_chunks[4],
|
||||
ui_cfg_chunks_col1[4],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[6]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[7]);
|
||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
|
||||
// Column 2
|
||||
let ui_cfg_chunks_col2 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Local Format input
|
||||
Constraint::Length(3), // Remote Format input
|
||||
Constraint::Length(3), // Notifications enabled
|
||||
Constraint::Length(3), // Notifications threshold
|
||||
Constraint::Length(1), // Filler
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(ui_cfg_chunks[1]);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
f,
|
||||
ui_cfg_chunks_col2[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
f,
|
||||
ui_cfg_chunks_col2[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||
f,
|
||||
ui_cfg_chunks_col2[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||
f,
|
||||
ui_cfg_chunks_col2[3],
|
||||
);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
@@ -341,6 +404,31 @@ impl SetupActivity {
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||
}
|
||||
// Notifications enabled
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||
{
|
||||
let enabled: usize = match self.config().get_notifications() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
|
||||
}
|
||||
// Notifications threshold
|
||||
if let Some(props) = self
|
||||
.view
|
||||
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||
{
|
||||
let value: u64 = self.config().get_notification_threshold();
|
||||
let props = BytesPropsBuilder::from(props).with_value(value).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_input_values
|
||||
@@ -404,5 +492,17 @@ impl SetupActivity {
|
||||
};
|
||||
self.config_mut().set_group_dirs(dirs);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) = self
|
||||
.view
|
||||
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||
{
|
||||
self.config_mut().set_notifications(opt == 0);
|
||||
}
|
||||
if let Some(Payload::One(Value::U64(bytes))) = self
|
||||
.view
|
||||
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||
{
|
||||
self.config_mut().set_notification_threshold(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user