feat(ui): Added paralell instructions and window with operations progress

This commit is contained in:
Black_Raven committed 2026-03-29 16:11:50 +03:00
1 parent 04a0dc21b9
commit 5878a6ec4a
4 files changed
+346 -154

No files matched your search

Generated
+1
View File
@@ -15,6 +15,7 @@ dependencies = [
"mockito",
"reqwest",
"rfd",
"serde",
"serde_json",
"serial_test",
"tar",
+1
View File
@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json", "stream"] }
serde = { version = "1.0", features = ["derive"] }
futures = "0.3"
flate2 = "1.0"
tar = "0.4"
+3 -2
View File
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
/// === Модуль для работы с удаленными хранилищами ===
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Remote {
pub name: String,
pub r#type: String,
@@ -13,7 +14,7 @@ pub mod remotes;
/// === Модуль для работы с файлами и директориями ===
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub name: String,
pub path: String,
+341 -152
View File
@@ -1,9 +1,9 @@
//! Графический интерфейс для операций rclone.
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::time::Duration;
use std::time::{Duration, Instant};
use tokio::runtime::Runtime;
use crate::rclone_install::RcloneApp;
use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions};
@@ -30,11 +30,25 @@ pub struct TransferProgress {
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 {
None,
Success(String),
Failure(String),
Success(u32, String), // (operation_id, message)
Failure(u32, String), // (operation_id, error)
FileList(Vec<FileInfo>),
RemoteList(Vec<Remote>),
SearchResults(Vec<FileInfo>),
ProgressUpdate(u32, f32, String),
}
/// Главная структура UI.
@@ -48,7 +62,7 @@ pub struct RcloneUI {
current_files: Vec<FileInfo>,
selected_paths: Vec<String>,
transfer_source_list: Vec<String>, // список выбранных источников для копирования/перемещения
transfer_source_list: Vec<String>,
transfer_dest: String,
transfer_progress: Option<TransferProgress>,
@@ -64,17 +78,18 @@ pub struct RcloneUI {
settings: AppSettings,
background_working: bool,
active_task_count: u32,
operation_result: Arc<Mutex<OperationResult>>,
operation_id: u32,
show_delete_remote_dialog: bool,
remote_to_delete: Option<String>,
show_browser_warning: bool,
pending_remote_creation: Option<(String, String)>,
// Отложенные действия
pending_load_path: 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)]
@@ -98,6 +113,17 @@ impl Default for AppSettings {
impl RcloneUI {
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 {
rclone: None,
state: AppState::Initializing,
@@ -118,26 +144,29 @@ impl RcloneUI {
available_remote_types: Vec::new(),
new_remote_config: std::collections::HashMap::new(),
settings: AppSettings::default(),
background_working: false,
active_task_count: 0,
operation_result: Arc::new(Mutex::new(OperationResult::None)),
operation_id: 0,
show_delete_remote_dialog: false,
remote_to_delete: None,
show_browser_warning: false,
pending_remote_creation: None,
pending_load_path: None,
pending_search_pattern: None,
rclone_init_receiver: Some(rx),
active_operations: Vec::new(),
next_op_id: 1,
}
}
fn init_rclone(&mut self) {
let rt = Runtime::new().unwrap();
let rclone_result = rt.block_on(async { RcloneApp::new().await });
match rclone_result {
fn poll_rclone_init(&mut self) {
if let Some(receiver) = &self.rclone_init_receiver {
if let Ok(result) = receiver.try_recv() {
match result {
Ok(rclone) => {
self.rclone = Some(Arc::new(rclone));
self.rclone = Some(rclone);
self.load_remote_types();
self.load_remotes();
self.state = AppState::Ready;
}
Err(e) => {
let error_msg = format!("Не удалось инициализировать rclone: {}", e);
@@ -145,6 +174,9 @@ impl RcloneUI {
self.state = AppState::Error(error_msg);
}
}
self.rclone_init_receiver = None;
}
}
}
fn load_remote_types(&mut self) {
@@ -162,99 +194,136 @@ impl RcloneUI {
}
fn load_remotes(&mut self) {
if let Some(rclone) = &self.rclone {
match operations::remotes::list(rclone) {
Ok(remotes) => {
self.remote_list = remotes;
self.state = AppState::Ready;
}
Err(e) => {
self.error_message = Some(e.clone());
self.state = AppState::Error(e);
}
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
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) {
if let Some(rclone) = &self.rclone {
match operations::files::list(rclone, path) {
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 self.active_task_count > 0 || self.rclone.is_none() {
return;
}
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
where
F: FnOnce() -> Result<String, String> + Send + 'static,
{
self.background_working = true;
self.operation_id = self.operation_id.wrapping_add(1);
let op_id = self.operation_id;
fn search_files(&mut self, pattern: &str) {
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
if let Some(rclone) = self.rclone.clone() {
self.active_task_count += 1;
let pattern = pattern.to_string();
let current_path = self.current_path.clone();
let result_holder = self.operation_result.clone();
std::thread::spawn(move || {
let result = f();
let options = FindOptions {
recursive: true,
max_results: 100,
};
let results = operations::search::by_name(&rclone, &current_path, &pattern, &options);
let mut holder = result_holder.lock().unwrap();
*holder = match result {
Ok(msg) => OperationResult::Success(msg),
Err(e) => OperationResult::Failure(e),
*holder = match results {
Ok(list) => OperationResult::SearchResults(list),
Err(e) => OperationResult::Failure(0, e),
};
});
op_id
}
}
fn poll_background_operation(&mut self) {
if !self.background_working {
if self.active_task_count == 0 {
return;
}
let result = {
let mut holder = self.operation_result.lock().unwrap();
match std::mem::replace(&mut *holder, OperationResult::None) {
OperationResult::Success(msg) => Some(Ok(msg)),
OperationResult::Failure(e) => Some(Err(e)),
_ => None,
}
std::mem::replace(&mut *holder, OperationResult::None)
};
if let Some(res) = result {
self.background_working = false;
match res {
Ok(msg) => {
println!("Операция успешна: {}", msg);
self.load_remotes();
let current_path = self.current_path.clone();
if !current_path.is_empty() {
self.load_files(&current_path);
match result {
OperationResult::Success(op_id, msg) => {
println!("✅ Операция {} успешна: {}", op_id, msg);
self.active_task_count = self.active_task_count.saturating_sub(1);
self.active_operations.retain(|op| op.id != op_id);
self.error_message = Some(msg);
}
self.error_message = None;
}
Err(e) => {
OperationResult::Failure(op_id, e) => {
eprintln!("❌ Операция {} ошибка: {}", op_id, 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);
}
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) {
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
if let Some(rclone) = self.rclone.clone() {
let name = name.to_string();
self.start_background_operation(move || {
operations::remotes::delete(&rclone, &name)
let op_id = self.add_operation(format!("Удаление хранилища '{}'", 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_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) {
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
if let Some(rclone) = self.rclone.clone() {
self.state = AppState::Moving;
let options = MoveOptions {
@@ -263,14 +332,18 @@ impl RcloneUI {
delete_empty_src_dirs: true,
};
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 mut errors = Vec::new();
let mut moved_count = 0;
let mut total_bytes = 0;
let total = sources.len() as f32;
for source in sources {
match operations::sync::move_files(&rclone, &source, &dest, &options) {
for (idx, source) in sources.iter().enumerate() {
match operations::sync::move_files(&rclone, source, &dest, &options) {
Ok(stats) => {
moved_count += 1;
total_bytes += stats.transferred;
@@ -279,19 +352,31 @@ impl RcloneUI {
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))
} else {
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) {
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
if let Some(rclone) = self.rclone.clone() {
self.state = AppState::Copying;
let options = CopyOptions {
@@ -301,13 +386,17 @@ impl RcloneUI {
no_traverse: false,
};
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 mut errors = Vec::new();
let mut copied_count = 0;
let total = sources.len() as f32;
for source in sources {
match operations::sync::copy(&rclone, &source, &dest, &options) {
for (idx, source) in sources.iter().enumerate() {
match operations::sync::copy(&rclone, source, &dest, &options) {
Ok(_stats) => {
copied_count += 1;
}
@@ -315,21 +404,32 @@ impl RcloneUI {
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))
} else {
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>) {
if self.active_task_count > 0 || self.rclone.is_none() {
return;
}
if let Some(rclone) = self.rclone.clone() {
// Создаём карту: путь -> является ли директорией
let mut is_dir_map = std::collections::HashMap::new();
for file in &self.current_files {
let full_path = if self.current_path.ends_with(':') {
@@ -339,19 +439,22 @@ impl RcloneUI {
};
is_dir_map.insert(full_path, file.is_dir);
}
self.start_background_operation(move || {
let op_id = self.add_operation(format!("Удаление {} элементов", paths.len()));
self.active_task_count += 1;
let result_holder = self.operation_result.clone();
std::thread::spawn(move || {
let mut errors = Vec::new();
let mut deleted_count = 0;
let total = paths.len() as f32;
for path in paths {
let is_dir = is_dir_map.get(&path).cloned().unwrap_or(false);
for (idx, path) in paths.iter().enumerate() {
let is_dir = is_dir_map.get(path).cloned().unwrap_or(false);
let options = DeleteOptions {
recursive: is_dir,
verbose: false,
dry_run: false,
};
match operations::sync::delete(&rclone, &path, &options) {
match operations::sync::delete(&rclone, path, &options) {
Ok(stats) => {
deleted_count += 1;
println!("Удалено: {} ({} элементов)", path, stats.files);
@@ -360,45 +463,69 @@ impl RcloneUI {
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))
} else {
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) {
self.pending_remote_creation = Some((name.to_string(), r#type.to_string()));
self.show_browser_warning = true;
}
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() {
let name = name.to_string();
let r#type = r#type.to_string();
let config = std::collections::HashMap::new();
self.start_background_operation(move || {
operations::remotes::create(&rclone, &name, &r#type, &config)
.map(|_| format!("Хранилище '{}' создано", name))
.map_err(|e| format!("Ошибка создания: {}", e))
let op_id = self.add_operation(format!("Создание хранилища '{}'", name));
self.active_task_count += 1;
let result_holder = self.operation_result.clone();
std::thread::spawn(move || {
let _ = result_holder.lock().map(|mut holder| {
*holder = OperationResult::ProgressUpdate(op_id, 0.5, "Открытие браузера для авторизации...".to_string());
});
}
}
fn search_files(&mut self, pattern: &str) {
if let Some(rclone) = &self.rclone {
let options = FindOptions {
recursive: true,
max_results: 100,
let result = operations::remotes::create(&rclone, &name, &r#type, &config)
.map(|_| format!("Хранилище '{}' создано", name))
.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),
};
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 MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB { format!("{:.2} ГБ", bytes as f64 / GB as f64) }
else if bytes >= MB { format!("{:.2} МБ", bytes as f64 / MB as f64) }
else if bytes >= KB { format!("{:.2} КБ", bytes as f64 / KB as f64) }
else { format!("{} Б", bytes) }
if bytes >= GB {
format!("{:.2} ГБ", bytes as f64 / GB as f64)
} else if bytes >= MB {
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 {
if speed < 1024.0 { format!("{:.0} Б/с", speed) }
else if speed < 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)) }
if speed < 1024.0 {
format!("{:.0} Б/с", speed)
} else if speed < 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 {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.poll_rclone_init();
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::menu::bar(ui, |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| {
if ui.button("Добавить новое хранилище").clicked() {
@@ -462,20 +599,27 @@ impl eframe::App for RcloneUI {
});
});
ui.separator();
// Индикатор состояния с анимацией
if self.background_working {
if self.active_task_count > 0 {
ui.horizontal(|ui| {
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 {
match &self.state {
AppState::Ready => { ui.colored_label(Color32::GREEN, "● Готов"); }
AppState::Initializing => { ui.colored_label(Color32::YELLOW, "● Инициализация..."); }
AppState::Error(_) => { ui.colored_label(Color32::RED, "● Ошибка"); }
_ => { ui.colored_label(Color32::BLUE, "● Занят"); }
AppState::Ready => {
ui.colored_label(Color32::GREEN, "● Готов");
}
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 {
ui.separator();
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)
.show(ctx, |ui| {
ui.heading("Хранилища");
@@ -499,7 +643,8 @@ impl eframe::App for RcloneUI {
} else {
for remote in &remotes {
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));
}
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| {
let current_path = self.current_path.clone();
@@ -525,29 +693,25 @@ impl eframe::App for RcloneUI {
ui.label("🔍 Поиск:");
if ui.text_edit_singleline(&mut new_search).changed() {
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());
} else {
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());
}
});
ui.separator();
// Анимированная загрузка в центральной панели
if self.background_working {
if self.active_task_count > 0 {
ui.centered_and_justified(|ui| {
ui.add(Spinner::new().size(64.0));
ui.add_space(10.0);
ui.colored_label(
egui::Color32::LIGHT_BLUE,
"⟳ Выполняется операция... Пожалуйста, подождите."
);
ui.colored_label(egui::Color32::LIGHT_BLUE, "⟳ Выполняется операция... Пожалуйста, подождите.");
});
ctx.request_repaint_after(Duration::from_millis(16));
ctx.request_repaint_after(Duration::from_millis(50));
return;
}
@@ -566,7 +730,7 @@ impl eframe::App for RcloneUI {
for file in &search_results {
ui.horizontal(|ui| {
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(':') {
format!("{}{}", current_path, file.name)
} else {
@@ -582,13 +746,12 @@ impl eframe::App for RcloneUI {
let current_files = self.current_files.clone();
let current_path = self.current_path.clone();
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("");
self.pending_load_path = Some(parent.to_string());
}
ui.heading("Файлы и папки");
for file in &current_files {
// Формируем полный путь rclone (с именем удалённого хранилища)
let full_path = if current_path.ends_with(':') {
format!("{}{}", current_path, file.name)
} else {
@@ -596,7 +759,7 @@ impl eframe::App for RcloneUI {
};
let selected = self.selected_paths.contains(&full_path);
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 {
self.selected_paths.retain(|p| p != &full_path);
} else {
@@ -604,7 +767,7 @@ impl eframe::App for RcloneUI {
}
}
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(':') {
format!("{}{}", current_path, file.name)
} else {
@@ -630,7 +793,7 @@ impl eframe::App for RcloneUI {
}
ui.separator();
if !self.selected_paths.is_empty() {
if !self.selected_paths.is_empty() && self.active_task_count == 0 {
ui.horizontal(|ui| {
ui.label(format!("Выбрано: {}", self.selected_paths.len()));
if ui.button("📋 Копировать").clicked() {
@@ -652,13 +815,17 @@ impl eframe::App for RcloneUI {
// Обработка отложенных действий
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);
}
if let Some(pattern) = self.pending_search_pattern.take() {
self.search_files(&pattern);
}
// Диалог передачи файлов (поддерживает несколько источников)
// Диалог передачи файлов (копирование/перемещение)
if self.show_transfer_dialog {
let source_count = self.transfer_source_list.len();
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 typ = self.new_remote_type.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("Имя хранилища:");
if ui.text_edit_singleline(&mut name).changed() {
self.new_remote_name = name;
}
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 {
if ui.selectable_value(&mut typ, t.clone(), t).clicked() {
self.new_remote_type = typ.clone();
@@ -751,7 +924,11 @@ impl eframe::App for RcloneUI {
// Диалог предупреждения о браузере
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.label("После завершения авторизации в браузере вернитесь в приложение.");
ui.label("Операция выполняется в фоне, интерфейс не зависнет.");
@@ -772,7 +949,11 @@ impl eframe::App for RcloneUI {
// Диалог подтверждения удаления хранилища
if self.show_delete_remote_dialog {
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 {
ui.label(format!("Удалить хранилище '{}'?", name));
ui.label("Это действие не удаляет данные, только конфигурацию.");
@@ -794,11 +975,19 @@ impl eframe::App for RcloneUI {
});
}
// Диалог ошибок
// Диалог ошибок / уведомлений
if let Some(error) = &self.error_message {
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.label(&error_clone);
if ui.button("OK").clicked() {
self.error_message = None;
@@ -809,6 +998,6 @@ impl eframe::App for RcloneUI {
});
}
ctx.request_repaint();
ctx.request_repaint_after(Duration::from_millis(100));
}
}