feat(ui): Added paralell instructions and window with operations progress
This commit is contained in:
1 parent
04a0dc21b9
commit
5878a6ec4a
4 files changed
+346
-154
No files matched your search
Generated
+1
@@ -15,6 +15,7 @@ dependencies = [
|
|||||||
"mockito",
|
"mockito",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"tar",
|
"tar",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json", "stream"] }
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json", "stream"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
|
|||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
/// === Модуль для работы с удаленными хранилищами ===
|
/// === Модуль для работы с удаленными хранилищами ===
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Remote {
|
pub struct Remote {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
@@ -13,7 +14,7 @@ pub mod remotes;
|
|||||||
|
|
||||||
/// === Модуль для работы с файлами и директориями ===
|
/// === Модуль для работы с файлами и директориями ===
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! Графический интерфейс для операций rclone.
|
//! Графический интерфейс для операций rclone.
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, Spinner};
|
use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, Spinner, ProgressBar, SidePanel};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use crate::rclone_install::RcloneApp;
|
use crate::rclone_install::RcloneApp;
|
||||||
use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions};
|
use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions};
|
||||||
@@ -30,11 +30,25 @@ pub struct TransferProgress {
|
|||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Операция для отображения в правой панели.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Operation {
|
||||||
|
pub id: u32,
|
||||||
|
pub description: String,
|
||||||
|
pub progress: f32,
|
||||||
|
pub status: String,
|
||||||
|
pub start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
/// Результат длительной операции.
|
/// Результат длительной операции.
|
||||||
enum OperationResult {
|
enum OperationResult {
|
||||||
None,
|
None,
|
||||||
Success(String),
|
Success(u32, String), // (operation_id, message)
|
||||||
Failure(String),
|
Failure(u32, String), // (operation_id, error)
|
||||||
|
FileList(Vec<FileInfo>),
|
||||||
|
RemoteList(Vec<Remote>),
|
||||||
|
SearchResults(Vec<FileInfo>),
|
||||||
|
ProgressUpdate(u32, f32, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Главная структура UI.
|
/// Главная структура UI.
|
||||||
@@ -48,7 +62,7 @@ pub struct RcloneUI {
|
|||||||
current_files: Vec<FileInfo>,
|
current_files: Vec<FileInfo>,
|
||||||
selected_paths: Vec<String>,
|
selected_paths: Vec<String>,
|
||||||
|
|
||||||
transfer_source_list: Vec<String>, // список выбранных источников для копирования/перемещения
|
transfer_source_list: Vec<String>,
|
||||||
transfer_dest: String,
|
transfer_dest: String,
|
||||||
transfer_progress: Option<TransferProgress>,
|
transfer_progress: Option<TransferProgress>,
|
||||||
|
|
||||||
@@ -64,17 +78,18 @@ pub struct RcloneUI {
|
|||||||
|
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
|
|
||||||
background_working: bool,
|
active_task_count: u32,
|
||||||
operation_result: Arc<Mutex<OperationResult>>,
|
operation_result: Arc<Mutex<OperationResult>>,
|
||||||
operation_id: u32,
|
|
||||||
show_delete_remote_dialog: bool,
|
show_delete_remote_dialog: bool,
|
||||||
remote_to_delete: Option<String>,
|
remote_to_delete: Option<String>,
|
||||||
show_browser_warning: bool,
|
show_browser_warning: bool,
|
||||||
pending_remote_creation: Option<(String, String)>,
|
pending_remote_creation: Option<(String, String)>,
|
||||||
|
|
||||||
// Отложенные действия
|
|
||||||
pending_load_path: Option<String>,
|
pending_load_path: Option<String>,
|
||||||
pending_search_pattern: Option<String>,
|
pending_search_pattern: Option<String>,
|
||||||
|
rclone_init_receiver: Option<std::sync::mpsc::Receiver<Result<Arc<RcloneApp>, String>>>,
|
||||||
|
active_operations: Vec<Operation>,
|
||||||
|
next_op_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -98,6 +113,17 @@ impl Default for AppSettings {
|
|||||||
|
|
||||||
impl RcloneUI {
|
impl RcloneUI {
|
||||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let rt = Runtime::new().unwrap();
|
||||||
|
let result = rt.block_on(async { RcloneApp::new().await });
|
||||||
|
let result_string = match result {
|
||||||
|
Ok(app) => Ok(Arc::new(app)),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
};
|
||||||
|
let _ = tx.send(result_string);
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rclone: None,
|
rclone: None,
|
||||||
state: AppState::Initializing,
|
state: AppState::Initializing,
|
||||||
@@ -118,26 +144,29 @@ impl RcloneUI {
|
|||||||
available_remote_types: Vec::new(),
|
available_remote_types: Vec::new(),
|
||||||
new_remote_config: std::collections::HashMap::new(),
|
new_remote_config: std::collections::HashMap::new(),
|
||||||
settings: AppSettings::default(),
|
settings: AppSettings::default(),
|
||||||
background_working: false,
|
active_task_count: 0,
|
||||||
operation_result: Arc::new(Mutex::new(OperationResult::None)),
|
operation_result: Arc::new(Mutex::new(OperationResult::None)),
|
||||||
operation_id: 0,
|
|
||||||
show_delete_remote_dialog: false,
|
show_delete_remote_dialog: false,
|
||||||
remote_to_delete: None,
|
remote_to_delete: None,
|
||||||
show_browser_warning: false,
|
show_browser_warning: false,
|
||||||
pending_remote_creation: None,
|
pending_remote_creation: None,
|
||||||
pending_load_path: None,
|
pending_load_path: None,
|
||||||
pending_search_pattern: None,
|
pending_search_pattern: None,
|
||||||
|
rclone_init_receiver: Some(rx),
|
||||||
|
active_operations: Vec::new(),
|
||||||
|
next_op_id: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_rclone(&mut self) {
|
fn poll_rclone_init(&mut self) {
|
||||||
let rt = Runtime::new().unwrap();
|
if let Some(receiver) = &self.rclone_init_receiver {
|
||||||
let rclone_result = rt.block_on(async { RcloneApp::new().await });
|
if let Ok(result) = receiver.try_recv() {
|
||||||
match rclone_result {
|
match result {
|
||||||
Ok(rclone) => {
|
Ok(rclone) => {
|
||||||
self.rclone = Some(Arc::new(rclone));
|
self.rclone = Some(rclone);
|
||||||
self.load_remote_types();
|
self.load_remote_types();
|
||||||
self.load_remotes();
|
self.load_remotes();
|
||||||
|
self.state = AppState::Ready;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = format!("Не удалось инициализировать rclone: {}", e);
|
let error_msg = format!("Не удалось инициализировать rclone: {}", e);
|
||||||
@@ -145,6 +174,9 @@ impl RcloneUI {
|
|||||||
self.state = AppState::Error(error_msg);
|
self.state = AppState::Error(error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.rclone_init_receiver = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_remote_types(&mut self) {
|
fn load_remote_types(&mut self) {
|
||||||
@@ -162,99 +194,136 @@ impl RcloneUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_remotes(&mut self) {
|
fn load_remotes(&mut self) {
|
||||||
if let Some(rclone) = &self.rclone {
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
match operations::remotes::list(rclone) {
|
return;
|
||||||
Ok(remotes) => {
|
|
||||||
self.remote_list = remotes;
|
|
||||||
self.state = AppState::Ready;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.error_message = Some(e.clone());
|
|
||||||
self.state = AppState::Error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
|
self.active_task_count += 1;
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let remotes = operations::remotes::list(&rclone);
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match remotes {
|
||||||
|
Ok(list) => OperationResult::RemoteList(list),
|
||||||
|
Err(e) => OperationResult::Failure(0, e),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_files(&mut self, path: &str) {
|
fn load_files(&mut self, path: &str) {
|
||||||
if let Some(rclone) = &self.rclone {
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
match operations::files::list(rclone, path) {
|
return;
|
||||||
Ok(files) => {
|
|
||||||
self.current_files = files;
|
|
||||||
self.current_path = path.to_string();
|
|
||||||
self.search_results.clear();
|
|
||||||
self.search_pattern.clear();
|
|
||||||
// Очищаем выбранные файлы при смене директории
|
|
||||||
self.selected_paths.clear();
|
|
||||||
}
|
|
||||||
Err(e) => self.error_message = Some(e),
|
|
||||||
}
|
}
|
||||||
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
|
self.active_task_count += 1;
|
||||||
|
let path = path.to_string();
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let files = operations::files::list(&rclone, &path);
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match files {
|
||||||
|
Ok(list) => OperationResult::FileList(list),
|
||||||
|
Err(e) => OperationResult::Failure(0, e),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_background_operation<F>(&mut self, f: F) -> u32
|
fn search_files(&mut self, pattern: &str) {
|
||||||
where
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
F: FnOnce() -> Result<String, String> + Send + 'static,
|
return;
|
||||||
{
|
}
|
||||||
self.background_working = true;
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
self.operation_id = self.operation_id.wrapping_add(1);
|
self.active_task_count += 1;
|
||||||
let op_id = self.operation_id;
|
let pattern = pattern.to_string();
|
||||||
|
let current_path = self.current_path.clone();
|
||||||
let result_holder = self.operation_result.clone();
|
let result_holder = self.operation_result.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = f();
|
let options = FindOptions {
|
||||||
|
recursive: true,
|
||||||
|
max_results: 100,
|
||||||
|
};
|
||||||
|
let results = operations::search::by_name(&rclone, ¤t_path, &pattern, &options);
|
||||||
let mut holder = result_holder.lock().unwrap();
|
let mut holder = result_holder.lock().unwrap();
|
||||||
*holder = match result {
|
*holder = match results {
|
||||||
Ok(msg) => OperationResult::Success(msg),
|
Ok(list) => OperationResult::SearchResults(list),
|
||||||
Err(e) => OperationResult::Failure(e),
|
Err(e) => OperationResult::Failure(0, e),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
op_id
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_background_operation(&mut self) {
|
fn poll_background_operation(&mut self) {
|
||||||
if !self.background_working {
|
if self.active_task_count == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let result = {
|
let result = {
|
||||||
let mut holder = self.operation_result.lock().unwrap();
|
let mut holder = self.operation_result.lock().unwrap();
|
||||||
match std::mem::replace(&mut *holder, OperationResult::None) {
|
std::mem::replace(&mut *holder, OperationResult::None)
|
||||||
OperationResult::Success(msg) => Some(Ok(msg)),
|
|
||||||
OperationResult::Failure(e) => Some(Err(e)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if let Some(res) = result {
|
|
||||||
self.background_working = false;
|
match result {
|
||||||
match res {
|
OperationResult::Success(op_id, msg) => {
|
||||||
Ok(msg) => {
|
println!("✅ Операция {} успешна: {}", op_id, msg);
|
||||||
println!("Операция успешна: {}", msg);
|
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||||
self.load_remotes();
|
self.active_operations.retain(|op| op.id != op_id);
|
||||||
let current_path = self.current_path.clone();
|
self.error_message = Some(msg);
|
||||||
if !current_path.is_empty() {
|
|
||||||
self.load_files(¤t_path);
|
|
||||||
}
|
}
|
||||||
self.error_message = None;
|
OperationResult::Failure(op_id, e) => {
|
||||||
}
|
eprintln!("❌ Операция {} ошибка: {}", op_id, e);
|
||||||
Err(e) => {
|
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||||
|
self.active_operations.retain(|op| op.id != op_id);
|
||||||
self.error_message = Some(e);
|
self.error_message = Some(e);
|
||||||
}
|
}
|
||||||
|
OperationResult::FileList(files) => {
|
||||||
|
self.current_files = files;
|
||||||
|
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
OperationResult::RemoteList(remotes) => {
|
||||||
|
self.remote_list = remotes;
|
||||||
|
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||||
|
}
|
||||||
|
OperationResult::SearchResults(results) => {
|
||||||
|
self.search_results = results;
|
||||||
|
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||||
|
}
|
||||||
|
OperationResult::ProgressUpdate(op_id, progress, status) => {
|
||||||
|
if let Some(op) = self.active_operations.iter_mut().find(|op| op.id == op_id) {
|
||||||
|
op.progress = progress;
|
||||||
|
op.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationResult::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_remote(&mut self, name: &str) {
|
fn delete_remote(&mut self, name: &str) {
|
||||||
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(rclone) = self.rclone.clone() {
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
let name = name.to_string();
|
let name = name.to_string();
|
||||||
self.start_background_operation(move || {
|
let op_id = self.add_operation(format!("Удаление хранилища '{}'", name));
|
||||||
operations::remotes::delete(&rclone, &name)
|
self.active_task_count += 1;
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = operations::remotes::delete(&rclone, &name)
|
||||||
.map(|_| format!("Хранилище '{}' удалено", name))
|
.map(|_| format!("Хранилище '{}' удалено", name))
|
||||||
.map_err(|e| format!("Ошибка удаления: {}", e))
|
.map_err(|e| format!("Ошибка удаления: {}", e));
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match result {
|
||||||
|
Ok(msg) => OperationResult::Success(op_id, msg),
|
||||||
|
Err(e) => OperationResult::Failure(op_id, e),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Массовое перемещение выбранных файлов/папок
|
|
||||||
fn perform_move_multiple(&mut self, sources: Vec<String>, dest: &str) {
|
fn perform_move_multiple(&mut self, sources: Vec<String>, dest: &str) {
|
||||||
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(rclone) = self.rclone.clone() {
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
self.state = AppState::Moving;
|
self.state = AppState::Moving;
|
||||||
let options = MoveOptions {
|
let options = MoveOptions {
|
||||||
@@ -263,14 +332,18 @@ impl RcloneUI {
|
|||||||
delete_empty_src_dirs: true,
|
delete_empty_src_dirs: true,
|
||||||
};
|
};
|
||||||
let dest = dest.to_string();
|
let dest = dest.to_string();
|
||||||
self.start_background_operation(move || {
|
let op_id = self.add_operation(format!("Перемещение {} элементов", sources.len()));
|
||||||
|
self.active_task_count += 1;
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
let _rt = Runtime::new().unwrap();
|
let _rt = Runtime::new().unwrap();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut moved_count = 0;
|
let mut moved_count = 0;
|
||||||
let mut total_bytes = 0;
|
let mut total_bytes = 0;
|
||||||
|
let total = sources.len() as f32;
|
||||||
|
|
||||||
for source in sources {
|
for (idx, source) in sources.iter().enumerate() {
|
||||||
match operations::sync::move_files(&rclone, &source, &dest, &options) {
|
match operations::sync::move_files(&rclone, source, &dest, &options) {
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
moved_count += 1;
|
moved_count += 1;
|
||||||
total_bytes += stats.transferred;
|
total_bytes += stats.transferred;
|
||||||
@@ -279,19 +352,31 @@ impl RcloneUI {
|
|||||||
errors.push(format!("{}: {}", source, e));
|
errors.push(format!("{}: {}", source, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let progress = (idx + 1) as f32 / total;
|
||||||
|
let _ = result_holder.lock().map(|mut holder| {
|
||||||
|
*holder = OperationResult::ProgressUpdate(op_id, progress, format!("Перемещено {} из {}", idx + 1, total));
|
||||||
|
});
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10)); // небольшая пауза для UI
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
let result = if errors.is_empty() {
|
||||||
Ok(format!("Перемещено {} элементов, {} байт", moved_count, total_bytes))
|
Ok(format!("Перемещено {} элементов, {} байт", moved_count, total_bytes))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Перемещено {}, ошибки:\n{}", moved_count, errors.join("\n")))
|
Err(format!("Перемещено {}, ошибки:\n{}", moved_count, errors.join("\n")))
|
||||||
}
|
};
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match result {
|
||||||
|
Ok(msg) => OperationResult::Success(op_id, msg),
|
||||||
|
Err(e) => OperationResult::Failure(op_id, e),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Массовое копирование выбранных файлов/папок
|
|
||||||
fn perform_copy_multiple(&mut self, sources: Vec<String>, dest: &str) {
|
fn perform_copy_multiple(&mut self, sources: Vec<String>, dest: &str) {
|
||||||
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(rclone) = self.rclone.clone() {
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
self.state = AppState::Copying;
|
self.state = AppState::Copying;
|
||||||
let options = CopyOptions {
|
let options = CopyOptions {
|
||||||
@@ -301,13 +386,17 @@ impl RcloneUI {
|
|||||||
no_traverse: false,
|
no_traverse: false,
|
||||||
};
|
};
|
||||||
let dest = dest.to_string();
|
let dest = dest.to_string();
|
||||||
self.start_background_operation(move || {
|
let op_id = self.add_operation(format!("Копирование {} элементов", sources.len()));
|
||||||
|
self.active_task_count += 1;
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
let _rt = Runtime::new().unwrap();
|
let _rt = Runtime::new().unwrap();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut copied_count = 0;
|
let mut copied_count = 0;
|
||||||
|
let total = sources.len() as f32;
|
||||||
|
|
||||||
for source in sources {
|
for (idx, source) in sources.iter().enumerate() {
|
||||||
match operations::sync::copy(&rclone, &source, &dest, &options) {
|
match operations::sync::copy(&rclone, source, &dest, &options) {
|
||||||
Ok(_stats) => {
|
Ok(_stats) => {
|
||||||
copied_count += 1;
|
copied_count += 1;
|
||||||
}
|
}
|
||||||
@@ -315,21 +404,32 @@ impl RcloneUI {
|
|||||||
errors.push(format!("{}: {}", source, e));
|
errors.push(format!("{}: {}", source, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let progress = (idx + 1) as f32 / total;
|
||||||
|
let _ = result_holder.lock().map(|mut holder| {
|
||||||
|
*holder = OperationResult::ProgressUpdate(op_id, progress, format!("Скопировано {} из {}", idx + 1, total));
|
||||||
|
});
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
let result = if errors.is_empty() {
|
||||||
Ok(format!("Скопировано {} элементов", copied_count))
|
Ok(format!("Скопировано {} элементов", copied_count))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Скопировано {}, ошибки:\n{}", copied_count, errors.join("\n")))
|
Err(format!("Скопировано {}, ошибки:\n{}", copied_count, errors.join("\n")))
|
||||||
}
|
};
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match result {
|
||||||
|
Ok(msg) => OperationResult::Success(op_id, msg),
|
||||||
|
Err(e) => OperationResult::Failure(op_id, e),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаление нескольких выбранных путей
|
|
||||||
fn delete_selected_paths(&mut self, paths: Vec<String>) {
|
fn delete_selected_paths(&mut self, paths: Vec<String>) {
|
||||||
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(rclone) = self.rclone.clone() {
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
// Создаём карту: путь -> является ли директорией
|
|
||||||
let mut is_dir_map = std::collections::HashMap::new();
|
let mut is_dir_map = std::collections::HashMap::new();
|
||||||
for file in &self.current_files {
|
for file in &self.current_files {
|
||||||
let full_path = if self.current_path.ends_with(':') {
|
let full_path = if self.current_path.ends_with(':') {
|
||||||
@@ -339,19 +439,22 @@ impl RcloneUI {
|
|||||||
};
|
};
|
||||||
is_dir_map.insert(full_path, file.is_dir);
|
is_dir_map.insert(full_path, file.is_dir);
|
||||||
}
|
}
|
||||||
|
let op_id = self.add_operation(format!("Удаление {} элементов", paths.len()));
|
||||||
self.start_background_operation(move || {
|
self.active_task_count += 1;
|
||||||
|
let result_holder = self.operation_result.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut deleted_count = 0;
|
let mut deleted_count = 0;
|
||||||
|
let total = paths.len() as f32;
|
||||||
|
|
||||||
for path in paths {
|
for (idx, path) in paths.iter().enumerate() {
|
||||||
let is_dir = is_dir_map.get(&path).cloned().unwrap_or(false);
|
let is_dir = is_dir_map.get(path).cloned().unwrap_or(false);
|
||||||
let options = DeleteOptions {
|
let options = DeleteOptions {
|
||||||
recursive: is_dir,
|
recursive: is_dir,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
match operations::sync::delete(&rclone, &path, &options) {
|
match operations::sync::delete(&rclone, path, &options) {
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
deleted_count += 1;
|
deleted_count += 1;
|
||||||
println!("Удалено: {} ({} элементов)", path, stats.files);
|
println!("Удалено: {} ({} элементов)", path, stats.files);
|
||||||
@@ -360,45 +463,69 @@ impl RcloneUI {
|
|||||||
errors.push(format!("{}: {}", path, e));
|
errors.push(format!("{}: {}", path, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let progress = (idx + 1) as f32 / total;
|
||||||
|
let _ = result_holder.lock().map(|mut holder| {
|
||||||
|
*holder = OperationResult::ProgressUpdate(op_id, progress, format!("Удалено {} из {}", idx + 1, total));
|
||||||
|
});
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
let result = if errors.is_empty() {
|
||||||
Ok(format!("Успешно удалено {} элементов", deleted_count))
|
Ok(format!("Успешно удалено {} элементов", deleted_count))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Удалено {} элементов, но возникли ошибки:\n{}", deleted_count, errors.join("\n")))
|
Err(format!("Удалено {} элементов, но возникли ошибки:\n{}", deleted_count, errors.join("\n")))
|
||||||
}
|
};
|
||||||
|
let mut holder = result_holder.lock().unwrap();
|
||||||
|
*holder = match result {
|
||||||
|
Ok(msg) => OperationResult::Success(op_id, msg),
|
||||||
|
Err(e) => OperationResult::Failure(op_id, e),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_operation(&mut self, description: String) -> u32 {
|
||||||
|
let id = self.next_op_id;
|
||||||
|
self.next_op_id += 1;
|
||||||
|
self.active_operations.push(Operation {
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
progress: 0.0,
|
||||||
|
status: "Начато".to_string(),
|
||||||
|
start_time: Instant::now(),
|
||||||
|
});
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
fn create_remote_with_warning(&mut self, name: &str, r#type: &str) {
|
fn create_remote_with_warning(&mut self, name: &str, r#type: &str) {
|
||||||
self.pending_remote_creation = Some((name.to_string(), r#type.to_string()));
|
self.pending_remote_creation = Some((name.to_string(), r#type.to_string()));
|
||||||
self.show_browser_warning = true;
|
self.show_browser_warning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_remote_creation(&mut self, name: &str, r#type: &str) {
|
fn execute_remote_creation(&mut self, name: &str, r#type: &str) {
|
||||||
|
if self.active_task_count > 0 || self.rclone.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(rclone) = self.rclone.clone() {
|
if let Some(rclone) = self.rclone.clone() {
|
||||||
let name = name.to_string();
|
let name = name.to_string();
|
||||||
let r#type = r#type.to_string();
|
let r#type = r#type.to_string();
|
||||||
let config = std::collections::HashMap::new();
|
let config = std::collections::HashMap::new();
|
||||||
self.start_background_operation(move || {
|
let op_id = self.add_operation(format!("Создание хранилища '{}'", name));
|
||||||
operations::remotes::create(&rclone, &name, &r#type, &config)
|
self.active_task_count += 1;
|
||||||
.map(|_| format!("Хранилище '{}' создано", name))
|
let result_holder = self.operation_result.clone();
|
||||||
.map_err(|e| format!("Ошибка создания: {}", e))
|
std::thread::spawn(move || {
|
||||||
|
let _ = result_holder.lock().map(|mut holder| {
|
||||||
|
*holder = OperationResult::ProgressUpdate(op_id, 0.5, "Открытие браузера для авторизации...".to_string());
|
||||||
});
|
});
|
||||||
}
|
let result = operations::remotes::create(&rclone, &name, &r#type, &config)
|
||||||
}
|
.map(|_| format!("Хранилище '{}' создано", name))
|
||||||
|
.map_err(|e| format!("Ошибка создания: {}", e));
|
||||||
fn search_files(&mut self, pattern: &str) {
|
let mut holder = result_holder.lock().unwrap();
|
||||||
if let Some(rclone) = &self.rclone {
|
*holder = match result {
|
||||||
let options = FindOptions {
|
Ok(msg) => OperationResult::Success(op_id, msg),
|
||||||
recursive: true,
|
Err(e) => OperationResult::Failure(op_id, e),
|
||||||
max_results: 100,
|
|
||||||
};
|
};
|
||||||
match operations::search::by_name(rclone, &self.current_path, pattern, &options) {
|
});
|
||||||
Ok(results) => self.search_results = results,
|
|
||||||
Err(e) => self.error_message = Some(e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,33 +540,43 @@ impl RcloneUI {
|
|||||||
const KB: u64 = 1024;
|
const KB: u64 = 1024;
|
||||||
const MB: u64 = KB * 1024;
|
const MB: u64 = KB * 1024;
|
||||||
const GB: u64 = MB * 1024;
|
const GB: u64 = MB * 1024;
|
||||||
if bytes >= GB { format!("{:.2} ГБ", bytes as f64 / GB as f64) }
|
if bytes >= GB {
|
||||||
else if bytes >= MB { format!("{:.2} МБ", bytes as f64 / MB as f64) }
|
format!("{:.2} ГБ", bytes as f64 / GB as f64)
|
||||||
else if bytes >= KB { format!("{:.2} КБ", bytes as f64 / KB as f64) }
|
} else if bytes >= MB {
|
||||||
else { format!("{} Б", bytes) }
|
format!("{:.2} МБ", bytes as f64 / MB as f64)
|
||||||
|
} else if bytes >= KB {
|
||||||
|
format!("{:.2} КБ", bytes as f64 / KB as f64)
|
||||||
|
} else {
|
||||||
|
format!("{} Б", bytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn format_speed(speed: f64) -> String {
|
fn format_speed(speed: f64) -> String {
|
||||||
if speed < 1024.0 { format!("{:.0} Б/с", speed) }
|
if speed < 1024.0 {
|
||||||
else if speed < 1024.0 * 1024.0 { format!("{:.1} КБ/с", speed / 1024.0) }
|
format!("{:.0} Б/с", speed)
|
||||||
else if speed < 1024.0 * 1024.0 * 1024.0 { format!("{:.1} МБ/с", speed / (1024.0 * 1024.0)) }
|
} else if speed < 1024.0 * 1024.0 {
|
||||||
else { format!("{:.2} ГБ/с", speed / (1024.0 * 1024.0 * 1024.0)) }
|
format!("{:.1} КБ/с", speed / 1024.0)
|
||||||
|
} else if speed < 1024.0 * 1024.0 * 1024.0 {
|
||||||
|
format!("{:.1} МБ/с", speed / (1024.0 * 1024.0))
|
||||||
|
} else {
|
||||||
|
format!("{:.2} ГБ/с", speed / (1024.0 * 1024.0 * 1024.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for RcloneUI {
|
impl eframe::App for RcloneUI {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
self.poll_rclone_init();
|
||||||
self.poll_background_operation();
|
self.poll_background_operation();
|
||||||
|
|
||||||
if matches!(self.state, AppState::Initializing) && self.rclone.is_none() {
|
|
||||||
self.init_rclone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Верхняя панель меню
|
// Верхняя панель меню
|
||||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |ui| {
|
egui::menu::bar(ui, |ui| {
|
||||||
ui.menu_button("Файл", |ui| {
|
ui.menu_button("Файл", |ui| {
|
||||||
if ui.button("Выход").clicked() { std::process::exit(0); }
|
if ui.button("Выход").clicked() {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ui.menu_button("Хранилища", |ui| {
|
ui.menu_button("Хранилища", |ui| {
|
||||||
if ui.button("Добавить новое хранилище").clicked() {
|
if ui.button("Добавить новое хранилище").clicked() {
|
||||||
@@ -462,20 +599,27 @@ impl eframe::App for RcloneUI {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
// Индикатор состояния с анимацией
|
if self.active_task_count > 0 {
|
||||||
if self.background_working {
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(Spinner::new().size(16.0));
|
ui.add(Spinner::new().size(16.0));
|
||||||
ui.colored_label(egui::Color32::YELLOW, " Работа в фоне...");
|
ui.colored_label(egui::Color32::YELLOW, format!(" Работа в фоне... ({} задач)", self.active_task_count));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
AppState::Ready => { ui.colored_label(Color32::GREEN, "● Готов"); }
|
AppState::Ready => {
|
||||||
AppState::Initializing => { ui.colored_label(Color32::YELLOW, "● Инициализация..."); }
|
ui.colored_label(Color32::GREEN, "● Готов");
|
||||||
AppState::Error(_) => { ui.colored_label(Color32::RED, "● Ошибка"); }
|
}
|
||||||
_ => { ui.colored_label(Color32::BLUE, "● Занят"); }
|
AppState::Initializing => {
|
||||||
|
ui.colored_label(Color32::YELLOW, "● Инициализация...");
|
||||||
|
}
|
||||||
|
AppState::Error(_) => {
|
||||||
|
ui.colored_label(Color32::RED, "● Ошибка");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ui.colored_label(Color32::BLUE, "● Занят");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if let Some(rclone) = &self.rclone {
|
if let Some(rclone) = &self.rclone {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(format!("rclone: {}", rclone.get_rclone_path().display()));
|
ui.label(format!("rclone: {}", rclone.get_rclone_path().display()));
|
||||||
@@ -484,7 +628,7 @@ impl eframe::App for RcloneUI {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Левая панель со списком хранилищ
|
// Левая панель со списком хранилищ
|
||||||
egui::SidePanel::left("remotes_panel")
|
SidePanel::left("remotes_panel")
|
||||||
.default_width(220.0)
|
.default_width(220.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.heading("Хранилища");
|
ui.heading("Хранилища");
|
||||||
@@ -499,7 +643,8 @@ impl eframe::App for RcloneUI {
|
|||||||
} else {
|
} else {
|
||||||
for remote in &remotes {
|
for remote in &remotes {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button(format!("📡 {}", remote.name)).clicked() {
|
let btn = ui.button(format!("📡 {}", remote.name));
|
||||||
|
if btn.clicked() && self.active_task_count == 0 {
|
||||||
self.pending_load_path = Some(format!("{}:", remote.name));
|
self.pending_load_path = Some(format!("{}:", remote.name));
|
||||||
}
|
}
|
||||||
if ui.button("🗑️").on_hover_text("Удалить хранилище").clicked() {
|
if ui.button("🗑️").on_hover_text("Удалить хранилище").clicked() {
|
||||||
@@ -513,6 +658,29 @@ impl eframe::App for RcloneUI {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Правая панель с операциями
|
||||||
|
SidePanel::right("operations_panel")
|
||||||
|
.default_width(280.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading("Операции");
|
||||||
|
ui.separator();
|
||||||
|
if self.active_operations.is_empty() {
|
||||||
|
ui.colored_label(Color32::GRAY, "Нет активных операций");
|
||||||
|
} else {
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
for op in &self.active_operations {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.label(&op.description);
|
||||||
|
ui.add(ProgressBar::new(op.progress).text(format!("{:.0}%", op.progress * 100.0)));
|
||||||
|
ui.colored_label(Color32::LIGHT_BLUE, &op.status);
|
||||||
|
ui.label(format!("Время: {:.0} сек", op.start_time.elapsed().as_secs()));
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Центральная панель
|
// Центральная панель
|
||||||
CentralPanel::default().show(ctx, |ui| {
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
let current_path = self.current_path.clone();
|
let current_path = self.current_path.clone();
|
||||||
@@ -525,29 +693,25 @@ impl eframe::App for RcloneUI {
|
|||||||
ui.label("🔍 Поиск:");
|
ui.label("🔍 Поиск:");
|
||||||
if ui.text_edit_singleline(&mut new_search).changed() {
|
if ui.text_edit_singleline(&mut new_search).changed() {
|
||||||
self.search_pattern = new_search.clone();
|
self.search_pattern = new_search.clone();
|
||||||
if !self.search_pattern.is_empty() && !self.current_path.is_empty() {
|
if !self.search_pattern.is_empty() && !self.current_path.is_empty() && self.active_task_count == 0 {
|
||||||
self.pending_search_pattern = Some(self.search_pattern.clone());
|
self.pending_search_pattern = Some(self.search_pattern.clone());
|
||||||
} else {
|
} else {
|
||||||
self.search_results.clear();
|
self.search_results.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui.button("🔄 Обновить").clicked() && !self.current_path.is_empty() {
|
if ui.button("🔄 Обновить").clicked() && !self.current_path.is_empty() && self.active_task_count == 0 {
|
||||||
self.pending_load_path = Some(self.current_path.clone());
|
self.pending_load_path = Some(self.current_path.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// Анимированная загрузка в центральной панели
|
if self.active_task_count > 0 {
|
||||||
if self.background_working {
|
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
ui.add(Spinner::new().size(64.0));
|
ui.add(Spinner::new().size(64.0));
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
ui.colored_label(
|
ui.colored_label(egui::Color32::LIGHT_BLUE, "⟳ Выполняется операция... Пожалуйста, подождите.");
|
||||||
egui::Color32::LIGHT_BLUE,
|
|
||||||
"⟳ Выполняется операция... Пожалуйста, подождите."
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
ctx.request_repaint_after(Duration::from_millis(16));
|
ctx.request_repaint_after(Duration::from_millis(50));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +730,7 @@ impl eframe::App for RcloneUI {
|
|||||||
for file in &search_results {
|
for file in &search_results {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(file.icon());
|
ui.label(file.icon());
|
||||||
if ui.link(&file.name).clicked() && file.is_dir {
|
if ui.link(&file.name).clicked() && file.is_dir && self.active_task_count == 0 {
|
||||||
let new_path = if current_path.ends_with(':') {
|
let new_path = if current_path.ends_with(':') {
|
||||||
format!("{}{}", current_path, file.name)
|
format!("{}{}", current_path, file.name)
|
||||||
} else {
|
} else {
|
||||||
@@ -582,13 +746,12 @@ impl eframe::App for RcloneUI {
|
|||||||
let current_files = self.current_files.clone();
|
let current_files = self.current_files.clone();
|
||||||
let current_path = self.current_path.clone();
|
let current_path = self.current_path.clone();
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() {
|
if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() && self.active_task_count == 0 {
|
||||||
let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or("");
|
let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or("");
|
||||||
self.pending_load_path = Some(parent.to_string());
|
self.pending_load_path = Some(parent.to_string());
|
||||||
}
|
}
|
||||||
ui.heading("Файлы и папки");
|
ui.heading("Файлы и папки");
|
||||||
for file in ¤t_files {
|
for file in ¤t_files {
|
||||||
// Формируем полный путь rclone (с именем удалённого хранилища)
|
|
||||||
let full_path = if current_path.ends_with(':') {
|
let full_path = if current_path.ends_with(':') {
|
||||||
format!("{}{}", current_path, file.name)
|
format!("{}{}", current_path, file.name)
|
||||||
} else {
|
} else {
|
||||||
@@ -596,7 +759,7 @@ impl eframe::App for RcloneUI {
|
|||||||
};
|
};
|
||||||
let selected = self.selected_paths.contains(&full_path);
|
let selected = self.selected_paths.contains(&full_path);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.selectable_label(selected, file.icon()).clicked() {
|
if ui.selectable_label(selected, file.icon()).clicked() && self.active_task_count == 0 {
|
||||||
if selected {
|
if selected {
|
||||||
self.selected_paths.retain(|p| p != &full_path);
|
self.selected_paths.retain(|p| p != &full_path);
|
||||||
} else {
|
} else {
|
||||||
@@ -604,7 +767,7 @@ impl eframe::App for RcloneUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if file.is_dir {
|
if file.is_dir {
|
||||||
if ui.link(&file.name).clicked() {
|
if ui.link(&file.name).clicked() && self.active_task_count == 0 {
|
||||||
let new_path = if current_path.ends_with(':') {
|
let new_path = if current_path.ends_with(':') {
|
||||||
format!("{}{}", current_path, file.name)
|
format!("{}{}", current_path, file.name)
|
||||||
} else {
|
} else {
|
||||||
@@ -630,7 +793,7 @@ impl eframe::App for RcloneUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if !self.selected_paths.is_empty() {
|
if !self.selected_paths.is_empty() && self.active_task_count == 0 {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(format!("Выбрано: {}", self.selected_paths.len()));
|
ui.label(format!("Выбрано: {}", self.selected_paths.len()));
|
||||||
if ui.button("📋 Копировать").clicked() {
|
if ui.button("📋 Копировать").clicked() {
|
||||||
@@ -652,13 +815,17 @@ impl eframe::App for RcloneUI {
|
|||||||
|
|
||||||
// Обработка отложенных действий
|
// Обработка отложенных действий
|
||||||
if let Some(path) = self.pending_load_path.take() {
|
if let Some(path) = self.pending_load_path.take() {
|
||||||
|
self.current_path = path.clone();
|
||||||
|
self.search_results.clear();
|
||||||
|
self.search_pattern.clear();
|
||||||
|
self.selected_paths.clear();
|
||||||
self.load_files(&path);
|
self.load_files(&path);
|
||||||
}
|
}
|
||||||
if let Some(pattern) = self.pending_search_pattern.take() {
|
if let Some(pattern) = self.pending_search_pattern.take() {
|
||||||
self.search_files(&pattern);
|
self.search_files(&pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Диалог передачи файлов (поддерживает несколько источников)
|
// Диалог передачи файлов (копирование/перемещение)
|
||||||
if self.show_transfer_dialog {
|
if self.show_transfer_dialog {
|
||||||
let source_count = self.transfer_source_list.len();
|
let source_count = self.transfer_source_list.len();
|
||||||
let dest = self.transfer_dest.clone();
|
let dest = self.transfer_dest.clone();
|
||||||
@@ -713,13 +880,19 @@ impl eframe::App for RcloneUI {
|
|||||||
let mut name = self.new_remote_name.clone();
|
let mut name = self.new_remote_name.clone();
|
||||||
let mut typ = self.new_remote_type.clone();
|
let mut typ = self.new_remote_type.clone();
|
||||||
let available_types = self.available_remote_types.clone();
|
let available_types = self.available_remote_types.clone();
|
||||||
Window::new("Добавление хранилища").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
|
Window::new("Добавление хранилища")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||||
|
.show(ctx, |ui| {
|
||||||
ui.label("Имя хранилища:");
|
ui.label("Имя хранилища:");
|
||||||
if ui.text_edit_singleline(&mut name).changed() {
|
if ui.text_edit_singleline(&mut name).changed() {
|
||||||
self.new_remote_name = name;
|
self.new_remote_name = name;
|
||||||
}
|
}
|
||||||
ui.label("Тип хранилища:");
|
ui.label("Тип хранилища:");
|
||||||
egui::ComboBox::from_label("").selected_text(&typ).show_ui(ui, |ui| {
|
egui::ComboBox::from_label("")
|
||||||
|
.selected_text(&typ)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
for t in &available_types {
|
for t in &available_types {
|
||||||
if ui.selectable_value(&mut typ, t.clone(), t).clicked() {
|
if ui.selectable_value(&mut typ, t.clone(), t).clicked() {
|
||||||
self.new_remote_type = typ.clone();
|
self.new_remote_type = typ.clone();
|
||||||
@@ -751,7 +924,11 @@ impl eframe::App for RcloneUI {
|
|||||||
|
|
||||||
// Диалог предупреждения о браузере
|
// Диалог предупреждения о браузере
|
||||||
if self.show_browser_warning {
|
if self.show_browser_warning {
|
||||||
Window::new("Внимание").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
|
Window::new("Внимание")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||||
|
.show(ctx, |ui| {
|
||||||
ui.colored_label(Color32::YELLOW, "🌐 Будет открыт браузер для авторизации");
|
ui.colored_label(Color32::YELLOW, "🌐 Будет открыт браузер для авторизации");
|
||||||
ui.label("После завершения авторизации в браузере вернитесь в приложение.");
|
ui.label("После завершения авторизации в браузере вернитесь в приложение.");
|
||||||
ui.label("Операция выполняется в фоне, интерфейс не зависнет.");
|
ui.label("Операция выполняется в фоне, интерфейс не зависнет.");
|
||||||
@@ -772,7 +949,11 @@ impl eframe::App for RcloneUI {
|
|||||||
// Диалог подтверждения удаления хранилища
|
// Диалог подтверждения удаления хранилища
|
||||||
if self.show_delete_remote_dialog {
|
if self.show_delete_remote_dialog {
|
||||||
let remote_name = self.remote_to_delete.clone();
|
let remote_name = self.remote_to_delete.clone();
|
||||||
Window::new("Удаление хранилища").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
|
Window::new("Удаление хранилища")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||||
|
.show(ctx, |ui| {
|
||||||
if let Some(ref name) = remote_name {
|
if let Some(ref name) = remote_name {
|
||||||
ui.label(format!("Удалить хранилище '{}'?", name));
|
ui.label(format!("Удалить хранилище '{}'?", name));
|
||||||
ui.label("Это действие не удаляет данные, только конфигурацию.");
|
ui.label("Это действие не удаляет данные, только конфигурацию.");
|
||||||
@@ -794,11 +975,19 @@ impl eframe::App for RcloneUI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Диалог ошибок
|
// Диалог ошибок / уведомлений
|
||||||
if let Some(error) = &self.error_message {
|
if let Some(error) = &self.error_message {
|
||||||
let error_clone = error.clone();
|
let error_clone = error.clone();
|
||||||
Window::new("Ошибка").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
|
Window::new("Сообщение")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
if error_clone.starts_with("✅") || error_clone.contains("успеш") || error_clone.contains("скопировано") {
|
||||||
|
ui.colored_label(Color32::GREEN, "✅ Успешно:");
|
||||||
|
} else {
|
||||||
ui.colored_label(Color32::RED, "⚠️ Произошла ошибка:");
|
ui.colored_label(Color32::RED, "⚠️ Произошла ошибка:");
|
||||||
|
}
|
||||||
ui.label(&error_clone);
|
ui.label(&error_clone);
|
||||||
if ui.button("OK").clicked() {
|
if ui.button("OK").clicked() {
|
||||||
self.error_message = None;
|
self.error_message = None;
|
||||||
@@ -809,6 +998,6 @@ impl eframe::App for RcloneUI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.request_repaint();
|
ctx.request_repaint_after(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user