diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..a77793f --- /dev/null +++ b/src/build.rs @@ -0,0 +1,49 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + #[cfg(target_os = "linux")] + { + // Check for X11 libraries + println!("cargo:rustc-link-lib=X11"); + println!("cargo:rustc-link-lib=Xrandr"); + println!("cargo:rustc-link-lib=Xinerama"); + println!("cargo:rustc-link-lib=Xcursor"); + println!("cargo:rustc-link-lib=Xi"); + + // Check for Wayland libraries (optional) + if env::var("CARGO_FEATURE_WAYLAND").is_ok() { + println!("cargo:rustc-link-lib=wayland-client"); + println!("cargo:rustc-link-lib=wayland-egl"); + } + + // Set library search paths + let lib_paths = vec![ + "/usr/lib/x86_64-linux-gnu", + "/usr/lib64", + "/usr/lib", + ]; + + for path in lib_paths { + println!("cargo:rustc-link-search={}", path); + } + + // Set pkg-config for better detection + if let Ok(pkg_config) = which::which("pkg-config") { + println!("cargo:rustc-env=PKG_CONFIG_PATH={}", pkg_config.display()); + } + } + + #[cfg(target_os = "windows")] + { + println!("cargo:rustc-link-lib=user32"); + println!("cargo:rustc-link-lib=shell32"); + println!("cargo:rustc-link-lib=gdi32"); + } + + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=Foundation"); + } +} diff --git a/src/operations/info.rs b/src/operations/info.rs index b9a3d83..0454b18 100644 --- a/src/operations/info.rs +++ b/src/operations/info.rs @@ -24,3 +24,20 @@ pub fn stats(app: &RcloneApp) -> Result { serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?; Ok(stats) } + +/// Получить список поддерживаемых бэкендов (типов хранилищ) +pub fn backends(app: &RcloneApp) -> Result, String> { + let output = app.run_command(&["help", "backends"])?; + let mut backends = Vec::new(); + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + // Формат строки: " drive Google Drive" + if let Some(first_word) = line.split_whitespace().next() { + backends.push(first_word.to_string()); + } + } + Ok(backends) +} diff --git a/src/ui.rs b/src/ui.rs index 7a65e58..8b3102b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,111 +1,86 @@ //! Графический интерфейс для операций rclone. -//! -//! Этот модуль содержит главную структуру `RcloneUI` и её реализацию, -//! включая панели, диалоги, просмотр файлов и управление передачей данных. use eframe::egui; -use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, ProgressBar}; -use std::sync::Arc; +use egui::{CentralPanel, ScrollArea, Window, Align2, Color32}; +use std::sync::{Arc, Mutex}; use tokio::runtime::Runtime; use crate::rclone_install::RcloneApp; -use crate::operations::{self, FileInfo, Remote, CopyOptions, FindOptions}; +use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions}; /// Состояние приложения. #[derive(Clone)] pub enum AppState { - /// Приложение инициализируется (проверка наличия rclone). 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, } -/// Главная структура UI, содержащая всё состояние и логику. +/// Результат длительной операции. +enum OperationResult { + None, + Success(String), + Failure(String), +} + +/// Главная структура UI. pub struct RcloneUI { - /// Экземпляр rclone (инициализируется при первом использовании). rclone: Option>, - /// Текущее состояние приложения. state: AppState, - /// Глобальное сообщение об ошибке. error_message: Option, - // Навигация - /// Текущий отображаемый путь. current_path: String, - /// Список доступных удалённых хранилищ. remote_list: Vec, - /// Файлы и папки в текущем пути. current_files: Vec, - - // Выделение - /// Выбранные пользователем пути. selected_paths: Vec, - // Передача - /// Исходный путь для операции передачи. transfer_source: String, - /// Целевой путь для операции передачи. transfer_dest: String, - /// Прогресс текущей передачи. transfer_progress: Option, - // Поиск - /// Текущий шаблон поиска. search_pattern: String, - /// Результаты последнего поиска. search_results: Vec, - // Диалоги UI - /// Открыт ли диалог передачи. show_transfer_dialog: bool, - /// Открыт ли диалог создания нового хранилища. show_new_remote_dialog: bool, - /// Имя нового хранилища. new_remote_name: String, - /// Тип нового хранилища (например, "s3", "dropbox"). new_remote_type: String, - /// Конфигурационные параметры нового хранилища. + available_remote_types: Vec, new_remote_config: std::collections::HashMap, - // Настройки - /// Настройки пользователя. settings: AppSettings, + + background_working: bool, + operation_result: Arc>, + operation_id: u32, + show_delete_remote_dialog: bool, + remote_to_delete: Option, + show_browser_warning: bool, + pending_remote_creation: Option<(String, String)>, + + // Отложенные действия + pending_load_path: Option, + pending_search_pattern: Option, } -/// Настройки приложения, задаваемые пользователем. #[derive(Clone)] pub struct AppSettings { - /// Показывать скрытые файлы. pub show_hidden: bool, - /// Запрашивать подтверждение перед началом передачи. pub confirm_before_transfer: bool, - /// Максимальное количество одновременных передач. pub max_concurrent_transfers: u32, - /// Ограничение пропускной способности (например, "1M"). pub bandwidth_limit: Option, } @@ -121,9 +96,6 @@ impl Default for AppSettings { } impl RcloneUI { - /// Создаёт новый экземпляр UI. - /// - /// Бэкенд rclone инициализируется позже, при первом вызове `update`. pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self { rclone: None, @@ -142,23 +114,28 @@ impl RcloneUI { 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(), settings: AppSettings::default(), + background_working: false, + 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. - /// - /// Пытается найти системную установку или скачивает последнюю версию. fn init_rclone(&mut self) { let rt = Runtime::new().unwrap(); - let rclone_result = rt.block_on(async { - RcloneApp::new().await - }); - + let rclone_result = rt.block_on(async { RcloneApp::new().await }); match rclone_result { Ok(rclone) => { self.rclone = Some(Arc::new(rclone)); + self.load_remote_types(); self.load_remotes(); } Err(e) => { @@ -169,7 +146,20 @@ 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 let Some(rclone) = &self.rclone { match operations::remotes::list(rclone) { @@ -178,18 +168,13 @@ impl RcloneUI { self.state = AppState::Ready; } Err(e) => { - let error_msg = e.clone(); - self.error_message = Some(e); - self.state = AppState::Error(error_msg); + self.error_message = Some(e.clone()); + self.state = AppState::Error(e); } } } } - /// Загружает содержимое каталога. - /// - /// # Аргументы - /// * `path` – путь к удалённому хранилищу (например, "remote:" или "remote:папка/подпапка"). fn load_files(&mut self, path: &str) { if let Some(rclone) = &self.rclone { match operations::files::list(rclone, path) { @@ -199,6 +184,55 @@ impl RcloneUI { self.search_results.clear(); self.search_pattern.clear(); } + Err(e) => self.error_message = Some(e), + } + } + } + + fn start_background_operation(&mut self, f: F) -> u32 + where + F: FnOnce() -> Result + Send + 'static, + { + self.background_working = true; + self.operation_id = self.operation_id.wrapping_add(1); + let op_id = self.operation_id; + let result_holder = self.operation_result.clone(); + std::thread::spawn(move || { + let result = f(); + let mut holder = result_holder.lock().unwrap(); + *holder = match result { + Ok(msg) => OperationResult::Success(msg), + Err(e) => OperationResult::Failure(e), + }; + }); + op_id + } + + fn poll_background_operation(&mut self) { + if !self.background_working { + 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, + } + }; + 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(¤t_path); + } + self.error_message = None; + } Err(e) => { self.error_message = Some(e); } @@ -206,98 +240,120 @@ impl RcloneUI { } } - /// Выполняет операцию копирования из источника в приёмник. - /// - /// Запускается в отдельном потоке, чтобы не блокировать UI. + fn delete_remote(&mut self, name: &str) { + if let Some(rclone) = self.rclone.clone() { + let name = name.to_string(); + self.start_background_operation(move || { + operations::remotes::delete(&rclone, &name) + .map(|_| format!("Хранилище '{}' удалено", name)) + .map_err(|e| format!("Ошибка удаления: {}", e)) + }); + } + } + + fn perform_move(&mut self, source: &str, 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 source = source.to_string(); + let dest = dest.to_string(); + self.start_background_operation(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + operations::sync::move_files(&rclone, &source, &dest, &options) + }) + .map(|stats| format!("Перемещено {} файлов, {} байт", stats.files, stats.transferred)) + .map_err(|e| format!("Ошибка перемещения: {}", e)) + }); + } + } + fn perform_copy(&mut self, source: &str, dest: &str) { - if let Some(rclone) = &self.rclone { + 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 rclone_clone = rclone.clone(); - let source_clone = source.to_string(); - let dest_clone = dest.to_string(); - let options_clone = options; - - std::thread::spawn(move || { + let source = source.to_string(); + let dest = dest.to_string(); + self.start_background_operation(move || { let rt = Runtime::new().unwrap(); rt.block_on(async { - operations::sync::copy(&rclone_clone, &source_clone, &dest_clone, &options_clone) + operations::sync::copy(&rclone, &source, &dest, &options) }) + .map(|stats| format!("Скопировано {} файлов", stats.files)) + .map_err(|e| format!("Ошибка копирования: {}", e)) + }); + } + } + + 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 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)) }); - - // Для демонстрации имитируем завершение - self.transfer_progress = Some(TransferProgress { - current: 100, - total: 100, - speed: 10_000_000.0, - file_name: "Завершено".to_string(), - }); - self.state = AppState::Ready; } } - /// Ищет файлы по шаблону. fn search_files(&mut self, pattern: &str) { if let Some(rclone) = &self.rclone { let options = FindOptions { recursive: true, 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); - } + Ok(results) => self.search_results = results, + Err(e) => self.error_message = Some(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 >= 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) } } - /// Форматирует скорость передачи (байт/с) в удобочитаемый вид. 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) { - // Инициализация rclone, если ещё не выполнена + self.poll_background_operation(); + if matches!(self.state, AppState::Initializing) && self.rclone.is_none() { self.init_rclone(); } @@ -306,11 +362,8 @@ impl eframe::App for RcloneUI { 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() { self.show_new_remote_dialog = true; @@ -319,45 +372,29 @@ impl eframe::App for RcloneUI { 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() { - if limit_str.is_empty() { - self.settings.bandwidth_limit = None; - } else { - self.settings.bandwidth_limit = Some(limit_str); - } + self.settings.bandwidth_limit = if limit_str.is_empty() { None } else { Some(limit_str) }; } }); }); - ui.separator(); - - match &self.state { - AppState::Ready => { - ui.colored_label(Color32::GREEN, "● Готов"); + if self.background_working { + ui.colored_label(Color32::YELLOW, "⟳ Работа в фоне..."); + } 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::Initializing => { - ui.colored_label(Color32::YELLOW, "● Инициализация..."); - } - AppState::Error(_) => { - ui.colored_label(Color32::RED, "● Ошибка"); - } - AppState::Copying => { - ui.colored_label(Color32::BLUE, "● Копирование..."); - } - _ => { - ui.colored_label(Color32::GRAY, "● Занят"); - } - } - + }; if let Some(rclone) = &self.rclone { ui.separator(); ui.label(format!("rclone: {}", rclone.get_rclone_path().display())); @@ -365,78 +402,67 @@ impl eframe::App for RcloneUI { }); }); - // Левая панель — список хранилищ + // Левая панель со списком хранилищ egui::SidePanel::left("remotes_panel") - .default_width(200.0) + .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, "Хранилища не найдены"); - ui.add_space(5.0); if ui.button("➕ Добавить хранилище").clicked() { self.show_new_remote_dialog = true; } } else { for remote in &remotes { - let button_text = format!("📡 {}", remote.name); - if ui.button(button_text).clicked() { - let path = format!("{}:", remote.name); - self.load_files(&path); - } + ui.horizontal(|ui| { + if ui.button(format!("📡 {}", remote.name)).clicked() { + 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); } } }); }); - // Центральная панель — основное содержимое + // Центральная панель 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("📍 Путь:"); - let path_text = if self.current_path.is_empty() { - "Выберите хранилище из левой панели".to_string() - } else { - self.current_path.clone() - }; - ui.label(path_text); - + ui.label(if current_path.is_empty() { "Выберите хранилище" } else { ¤t_path }); ui.separator(); - ui.label("🔍 Поиск:"); - let mut new_search = self.search_pattern.clone(); if ui.text_edit_singleline(&mut new_search).changed() { self.search_pattern = new_search.clone(); - if !new_search.is_empty() && !self.current_path.is_empty() { - self.search_files(&new_search); + if !self.search_pattern.is_empty() && !self.current_path.is_empty() { + self.pending_search_pattern = Some(self.search_pattern.clone()); } else { self.search_results.clear(); } } - - let current_path_clone = self.current_path.clone(); - if ui.button("🔄 Обновить").clicked() { - if !current_path_clone.is_empty() { - self.load_files(¤t_path_clone); - } + if ui.button("🔄 Обновить").clicked() && !self.current_path.is_empty() { + self.pending_load_path = Some(self.current_path.clone()); } }); - ui.separator(); - // Состояние загрузки - if matches!(self.state, AppState::Loading) || matches!(self.state, AppState::Initializing) { + if self.background_working { ui.centered_and_justified(|ui| { - ui.label("Загрузка..."); + ui.colored_label(Color32::YELLOW, "⟳ Выполняется операция... Пожалуйста, подождите."); }); return; } - // Состояние ошибки if let AppState::Error(msg) = &self.state { ui.centered_and_justified(|ui| { ui.colored_label(Color32::RED, format!("Ошибка: {}", msg)); @@ -444,267 +470,236 @@ impl eframe::App for RcloneUI { 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("Результаты поиска"); - ui.separator(); - for file in &search_results { - let file_clone = file.clone(); ui.horizontal(|ui| { ui.label(file.icon()); - if ui.link(&file.name).clicked() { - if file_clone.is_dir { - let new_path = if current_path.ends_with(':') { - format!("{}{}", current_path, file_clone.name) - } else { - format!("{}/{}", current_path, file_clone.name) - }; - self.load_files(&new_path); - } - } - ui.label(Self::format_size(file_clone.size)); - if let Some(modified) = &file_clone.modified { - ui.label(modified); + if ui.link(&file.name).clicked() && file.is_dir { + 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)); }); } }); } 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('/') { - if ui.button("📁 .. (Родительская папка)").clicked() { - let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); - self.load_files(parent); - } - ui.add_space(5.0); + if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() { + let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); + self.pending_load_path = Some(parent.to_string()); } - ui.heading("Файлы и папки"); - ui.separator(); - - if current_files.is_empty() { - ui.colored_label(Color32::GRAY, "Папка пуста"); - } else { - for file in ¤t_files { - let file_clone = file.clone(); - let selected = self.selected_paths.contains(&file.path); - ui.horizontal(|ui| { - if ui.selectable_label(selected, file.icon()).clicked() { - if selected { - self.selected_paths.retain(|p| p != &file_clone.path); + for file in ¤t_files { + let selected = self.selected_paths.contains(&file.path); + ui.horizontal(|ui| { + if ui.selectable_label(selected, file.icon()).clicked() { + if selected { self.selected_paths.retain(|p| p != &file.path); } + else { self.selected_paths.push(file.path.clone()); } + } + if file.is_dir { + if ui.link(&file.name).clicked() { + let new_path = if current_path.ends_with(':') { + format!("{}{}", current_path, file.name) } else { - self.selected_paths.push(file_clone.path.clone()); - } + format!("{}/{}", current_path, file.name) + }; + self.pending_load_path = Some(new_path); } - - if file.is_dir { - if ui.link(&file.name).clicked() { - let new_path = if current_path.ends_with(':') { - format!("{}{}", current_path, file.name) - } else { - format!("{}/{}", current_path, file.name) - }; - self.load_files(&new_path); - } - } else { - ui.label(&file.name); - } - - ui.label(Self::format_size(file.size)); - if let Some(modified) = &file.modified { - ui.label(modified); - } - }); - } + } else { + ui.label(&file.name); + } + ui.label(Self::format_size(file.size)); + }); } }); } else { - // Приветственный экран ui.centered_and_justified(|ui| { - ui.vertical(|ui| { - ui.add_space(50.0); - ui.heading("Добро пожаловать в Rclone UI Manager"); - ui.add_space(20.0); - ui.label("Выберите хранилище из левой панели, чтобы начать"); - ui.add_space(10.0); - ui.label("Или добавьте новое хранилище через меню «Хранилища»"); - ui.add_space(30.0); - if ui.button("➕ Добавить новое хранилище").clicked() { - self.show_new_remote_dialog = true; - } - }); + ui.heading("Добро пожаловать в Rclone UI Manager"); + ui.label("Выберите хранилище из левой панели или добавьте новое"); + if ui.button("➕ Добавить новое хранилище").clicked() { + self.show_new_remote_dialog = true; + } }); } ui.separator(); - - // Кнопки действий if !self.selected_paths.is_empty() { ui.horizontal(|ui| { - ui.label(format!("Выбрано: {} элемент(ов)", self.selected_paths.len())); - ui.separator(); - + ui.label(format!("Выбрано: {}", self.selected_paths.len())); if ui.button("📋 Копировать").clicked() { self.transfer_source = self.selected_paths[0].clone(); self.show_transfer_dialog = true; } - if ui.button("✂️ Переместить").clicked() { self.transfer_source = self.selected_paths[0].clone(); self.show_transfer_dialog = true; } - if ui.button("🗑️ Удалить").clicked() { - if self.settings.confirm_before_transfer { - self.show_transfer_dialog = true; + if let Some(rclone) = self.rclone.clone() { + let path = self.selected_paths[0].clone(); + let options = DeleteOptions { recursive: true, verbose: true, dry_run: false }; + self.start_background_operation(move || { + operations::sync::delete(&rclone, &path, &options) + .map(|_| format!("Удалено: {}", path)) + .map_err(|e| format!("Ошибка удаления: {}", e)) + }); + self.selected_paths.clear(); } } }); } - - // Прогресс передачи - if let Some(progress) = &self.transfer_progress { - ui.separator(); - ui.horizontal(|ui| { - ui.label(format!("📊 Передача: {}", progress.file_name)); - let percent = if progress.total > 0 { - (progress.current as f64 / progress.total as f64) as f32 - } else { - 0.0 - }; - ui.add(ProgressBar::new(percent).show_percentage()); - ui.label(format!("Скорость: {}", Self::format_speed(progress.speed))); - }); - } }); - // Диалоги - if self.show_transfer_dialog { - let transfer_source = self.transfer_source.clone(); - let transfer_dest = self.transfer_dest.clone(); - let mut new_transfer_dest = transfer_dest.clone(); - - Window::new("Передача файлов") - .collapsible(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.label("Источник:"); - ui.label(&transfer_source); - - ui.add_space(10.0); - - ui.label("Назначение:"); - ui.text_edit_singleline(&mut new_transfer_dest); - - ui.add_space(10.0); - - ui.separator(); - - ui.horizontal(|ui| { - if ui.button("✅ Начать передачу").clicked() { - let source = transfer_source.clone(); - let dest = new_transfer_dest.clone(); - self.transfer_dest = dest.clone(); - self.perform_copy(&source, &dest); - self.show_transfer_dialog = false; - self.selected_paths.clear(); - } - - if ui.button("❌ Отмена").clicked() { - self.show_transfer_dialog = false; - } - }); - }); + // Обработка отложенных действий + if let Some(path) = self.pending_load_path.take() { + self.load_files(&path); + } + if let Some(pattern) = self.pending_search_pattern.take() { + self.search_files(&pattern); } + // Диалог передачи файлов + if self.show_transfer_dialog { + let source = self.transfer_source.clone(); + 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)); + 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(); + ui.horizontal(|ui| { + if ui.button("✅ Копировать").clicked() { + self.perform_copy(&source, ¤t_dest); + self.show_transfer_dialog = false; + self.selected_paths.clear(); + } + if ui.button("✂️ Переместить").clicked() { + self.perform_move(&source, ¤t_dest); + self.show_transfer_dialog = false; + self.selected_paths.clear(); + } + if ui.button("❌ Отмена").clicked() { + self.show_transfer_dialog = false; + } + }); + }); + } + + // Диалог добавления хранилища if self.show_new_remote_dialog { - let mut new_remote_name = self.new_remote_name.clone(); - let mut new_remote_type = self.new_remote_type.clone(); - Window::new("Добавление хранилища") - .collapsible(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.label("Имя хранилища:"); - ui.text_edit_singleline(&mut new_remote_name); - - ui.add_space(5.0); - - ui.label("Тип хранилища:"); - ui.text_edit_singleline(&mut new_remote_type); - - ui.add_space(10.0); - - ui.label("Распространённые типы: s3, dropbox, google drive, local"); - ui.add_space(10.0); - - ui.separator(); - - ui.horizontal(|ui| { - if ui.button("✅ Создать").clicked() { - if let Some(rclone) = &self.rclone { - let config = std::collections::HashMap::new(); - match operations::remotes::create( - rclone, - &new_remote_name, - &new_remote_type, - &config, - ) { - Ok(_) => { - self.new_remote_name = new_remote_name; - self.new_remote_type = new_remote_type; - self.load_remotes(); - self.show_new_remote_dialog = false; - self.new_remote_name.clear(); - self.new_remote_type.clear(); - } - Err(e) => { - self.error_message = Some(e); - } - } - } + 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(); } - - if ui.button("❌ Отмена").clicked() { + } + }); + 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; - self.new_remote_name.clear(); - self.new_remote_type.clear(); + } + } + 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| { - ui.colored_label(Color32::RED, "⚠️ Произошла ошибка:"); - ui.add_space(5.0); - ui.label(&error_clone); - ui.add_space(10.0); - if ui.button("OK").clicked() { - self.error_message = None; - if let AppState::Error(_) = &self.state { - self.state = AppState::Ready; - } + Window::new("Ошибка").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { + 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(); } }