diff --git a/src/ui.rs b/src/ui.rs index 52bf7e5..80c5a96 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -58,3 +58,568 @@ pub struct AppSettings { 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 { + Self { + rclone: None, + state: AppState::Initializing, + error_message: None, + current_path: String::new(), + remote_list: Vec::new(), + current_files: Vec::new(), + selected_paths: Vec::new(), + transfer_source: String::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(), + new_remote_config: std::collections::HashMap::new(), + settings: AppSettings::default(), + } + } + + fn init_rclone(&mut self) { + let rt = Runtime::new().unwrap(); + let rclone_result = rt.block_on(async { + RcloneApp::new().await + }); + + match rclone_result { + Ok(rclone) => { + self.rclone = Some(Arc::new(rclone)); + self.load_remotes(); + } + Err(e) => { + let error_msg = format!("Не удалось инициализировать rclone: {}", e); + self.error_message = Some(error_msg.clone()); + self.state = AppState::Error(error_msg); + } + } + } + + 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) => { + let error_msg = e.clone(); + self.error_message = Some(e); + self.state = AppState::Error(error_msg); + } + } + } + } + + 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(); + } + Err(e) => { + self.error_message = Some(e); + } + } + } + } + + fn perform_copy(&mut self, source: &str, dest: &str) { + if let Some(rclone) = &self.rclone { + 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 rt = Runtime::new().unwrap(); + rt.block_on(async { + operations::sync::copy(&rclone_clone, &source_clone, &dest_clone, &options_clone) + }) + }); + + 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); + } + } + } + } + + 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) + } + } + + 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)) + } + } +} + +impl eframe::App for RcloneUI { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + 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); + } + }); + + 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() { + if limit_str.is_empty() { + self.settings.bandwidth_limit = None; + } else { + self.settings.bandwidth_limit = Some(limit_str); + } + } + }); + }); + + ui.separator(); + + match &self.state { + AppState::Ready => { + ui.colored_label(Color32::GREEN, "● Готов"); + } + 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())); + } + }); + }); + + egui::SidePanel::left("remotes_panel") + .default_width(200.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.add_space(2.0); + } + } + }); + }); + + CentralPanel::default().show(ctx, |ui| { + // Верхняя панель с путём и поиском + 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.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); + } 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); + } + } + }); + + ui.separator(); + + if matches!(self.state, AppState::Loading) || matches!(self.state, AppState::Initializing) { + ui.centered_and_justified(|ui| { + ui.label("Загрузка..."); + }); + 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("Результаты поиска"); + 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); + } + }); + } + }); + } 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); + } + + 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); + } else { + self.selected_paths.push(file_clone.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 { + 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.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.separator(); + + if !self.selected_paths.is_empty() { + ui.horizontal(|ui| { + ui.label(format!("Выбрано: {} элемент(ов)", self.selected_paths.len())); + ui.separator(); + + 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(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 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); + } + } + } + } + + if ui.button("❌ Отмена").clicked() { + self.show_new_remote_dialog = false; + self.new_remote_name.clear(); + self.new_remote_type.clear(); + } + }); + }); + } + + 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; + } + } + }); + } + + ctx.request_repaint(); + } +}