diff --git a/Cargo.lock b/Cargo.lock index 957a48c..6731dd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "dirs", "eframe", "egui", + "egui_extras", "flate2", "futures", "mockito", @@ -46,6 +47,10 @@ name = "accesskit" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" +dependencies = [ + "enumn", + "serde", +] [[package]] name = "accesskit_consumer" @@ -138,6 +143,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -1157,6 +1163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -1204,6 +1211,7 @@ dependencies = [ "epaint", "log", "nohash-hasher", + "serde", ] [[package]] @@ -1241,6 +1249,19 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_extras" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b78779f35ded1a853786c9ce0b43fe1053e10a21ea3b23ebea411805ce41593" +dependencies = [ + "egui", + "enum-map", + "log", + "mime_guess2", + "serde", +] + [[package]] name = "egui_glow" version = "0.27.2" @@ -1270,6 +1291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -1287,6 +1309,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1308,6 +1351,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "epaint" version = "0.27.2" @@ -1322,6 +1376,7 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", + "serde", ] [[package]] @@ -2461,6 +2516,18 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf", + "phf_shared", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2891,6 +2958,50 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3561,6 +3672,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -4078,6 +4195,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 5c4c4fc..1a0f012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,10 @@ serde_json = "1.0" chrono = "0.4" rfd = "0.14" -# GUI - simplified for better compatibility +# GUI eframe = { version = "0.27", features = ["default"] } egui = "0.27" +egui_extras = { version = "0.27" } [dev-dependencies] tempfile = "3.8" diff --git a/assets/icon.png b/assets/icon.png index e69de29..ab4a408 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/src/main.rs b/src/main.rs index 1e566ac..2e31609 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,3 @@ -//! Rclone UI Manager — графический интерфейс для rclone. -//! -//! Приложение предоставляет кроссплатформенную GUI для управления удалёнными -//! хранилищами rclone, просмотра файлов и выполнения операций копирования, -//! синхронизации, перемещения. - mod rclone_install; mod operations; mod ui; @@ -11,26 +5,12 @@ mod ui; use eframe::egui; use egui::{FontData, FontDefinitions, FontFamily}; -/// Точка входа в приложение. -/// -/// Загружает иконку окна, подключает пользовательские шрифты (Sans.otf и NotoColorEmoji.ttf) -/// и запускает нативное окно eframe с приложением RcloneUI. fn main() -> Result<(), eframe::Error> { - // Попытка загрузить иконку окна из assets/icon.png - let icon = std::fs::read("assets/icon.png") - .ok() - .and_then(|bytes| eframe::icon_data::from_png_bytes(&bytes).ok()) - .map(std::sync::Arc::new); - - let mut viewport = egui::ViewportBuilder::default() + let viewport = egui::ViewportBuilder::default() .with_inner_size([1200.0, 800.0]) .with_min_inner_size([800.0, 600.0]) .with_title("Rclone UI Manager"); - if let Some(icon) = icon { - viewport = viewport.with_icon(icon); - } - let options = eframe::NativeOptions { viewport, ..Default::default() @@ -40,41 +20,22 @@ fn main() -> Result<(), eframe::Error> { "Rclone UI Manager", options, Box::new(|cc| { - // Настройка пользовательских шрифтов let mut fonts = FontDefinitions::default(); - - // 1. Основной шрифт без засечек (Sans.otf) делаем шрифтом по умолчанию для пропорционального текста. + if let Ok(font_data) = std::fs::read("assets/fonts/Sans.otf") { - fonts.font_data.insert( - "sans".to_owned(), - FontData::from_owned(font_data), - ); - if let Some(proportional) = fonts.families.get_mut(&FontFamily::Proportional) { - proportional.insert(0, "sans".to_owned()); - } else { - fonts.families.insert( - FontFamily::Proportional, - vec!["sans".to_owned()], - ); - } + fonts.font_data.insert("sans".to_owned(), FontData::from_owned(font_data)); + fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "sans".to_owned()); } - // 2. Шрифт с эмодзи (NotoColorEmoji.ttf) подключаем как запасной. if let Ok(emoji_data) = std::fs::read("assets/fonts/NotoColorEmoji.ttf") { - fonts.font_data.insert( - "emoji".to_owned(), - FontData::from_owned(emoji_data), - ); - if let Some(proportional) = fonts.families.get_mut(&FontFamily::Proportional) { - if !proportional.contains(&"emoji".to_owned()) { - proportional.push("emoji".to_owned()); - } + fonts.font_data.insert("emoji".to_owned(), FontData::from_owned(emoji_data)); + let prop = fonts.families.entry(FontFamily::Proportional).or_default(); + if !prop.contains(&"emoji".to_owned()) { + prop.push("emoji".to_owned()); } } - // Применяем настройки шрифтов к egui-контексту. cc.egui_ctx.set_fonts(fonts); - Box::new(ui::RcloneUI::new(cc)) }), ) diff --git a/src/ui.rs b/src/ui.rs index a9dd5cf..6287c73 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,35 +2,20 @@ use eframe::egui; use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, Spinner, ProgressBar, SidePanel}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::mpsc::{channel, Sender, Receiver}; use std::time::{Duration, Instant}; use tokio::runtime::Runtime; use crate::rclone_install::RcloneApp; -use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions}; +use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions}; -/// Состояние приложения. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub enum AppState { Initializing, Ready, Error(String), - Copying, - Syncing, - Moving, - Deleting, - Loading, } -/// Информация о ходе передачи. -#[derive(Clone)] -pub struct TransferProgress { - pub current: u64, - pub total: u64, - pub speed: f64, - pub file_name: String, -} - -/// Операция для отображения в правой панели. #[derive(Clone)] pub struct Operation { pub id: u32, @@ -40,88 +25,65 @@ pub struct Operation { pub start_time: Instant, } -/// Результат длительной операции. enum OperationResult { - None, - Success(u32, String), // (operation_id, message) - Failure(u32, String), // (operation_id, error) + Success(u32, String), + Failure(u32, String), FileList(Vec), RemoteList(Vec), - SearchResults(Vec), ProgressUpdate(u32, f32, String), } -/// Главная структура UI. pub struct RcloneUI { rclone: Option>, state: AppState, error_message: Option, current_path: String, + // История переходов + history_back: Vec, + history_forward: Vec, + remote_list: Vec, current_files: Vec, selected_paths: Vec, - transfer_source_list: Vec, + transfer_source_info: Vec<(String, bool)>, transfer_dest: String, - transfer_progress: Option, - - search_pattern: String, - search_results: Vec, show_transfer_dialog: bool, - show_new_remote_dialog: bool, - new_remote_name: String, - new_remote_type: String, - available_remote_types: Vec, - new_remote_config: std::collections::HashMap, - + is_move_mode: bool, settings: AppSettings, active_task_count: u32, - operation_result: Arc>, - show_delete_remote_dialog: bool, - remote_to_delete: Option, - show_browser_warning: bool, - pending_remote_creation: Option<(String, String)>, - + operation_tx: Sender, + operation_rx: Receiver, + pending_load_path: Option, - pending_search_pattern: Option, - rclone_init_receiver: Option, String>>>, + rclone_init_receiver: Option, String>>>, + active_operations: Vec, next_op_id: u32, } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct AppSettings { pub show_hidden: bool, - pub confirm_before_transfer: bool, - pub max_concurrent_transfers: u32, pub bandwidth_limit: Option, } -impl Default for AppSettings { - fn default() -> Self { - Self { - show_hidden: false, - confirm_before_transfer: true, - max_concurrent_transfers: 4, - bandwidth_limit: None, - } - } -} - impl RcloneUI { pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - let (tx, rx) = std::sync::mpsc::channel(); + let (init_tx, init_rx) = channel(); + let (op_tx, op_rx) = channel(); + std::thread::spawn(move || { let rt = Runtime::new().unwrap(); let result = rt.block_on(async { RcloneApp::new().await }); - let result_string = match result { + let res = match result { Ok(app) => Ok(Arc::new(app)), Err(e) => Err(e.to_string()), }; - let _ = tx.send(result_string); + let _ = init_tx.send(res); }); Self { @@ -129,49 +91,63 @@ impl RcloneUI { state: AppState::Initializing, error_message: None, current_path: String::new(), + history_back: Vec::new(), + history_forward: Vec::new(), remote_list: Vec::new(), current_files: Vec::new(), selected_paths: Vec::new(), - transfer_source_list: Vec::new(), + transfer_source_info: Vec::new(), transfer_dest: String::new(), - transfer_progress: None, - search_pattern: String::new(), - search_results: Vec::new(), show_transfer_dialog: false, - show_new_remote_dialog: false, - new_remote_name: String::new(), - new_remote_type: String::new(), - available_remote_types: Vec::new(), - new_remote_config: std::collections::HashMap::new(), + is_move_mode: false, settings: AppSettings::default(), active_task_count: 0, - operation_result: Arc::new(Mutex::new(OperationResult::None)), - show_delete_remote_dialog: false, - remote_to_delete: None, - show_browser_warning: false, - pending_remote_creation: None, + operation_tx: op_tx, + operation_rx: op_rx, pending_load_path: None, - pending_search_pattern: None, - rclone_init_receiver: Some(rx), + rclone_init_receiver: Some(init_rx), active_operations: Vec::new(), next_op_id: 1, } } + // --- Логика навигации --- + fn navigate_to(&mut self, new_path: String) { + if self.current_path == new_path { return; } + if !self.current_path.is_empty() { + self.history_back.push(self.current_path.clone()); + } + self.history_forward.clear(); + self.pending_load_path = Some(new_path); + } + + fn go_back(&mut self) { + if let Some(prev_path) = self.history_back.pop() { + self.history_forward.push(self.current_path.clone()); + self.pending_load_path = Some(prev_path); + } + } + + fn go_forward(&mut self) { + if let Some(next_path) = self.history_forward.pop() { + self.history_back.push(self.current_path.clone()); + self.pending_load_path = Some(next_path); + } + } + + // --- Фоновые процессы --- 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(rclone); - self.load_remote_types(); self.load_remotes(); self.state = AppState::Ready; } Err(e) => { - let error_msg = format!("Не удалось инициализировать rclone: {}", e); - self.error_message = Some(error_msg.clone()); - self.state = AppState::Error(error_msg); + self.error_message = Some(format!("Критическая ошибка: {}", e)); + self.state = AppState::Error(e); } } self.rclone_init_receiver = None; @@ -179,316 +155,133 @@ impl RcloneUI { } } - fn load_remote_types(&mut self) { - if let Some(rclone) = &self.rclone { - match operations::info::backends(rclone) { - Ok(types) => self.available_remote_types = types, - Err(_) => { - self.available_remote_types = vec![ - "drive".to_string(), "s3".to_string(), "dropbox".to_string(), - "local".to_string(), "onedrive".to_string(), "webdav".to_string(), - ]; - } - } - } - } - fn load_remotes(&mut self) { - 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(); + let tx = self.operation_tx.clone(); std::thread::spawn(move || { - let remotes = operations::remotes::list(&rclone); - let mut holder = result_holder.lock().unwrap(); - *holder = match remotes { + let _ = tx.send(match operations::remotes::list(&rclone) { Ok(list) => OperationResult::RemoteList(list), - Err(e) => OperationResult::Failure(0, e), - }; + Err(e) => OperationResult::Failure(0, format!("Ошибка списка хранилищ: {}", e)), + }); }); } } fn load_files(&mut self, path: &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 path = path.to_string(); - let result_holder = self.operation_result.clone(); + let tx = self.operation_tx.clone(); std::thread::spawn(move || { - let files = operations::files::list(&rclone, &path); - let mut holder = result_holder.lock().unwrap(); - *holder = match files { + let _ = tx.send(match operations::files::list(&rclone, &path) { Ok(list) => OperationResult::FileList(list), - Err(e) => OperationResult::Failure(0, e), - }; - }); - } - } - - 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 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(); - *holder = match results { - Ok(list) => OperationResult::SearchResults(list), - Err(e) => OperationResult::Failure(0, e), - }; + Err(e) => OperationResult::Failure(0, format!("Ошибка доступа к папке: {}", e)), + }); }); } } fn poll_background_operation(&mut self) { - if self.active_task_count == 0 { - if matches!(self.state, AppState::Copying | AppState::Moving | AppState::Deleting | AppState::Loading) { - self.state = AppState::Ready; - } - return; - } - let result = { - let mut holder = self.operation_result.lock().unwrap(); - std::mem::replace(&mut *holder, OperationResult::None) - }; - - 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.state = AppState::Ready; - } - 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); // ошибки показываем - self.state = AppState::Ready; - } - OperationResult::FileList(files) => { - self.current_files = files; - self.active_task_count = self.active_task_count.saturating_sub(1); - self.state = AppState::Ready; - } - OperationResult::RemoteList(remotes) => { - self.remote_list = remotes; - self.active_task_count = self.active_task_count.saturating_sub(1); - self.state = AppState::Ready; - } - OperationResult::SearchResults(results) => { - self.search_results = results; - self.active_task_count = self.active_task_count.saturating_sub(1); - self.state = AppState::Ready; - } - 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; + while let Ok(result) = self.operation_rx.try_recv() { + match result { + OperationResult::Success(op_id, _) => { + self.active_task_count = self.active_task_count.saturating_sub(1); + self.active_operations.retain(|op| op.id != op_id); + self.pending_load_path = Some(self.current_path.clone()); + } + OperationResult::Failure(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::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; - } + // --- Операции --- + fn perform_copy_multiple(&mut self, sources: Vec<(String, bool)>, dest: &str) { if let Some(rclone) = self.rclone.clone() { - let name = name.to_string(); - let op_id = self.add_operation(format!("Удаление хранилища '{}'", name)); + let options = CopyOptions { verbose: true, dry_run: false, bandwidth_limit: self.settings.bandwidth_limit.clone(), no_traverse: false }; + let dest_base = dest.to_string(); + let op_id = self.add_operation(format!("Копирование {} эл.", sources.len())); self.active_task_count += 1; - let result_holder = self.operation_result.clone(); + let tx = self.operation_tx.clone(); std::thread::spawn(move || { - let result = operations::remotes::delete(&rclone, &name) - .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), - }; + let total = sources.len() as f32; + for (idx, (source, is_dir)) in sources.iter().enumerate() { + let name = source.split([':', '/']).last().unwrap_or(""); + let final_dest = if *is_dir { format!("{}/{}", dest_base.trim_end_matches('/'), name) } else { dest_base.clone() }; + if let Err(e) = operations::sync::copy(&rclone, source, &final_dest, &options) { + let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка копирования {}: {}", source, e))); return; + } + let _ = tx.send(OperationResult::ProgressUpdate(op_id, (idx + 1) as f32 / total, format!("{}/{}", idx+1, total))); + } + let _ = tx.send(OperationResult::Success(op_id, "Завершено".into())); }); } } - fn perform_move_multiple(&mut self, sources: Vec, dest: &str) { - if self.active_task_count > 0 || self.rclone.is_none() { - return; - } + fn perform_move_multiple(&mut self, sources: Vec<(String, bool)>, dest: &str) { if let Some(rclone) = self.rclone.clone() { - self.state = AppState::Moving; - let options = MoveOptions { - verbose: true, - dry_run: false, - delete_empty_src_dirs: true, - }; - let dest = dest.to_string(); - let op_id = self.add_operation(format!("Перемещение {} элементов", sources.len())); + let options = MoveOptions { verbose: true, dry_run: false, delete_empty_src_dirs: true }; + let dest_base = dest.to_string(); + let op_id = self.add_operation(format!("Перемещение {} эл.", sources.len())); self.active_task_count += 1; - let result_holder = self.operation_result.clone(); + let tx = self.operation_tx.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 (idx, source) in sources.iter().enumerate() { - match operations::sync::move_files(&rclone, source, &dest, &options) { - Ok(stats) => { - moved_count += 1; - total_bytes += stats.transferred; - } - Err(e) => { - errors.push(format!("{}: {}", source, e)); - } + for (idx, (source, is_dir)) in sources.iter().enumerate() { + let name = source.split([':', '/']).last().unwrap_or(""); + let final_dest = if *is_dir { format!("{}/{}", dest_base.trim_end_matches('/'), name) } else { dest_base.clone() }; + if let Err(e) = operations::sync::move_files(&rclone, source, &final_dest, &options) { + let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка перемещения {}: {}", source, e))); return; } - 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 + let _ = tx.send(OperationResult::ProgressUpdate(op_id, (idx + 1) as f32 / total, format!("{}/{}", idx+1, total))); } - - 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, 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 { - verbose: true, - dry_run: false, - bandwidth_limit: self.settings.bandwidth_limit.clone(), - no_traverse: false, - }; - let dest = dest.to_string(); - 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 (idx, source) in sources.iter().enumerate() { - match operations::sync::copy(&rclone, source, &dest, &options) { - Ok(_stats) => { - copied_count += 1; - } - Err(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)); - } - - 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), - }; + let _ = tx.send(OperationResult::Success(op_id, "Завершено".into())); }); } } fn delete_selected_paths(&mut self, paths: Vec) { - 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(':') { - format!("{}{}", self.current_path, file.name) - } else { - format!("{}/{}", self.current_path, file.name) - }; - is_dir_map.insert(full_path, file.is_dir); + let mut path_info = Vec::new(); + for path in &paths { + let is_dir = self.current_files.iter().find(|f| { + let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, f.name) } + else { format!("{}/{}", self.current_path, f.name) }; + &full == path + }).map(|f| f.is_dir).unwrap_or(false); + path_info.push((path.clone(), is_dir)); } - let op_id = self.add_operation(format!("Удаление {} элементов", paths.len())); + let op_id = self.add_operation(format!("Удаление {} эл.", paths.len())); self.active_task_count += 1; - let result_holder = self.operation_result.clone(); + let tx = self.operation_tx.clone(); std::thread::spawn(move || { - let mut errors = Vec::new(); - let mut deleted_count = 0; - let total = paths.len() as f32; - - 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) { - Ok(stats) => { - deleted_count += 1; - println!("Удалено: {} ({} элементов)", path, stats.files); - } - Err(e) => { - errors.push(format!("{}: {}", path, e)); - } + let total = path_info.len() as f32; + for (idx, (p, is_dir)) in path_info.iter().enumerate() { + let opts = DeleteOptions { recursive: *is_dir, verbose: false, dry_run: false }; + if let Err(e) = operations::sync::delete(&rclone, p, &opts) { + let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка удаления {}: {}", p, e))); return; } - 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)); + let _ = tx.send(OperationResult::ProgressUpdate(op_id, (idx + 1) as f32 / total, format!("{}/{}", idx+1, total))); } - - 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), - }; + let _ = tx.send(OperationResult::Success(op_id, "Удалено".into())); }); } } @@ -496,81 +289,26 @@ impl RcloneUI { 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(), - }); + self.active_operations.push(Operation { id, description, progress: 0.0, status: "В очереди".into(), 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(); - 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()); - }); - 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), - }; - }); - } - } - - fn validate_remote_name(name: &str) -> bool { - if name.is_empty() || name.starts_with(' ') || name.starts_with('-') || name.ends_with(' ') { - return false; - } - name.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.' || ch == '+' || ch == '@' || ch == ' ') - } - fn format_size(bytes: u64) -> String { - 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 >= 1073741824 { format!("{:.2} ГБ", bytes as f64 / 1073741824.0) } + else if bytes >= 1048576 { format!("{:.2} МБ", bytes as f64 / 1048576.0) } + else if bytes >= 1024 { format!("{:.2} КБ", bytes as f64 / 1024.0) } + 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)) - } + fn get_selected_info(&self) -> Vec<(String, bool)> { + self.selected_paths.iter().map(|path| { + let is_dir = self.current_files.iter().find(|f| { + let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, f.name) } + else { format!("{}/{}", self.current_path, f.name) }; + &full == path + }).map(|f| f.is_dir).unwrap_or(false); + (path.clone(), is_dir) + }).collect() } } @@ -579,433 +317,149 @@ impl eframe::App for RcloneUI { self.poll_rclone_init(); self.poll_background_operation(); - // Верхняя панель меню - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { + let is_busy = !self.active_operations.is_empty(); + + egui::TopBottomPanel::top("top").show(ctx, |ui| { egui::menu::bar(ui, |ui| { - ui.menu_button("Файл", |ui| { - if ui.button("Выход").clicked() { - std::process::exit(0); - } - }); - ui.menu_button("Хранилища", |ui| { - if ui.button("Добавить новое хранилище").clicked() { - self.show_new_remote_dialog = true; - } - if ui.button("Обновить список").clicked() { - self.load_remotes(); - } - }); - ui.menu_button("Настройки", |ui| { - ui.checkbox(&mut self.settings.show_hidden, "Показывать скрытые файлы"); - ui.checkbox(&mut self.settings.confirm_before_transfer, "Подтверждать перед передачей"); - ui.add(egui::Slider::new(&mut self.settings.max_concurrent_transfers, 1..=10).text("Макс. одновременных передач")); - ui.horizontal(|ui| { - ui.label("Ограничение скорости:"); - let mut limit_str = self.settings.bandwidth_limit.clone().unwrap_or_default(); - if ui.text_edit_singleline(&mut limit_str).changed() { - self.settings.bandwidth_limit = if limit_str.is_empty() { None } else { Some(limit_str) }; - } - }); - }); + ui.label("🚀 UI rclone"); ui.separator(); - if self.active_task_count > 0 { - ui.horizontal(|ui| { - ui.add(Spinner::new().size(16.0)); - 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, "● Занят"); - } + + // Навигация + ui.add_enabled_ui(!self.history_back.is_empty(), |ui| { + if ui.button("⬅").clicked() { self.go_back(); } + }); + ui.add_enabled_ui(!self.history_forward.is_empty(), |ui| { + if ui.button("➡").clicked() { self.go_forward(); } + }); + + if self.active_task_count > 0 { ui.add(Spinner::new().size(16.0)); } + }); + }); + + SidePanel::left("left").default_width(200.0).show(ctx, |ui| { + ui.add_space(10.0); + ui.heading("Хранилища"); + ui.separator(); + ScrollArea::vertical().show(ui, |ui| { + for remote in self.remote_list.clone() { + if ui.selectable_label(self.current_path.starts_with(&remote.name), format!("📡 {}", remote.name)).clicked() { + self.navigate_to(format!("{}:", remote.name)); } } - if let Some(rclone) = &self.rclone { - ui.separator(); - ui.label(format!("rclone: {}", rclone.get_rclone_path().display())); - } }); }); - // Левая панель со списком хранилищ - SidePanel::left("remotes_panel") - .default_width(220.0) - .show(ctx, |ui| { - ui.heading("Хранилища"); - ui.separator(); - ScrollArea::vertical().show(ui, |ui| { - let remotes = self.remote_list.clone(); - if remotes.is_empty() && matches!(self.state, AppState::Ready) { - ui.colored_label(Color32::GRAY, "Хранилища не найдены"); - if ui.button("➕ Добавить хранилище").clicked() { - self.show_new_remote_dialog = true; - } - } else { - for remote in &remotes { - ui.horizontal(|ui| { - 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() { - self.remote_to_delete = Some(remote.name.clone()); - self.show_delete_remote_dialog = true; - } - }); - ui.add_space(2.0); - } - } - }); - }); - - // Правая панель с операциями - 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); - } + SidePanel::right("right").default_width(220.0).show(ctx, |ui| { + ui.heading("Активность"); + ui.separator(); + 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.small(&op.status); }); } }); + }); - // Центральная панель CentralPanel::default().show(ctx, |ui| { - let current_path = self.current_path.clone(); - let search_pattern = self.search_pattern.clone(); - let mut new_search = search_pattern.clone(); ui.horizontal(|ui| { - ui.label("📍 Путь:"); - ui.label(if current_path.is_empty() { "Выберите хранилище" } else { ¤t_path }); - ui.separator(); - 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() && 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() && self.active_task_count == 0 { - self.pending_load_path = Some(self.current_path.clone()); - } + ui.label(format!("📍 {}", self.current_path)); + if ui.button("🔄").clicked() { self.pending_load_path = Some(self.current_path.clone()); } }); ui.separator(); - 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, "⟳ Выполняется операция... Пожалуйста, подождите."); - }); - ctx.request_repaint_after(Duration::from_millis(50)); - return; - } - - if let AppState::Error(msg) = &self.state { - ui.centered_and_justified(|ui| { - ui.colored_label(Color32::RED, format!("Ошибка: {}", msg)); - }); - return; - } - - if !self.search_results.is_empty() { - let search_results = self.search_results.clone(); - let current_path = self.current_path.clone(); - ScrollArea::vertical().show(ui, |ui| { - ui.heading("Результаты поиска"); - for file in &search_results { - ui.horizontal(|ui| { - ui.label(file.icon()); - 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 { - format!("{}/{}", current_path, file.name) - }; - self.pending_load_path = Some(new_path); - } - ui.label(Self::format_size(file.size)); - }); + ScrollArea::vertical().id_source("files").show(ui, |ui| { + if !self.current_path.is_empty() { + if self.current_path.contains('/') && ui.button("📁 .. (Вверх)").clicked() { + if let Some((p, _)) = self.current_path.rsplit_once('/') { self.navigate_to(p.into()); } } - }); - } else if !self.current_path.is_empty() { - 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() && 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 ¤t_files { - let full_path = if current_path.ends_with(':') { - format!("{}{}", current_path, file.name) - } else { - format!("{}/{}", current_path, file.name) - }; - let selected = self.selected_paths.contains(&full_path); + + for file in self.current_files.clone() { + let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, file.name) } + else { format!("{}/{}", self.current_path, file.name) }; + let sel = self.selected_paths.contains(&full); + ui.horizontal(|ui| { - if ui.selectable_label(selected, file.icon()).clicked() && self.active_task_count == 0 { - if selected { - self.selected_paths.retain(|p| p != &full_path); - } else { - self.selected_paths.push(full_path.clone()); - } + if ui.selectable_label(sel, file.icon()).clicked() { + if sel { self.selected_paths.retain(|p| p != &full); } + else { self.selected_paths.push(full.clone()); } } if file.is_dir { - 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 { - format!("{}/{}", current_path, file.name) - }; - self.pending_load_path = Some(new_path); - } - } else { - ui.label(&file.name); - } - ui.label(Self::format_size(file.size)); + if ui.link(&file.name).clicked() { self.navigate_to(full); } + } else { ui.label(&file.name); } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.small(Self::format_size(file.size)); + }); }); } - }); - } else { - ui.centered_and_justified(|ui| { - ui.heading("Добро пожаловать в Rclone UI Manager"); - ui.label("Выберите хранилище из левой панели или добавьте новое"); - if ui.button("➕ Добавить новое хранилище").clicked() { - self.show_new_remote_dialog = true; - } - }); - } + } else { + ui.centered_and_justified(|ui| { ui.label("Выберите диск в левой панели"); }); + } + }); - ui.separator(); - if !self.selected_paths.is_empty() && self.active_task_count == 0 { + if !self.selected_paths.is_empty() { + ui.separator(); ui.horizontal(|ui| { ui.label(format!("Выбрано: {}", self.selected_paths.len())); - if ui.button("📋 Копировать").clicked() { - self.transfer_source_list = self.selected_paths.clone(); - self.show_transfer_dialog = true; - } - if ui.button("✂️ Переместить").clicked() { - self.transfer_source_list = self.selected_paths.clone(); - self.show_transfer_dialog = true; - } - if ui.button("🗑️ Удалить").clicked() { - let paths_to_delete = self.selected_paths.clone(); - self.delete_selected_paths(paths_to_delete); - self.selected_paths.clear(); - } + ui.add_enabled_ui(!is_busy, |ui| { + if ui.button("📋 Копировать").clicked() { + self.transfer_source_info = self.get_selected_info(); + self.is_move_mode = false; + self.show_transfer_dialog = true; + } + if ui.button("✂ Переместить").clicked() { + self.transfer_source_info = self.get_selected_info(); + self.is_move_mode = true; + self.show_transfer_dialog = true; + } + if ui.button("🗑 Удалить").clicked() { + self.delete_selected_paths(self.selected_paths.clone()); + self.selected_paths.clear(); + } + }); }); } }); - // Обработка отложенных действий + // Модальные окна + if self.show_transfer_dialog { + Window::new(if self.is_move_mode { "Перемещение" } else { "Копирование" }) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { + ui.label("Куда отправить:"); + ui.text_edit_singleline(&mut self.transfer_dest); + if !self.transfer_dest.contains(':') && !self.transfer_dest.is_empty() { + ui.colored_label(Color32::YELLOW, "⚠ Путь rclone должен содержать ':'"); + } + ui.horizontal(|ui| { + if ui.button("ОК").clicked() && !self.transfer_dest.is_empty() { + let (s, d) = (self.transfer_source_info.clone(), self.transfer_dest.clone()); + if self.is_move_mode { self.perform_move_multiple(s, &d); } + else { self.perform_copy_multiple(s, &d); } + self.show_transfer_dialog = false; + } + if ui.button("Отмена").clicked() { self.show_transfer_dialog = false; } + }); + }); + } + + if let Some(msg) = &self.error_message { + let mut close = false; + Window::new("Сообщение").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { + ui.colored_label(Color32::LIGHT_RED, "⚠️ Событие:"); + ui.label(msg); + if ui.button("Закрыть").clicked() { close = true; } + }); + if close { self.error_message = None; } + } + + // Выполнение отложенной загрузки 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(); - Window::new("Передача файлов") - .collapsible(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.label(format!("Выбрано элементов для передачи: {}", source_count)); - if source_count <= 3 { - for src in &self.transfer_source_list { - ui.label(format!("📄 {}", src)); - } - } else { - ui.label(format!("и ещё {} элементов", source_count - 3)); - } - ui.separator(); - ui.label("Назначение (директория):"); - let mut dest_edit = dest.clone(); - if ui.text_edit_singleline(&mut dest_edit).changed() { - self.transfer_dest = dest_edit; - } - ui.separator(); - let current_dest = self.transfer_dest.clone(); - let sources = self.transfer_source_list.clone(); - ui.horizontal(|ui| { - if ui.button("✅ Копировать").clicked() { - self.perform_copy_multiple(sources.clone(), ¤t_dest); - self.show_transfer_dialog = false; - self.selected_paths.clear(); - self.transfer_source_list.clear(); - self.transfer_dest.clear(); - } - if ui.button("✂️ Переместить").clicked() { - self.perform_move_multiple(sources.clone(), ¤t_dest); - self.show_transfer_dialog = false; - self.selected_paths.clear(); - self.transfer_source_list.clear(); - self.transfer_dest.clear(); - } - if ui.button("❌ Отмена").clicked() { - self.show_transfer_dialog = false; - self.transfer_source_list.clear(); - self.transfer_dest.clear(); - } - }); - }); - } - - // Диалог добавления хранилища - if self.show_new_remote_dialog { - 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| { - 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| { - for t in &available_types { - if ui.selectable_value(&mut typ, t.clone(), t).clicked() { - self.new_remote_type = typ.clone(); - } - } - }); - ui.label("⚠️ Некоторые типы (Google Drive, Dropbox и др.) откроют браузер для авторизации."); - let name_clone = self.new_remote_name.clone(); - let type_clone = self.new_remote_type.clone(); - ui.horizontal(|ui| { - if ui.button("✅ Создать").clicked() { - if !Self::validate_remote_name(&name_clone) { - self.error_message = Some("Недопустимое имя хранилища".to_string()); - } else if type_clone.is_empty() { - self.error_message = Some("Выберите тип".to_string()); - } else { - self.create_remote_with_warning(&name_clone, &type_clone); - self.show_new_remote_dialog = false; - } - } - if ui.button("❌ Отмена").clicked() { - self.show_new_remote_dialog = false; - self.new_remote_name.clear(); - self.new_remote_type.clear(); - } - }); - }); - } - - // Диалог предупреждения о браузере - if self.show_browser_warning { - 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("Операция выполняется в фоне, интерфейс не зависнет."); - ui.separator(); - if ui.button("Продолжить").clicked() { - if let Some((name, typ)) = self.pending_remote_creation.take() { - self.execute_remote_creation(&name, &typ); - } - self.show_browser_warning = false; - } - if ui.button("Отмена").clicked() { - self.pending_remote_creation = None; - self.show_browser_warning = false; - } - }); - } - - // Диалог подтверждения удаления хранилища - 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| { - if let Some(ref name) = remote_name { - ui.label(format!("Удалить хранилище '{}'?", name)); - ui.label("Это действие не удаляет данные, только конфигурацию."); - let name_clone = name.clone(); - ui.horizontal(|ui| { - if ui.button("✅ Удалить").clicked() { - self.delete_remote(&name_clone); - self.show_delete_remote_dialog = false; - self.remote_to_delete = None; - } - if ui.button("❌ Отмена").clicked() { - self.show_delete_remote_dialog = false; - self.remote_to_delete = None; - } - }); - } else { - self.show_delete_remote_dialog = false; - } - }); - } - - // Диалог ошибок / уведомлений - 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| { - 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; - if let AppState::Error(_) = &self.state { - self.state = AppState::Ready; - } - } - }); - } ctx.request_repaint_after(Duration::from_millis(100)); }