From 43a59950365ef452776e81f66a101f973ecec128 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 29 Mar 2026 14:18:34 +0300 Subject: [PATCH] fix(Ui): Fix err with deleting files --- src/ui.rs | 363 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 224 insertions(+), 139 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 5e072f8..586fb18 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,12 +1,14 @@ //! Графический интерфейс для операций rclone. +use crate::operations::{ + self, CopyOptions, DeleteOptions, FileInfo, FindOptions, MoveOptions, Remote, +}; +use crate::rclone_install::RcloneApp; use eframe::egui; -use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, Spinner}; +use egui::{Align2, CentralPanel, Color32, ScrollArea, Spinner, Window}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::runtime::Runtime; -use crate::rclone_install::RcloneApp; -use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions, FindOptions}; /// Состояние приложения. #[derive(Clone)] @@ -42,28 +44,28 @@ pub struct RcloneUI { 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, - + 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, - + settings: AppSettings, - + background_working: bool, operation_result: Arc>, operation_id: u32, @@ -71,7 +73,7 @@ pub struct RcloneUI { remote_to_delete: Option, show_browser_warning: bool, pending_remote_creation: Option<(String, String)>, - + // Отложенные действия pending_load_path: Option, pending_search_pattern: Option, @@ -129,7 +131,7 @@ impl RcloneUI { pending_search_pattern: None, } } - + fn init_rclone(&mut self) { let rt = Runtime::new().unwrap(); let rclone_result = rt.block_on(async { RcloneApp::new().await }); @@ -146,21 +148,25 @@ 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(), + "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) { @@ -175,7 +181,7 @@ impl RcloneUI { } } } - + fn load_files(&mut self, path: &str) { if let Some(rclone) = &self.rclone { match operations::files::list(rclone, path) { @@ -191,7 +197,7 @@ impl RcloneUI { } } } - + fn start_background_operation(&mut self, f: F) -> u32 where F: FnOnce() -> Result + Send + 'static, @@ -210,7 +216,7 @@ impl RcloneUI { }); op_id } - + fn poll_background_operation(&mut self) { if !self.background_working { return; @@ -241,7 +247,7 @@ impl RcloneUI { } } } - + fn delete_remote(&mut self, name: &str) { if let Some(rclone) = self.rclone.clone() { let name = name.to_string(); @@ -252,7 +258,7 @@ impl RcloneUI { }); } } - + fn perform_move(&mut self, source: &str, dest: &str) { if let Some(rclone) = self.rclone.clone() { self.state = AppState::Moving; @@ -268,12 +274,17 @@ impl RcloneUI { rt.block_on(async { operations::sync::move_files(&rclone, &source, &dest, &options) }) - .map(|stats| format!("Перемещено {} файлов, {} байт", stats.files, stats.transferred)) + .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.clone() { self.state = AppState::Copying; @@ -287,20 +298,18 @@ impl RcloneUI { let dest = dest.to_string(); self.start_background_operation(move || { let rt = Runtime::new().unwrap(); - rt.block_on(async { - operations::sync::copy(&rclone, &source, &dest, &options) - }) - .map(|stats| format!("Скопировано {} файлов", stats.files)) - .map_err(|e| format!("Ошибка копирования: {}", e)) + rt.block_on(async { 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(); @@ -313,7 +322,7 @@ impl RcloneUI { }); } } - + fn search_files(&mut self, pattern: &str) { if let Some(rclone) = &self.rclone { let options = FindOptions { @@ -326,45 +335,66 @@ impl RcloneUI { } } } - + fn validate_remote_name(name: &str) -> bool { - if name.is_empty() || name.starts_with(' ') || name.starts_with('-') || name.ends_with(' ') { + 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 == ' ') + 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) { self.poll_background_operation(); - + if matches!(self.state, AppState::Initializing) && self.rclone.is_none() { self.init_rclone(); } - + // Верхняя панель меню egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("Файл", |ui| { - if ui.button("Выход").clicked() { std::process::exit(0); } + if ui.button("Выход").clicked() { + std::process::exit(0); + } }); ui.menu_button("Хранилища", |ui| { if ui.button("Добавить новое хранилище").clicked() { @@ -376,13 +406,24 @@ impl eframe::App for RcloneUI { }); 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.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(); + 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) }; + self.settings.bandwidth_limit = if limit_str.is_empty() { + None + } else { + Some(limit_str) + }; } }); }); @@ -395,10 +436,18 @@ impl eframe::App for RcloneUI { }); } else { match &self.state { - AppState::Ready => { ui.colored_label(Color32::GREEN, "● Готов"); } - AppState::Initializing => { ui.colored_label(Color32::YELLOW, "● Инициализация..."); } - AppState::Error(_) => { ui.colored_label(Color32::RED, "● Ошибка"); } - _ => { ui.colored_label(Color32::BLUE, "● Занят"); } + AppState::Ready => { + ui.colored_label(Color32::GREEN, "● Готов"); + } + AppState::Initializing => { + ui.colored_label(Color32::YELLOW, "● Инициализация..."); + } + AppState::Error(_) => { + ui.colored_label(Color32::RED, "● Ошибка"); + } + _ => { + ui.colored_label(Color32::BLUE, "● Занят"); + } } }; if let Some(rclone) = &self.rclone { @@ -407,7 +456,7 @@ impl eframe::App for RcloneUI { } }); }); - + // Левая панель со списком хранилищ egui::SidePanel::left("remotes_panel") .default_width(220.0) @@ -427,7 +476,8 @@ impl eframe::App for RcloneUI { if ui.button(format!("📡 {}", remote.name)).clicked() { self.pending_load_path = Some(format!("{}:", remote.name)); } - if ui.button("🗑️").on_hover_text("Удалить хранилище").clicked() { + if ui.button("🗑️").on_hover_text("Удалить хранилище").clicked() + { self.remote_to_delete = Some(remote.name.clone()); self.show_delete_remote_dialog = true; } @@ -437,7 +487,7 @@ impl eframe::App for RcloneUI { } }); }); - + // Центральная панель CentralPanel::default().show(ctx, |ui| { let current_path = self.current_path.clone(); @@ -445,7 +495,11 @@ impl eframe::App for RcloneUI { let mut new_search = search_pattern.clone(); ui.horizontal(|ui| { ui.label("📍 Путь:"); - ui.label(if current_path.is_empty() { "Выберите хранилище" } else { ¤t_path }); + ui.label(if current_path.is_empty() { + "Выберите хранилище" + } else { + ¤t_path + }); ui.separator(); ui.label("🔍 Поиск:"); if ui.text_edit_singleline(&mut new_search).changed() { @@ -461,7 +515,7 @@ impl eframe::App for RcloneUI { } }); ui.separator(); - + // Анимированная загрузка в центральной панели if self.background_working { ui.centered_and_justified(|ui| { @@ -469,20 +523,20 @@ impl eframe::App for RcloneUI { ui.add_space(10.0); ui.colored_label( egui::Color32::LIGHT_BLUE, - "⟳ Выполняется операция... Пожалуйста, подождите." + "⟳ Выполняется операция... Пожалуйста, подождите.", ); }); ctx.request_repaint_after(Duration::from_millis(16)); 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(); @@ -507,7 +561,9 @@ impl eframe::App for RcloneUI { let current_files = self.current_files.clone(); let current_path = self.current_path.clone(); ScrollArea::vertical().show(ui, |ui| { - if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() { + if current_path.contains('/') + && ui.button("📁 .. (Родительская папка)").clicked() + { let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); self.pending_load_path = Some(parent.to_string()); } @@ -553,7 +609,7 @@ impl eframe::App for RcloneUI { } }); } - + ui.separator(); if !self.selected_paths.is_empty() { ui.horizontal(|ui| { @@ -569,7 +625,20 @@ impl eframe::App for RcloneUI { if ui.button("🗑️ Удалить").clicked() { if let Some(rclone) = self.rclone.clone() { let path = self.selected_paths[0].clone(); - let options = DeleteOptions { recursive: true, verbose: true, dry_run: false }; + // Определяем, является ли выбранный путь директорией + let is_dir = self.current_files.iter().any(|f| { + let full_path = if self.current_path.ends_with(':') { + format!("{}{}", self.current_path, f.name) + } else { + format!("{}/{}", self.current_path, f.name) + }; + full_path == path && f.is_dir + }); + let options = DeleteOptions { + recursive: is_dir, + verbose: true, + dry_run: false, + }; self.start_background_operation(move || { operations::sync::delete(&rclone, &path, &options) .map(|_| format!("Удалено: {}", path)) @@ -581,7 +650,7 @@ impl eframe::App for RcloneUI { }); } }); - + // Обработка отложенных действий if let Some(path) = self.pending_load_path.take() { self.load_files(&path); @@ -589,38 +658,42 @@ impl eframe::App for RcloneUI { 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; + 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 name = self.new_remote_name.clone(); @@ -661,67 +734,79 @@ impl eframe::App for RcloneUI { }); }); } - + // Диалог предупреждения о браузере if self.show_browser_warning { - Window::new("Внимание").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { - 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); + 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; } - self.show_browser_warning = false; - } - if ui.button("Отмена").clicked() { - self.pending_remote_creation = None; - 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; - } - }); + 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.label(&error_clone); - 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(); } }