feat(Ui): First stable ui

This commit is contained in:
Black_Raven committed 2026-03-29 13:07:35 +03:00
1 parent a5e4244ed4
commit 65698c0a2a
3 files changed
+391 -330

No files matched your search

+49
View File
@@ -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");
}
}
+17
View File
@@ -24,3 +24,20 @@ pub fn stats(app: &RcloneApp) -> Result<Value, String> {
serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?; serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?;
Ok(stats) Ok(stats)
} }
/// Получить список поддерживаемых бэкендов (типов хранилищ)
pub fn backends(app: &RcloneApp) -> Result<Vec<String>, 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)
}
+325 -330
View File
@@ -1,111 +1,86 @@
//! Графический интерфейс для операций rclone. //! Графический интерфейс для операций rclone.
//!
//! Этот модуль содержит главную структуру `RcloneUI` и её реализацию,
//! включая панели, диалоги, просмотр файлов и управление передачей данных.
use eframe::egui; use eframe::egui;
use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, ProgressBar}; use egui::{CentralPanel, ScrollArea, Window, Align2, Color32};
use std::sync::Arc; use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::rclone_install::RcloneApp; 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)] #[derive(Clone)]
pub enum AppState { pub enum AppState {
/// Приложение инициализируется (проверка наличия rclone).
Initializing, Initializing,
/// Приложение готово и простаивает.
Ready, Ready,
/// Произошла ошибка, хранится текст ошибки.
Error(String), Error(String),
/// Выполняется копирование.
Copying, Copying,
/// Выполняется синхронизация.
Syncing, Syncing,
/// Выполняется перемещение.
Moving, Moving,
/// Выполняется удаление.
Deleting, Deleting,
/// Загрузка файлов или информации о хранилищах.
Loading, Loading,
} }
/// Информация о ходе передачи. /// Информация о ходе передачи.
#[derive(Clone)] #[derive(Clone)]
pub struct TransferProgress { pub struct TransferProgress {
/// Передано байт.
pub current: u64, pub current: u64,
/// Всего байт.
pub total: u64, pub total: u64,
/// Текущая скорость (байт/с).
pub speed: f64, pub speed: f64,
/// Имя текущего передаваемого файла.
pub file_name: String, pub file_name: String,
} }
/// Главная структура UI, содержащая всё состояние и логику. /// Результат длительной операции.
enum OperationResult {
None,
Success(String),
Failure(String),
}
/// Главная структура UI.
pub struct RcloneUI { pub struct RcloneUI {
/// Экземпляр rclone (инициализируется при первом использовании).
rclone: Option<Arc<RcloneApp>>, rclone: Option<Arc<RcloneApp>>,
/// Текущее состояние приложения.
state: AppState, state: AppState,
/// Глобальное сообщение об ошибке.
error_message: Option<String>, error_message: Option<String>,
// Навигация
/// Текущий отображаемый путь.
current_path: String, current_path: String,
/// Список доступных удалённых хранилищ.
remote_list: Vec<Remote>, remote_list: Vec<Remote>,
/// Файлы и папки в текущем пути.
current_files: Vec<FileInfo>, current_files: Vec<FileInfo>,
// Выделение
/// Выбранные пользователем пути.
selected_paths: Vec<String>, selected_paths: Vec<String>,
// Передача
/// Исходный путь для операции передачи.
transfer_source: String, transfer_source: String,
/// Целевой путь для операции передачи.
transfer_dest: String, transfer_dest: String,
/// Прогресс текущей передачи.
transfer_progress: Option<TransferProgress>, transfer_progress: Option<TransferProgress>,
// Поиск
/// Текущий шаблон поиска.
search_pattern: String, search_pattern: String,
/// Результаты последнего поиска.
search_results: Vec<FileInfo>, search_results: Vec<FileInfo>,
// Диалоги UI
/// Открыт ли диалог передачи.
show_transfer_dialog: bool, show_transfer_dialog: bool,
/// Открыт ли диалог создания нового хранилища.
show_new_remote_dialog: bool, show_new_remote_dialog: bool,
/// Имя нового хранилища.
new_remote_name: String, new_remote_name: String,
/// Тип нового хранилища (например, "s3", "dropbox").
new_remote_type: String, new_remote_type: String,
/// Конфигурационные параметры нового хранилища. available_remote_types: Vec<String>,
new_remote_config: std::collections::HashMap<String, String>, new_remote_config: std::collections::HashMap<String, String>,
// Настройки
/// Настройки пользователя.
settings: AppSettings, settings: AppSettings,
background_working: bool,
operation_result: Arc<Mutex<OperationResult>>,
operation_id: u32,
show_delete_remote_dialog: bool,
remote_to_delete: Option<String>,
show_browser_warning: bool,
pending_remote_creation: Option<(String, String)>,
// Отложенные действия
pending_load_path: Option<String>,
pending_search_pattern: Option<String>,
} }
/// Настройки приложения, задаваемые пользователем.
#[derive(Clone)] #[derive(Clone)]
pub struct AppSettings { pub struct AppSettings {
/// Показывать скрытые файлы.
pub show_hidden: bool, pub show_hidden: bool,
/// Запрашивать подтверждение перед началом передачи.
pub confirm_before_transfer: bool, pub confirm_before_transfer: bool,
/// Максимальное количество одновременных передач.
pub max_concurrent_transfers: u32, pub max_concurrent_transfers: u32,
/// Ограничение пропускной способности (например, "1M").
pub bandwidth_limit: Option<String>, pub bandwidth_limit: Option<String>,
} }
@@ -121,9 +96,6 @@ impl Default for AppSettings {
} }
impl RcloneUI { impl RcloneUI {
/// Создаёт новый экземпляр UI.
///
/// Бэкенд rclone инициализируется позже, при первом вызове `update`.
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self { Self {
rclone: None, rclone: None,
@@ -142,23 +114,28 @@ impl RcloneUI {
show_new_remote_dialog: false, show_new_remote_dialog: false,
new_remote_name: String::new(), new_remote_name: String::new(),
new_remote_type: String::new(), new_remote_type: String::new(),
available_remote_types: Vec::new(),
new_remote_config: std::collections::HashMap::new(), new_remote_config: std::collections::HashMap::new(),
settings: AppSettings::default(), 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) { fn init_rclone(&mut self) {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
let rclone_result = rt.block_on(async { let rclone_result = rt.block_on(async { RcloneApp::new().await });
RcloneApp::new().await
});
match rclone_result { match rclone_result {
Ok(rclone) => { Ok(rclone) => {
self.rclone = Some(Arc::new(rclone)); self.rclone = Some(Arc::new(rclone));
self.load_remote_types();
self.load_remotes(); self.load_remotes();
} }
Err(e) => { 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) { fn load_remotes(&mut self) {
if let Some(rclone) = &self.rclone { if let Some(rclone) = &self.rclone {
match operations::remotes::list(rclone) { match operations::remotes::list(rclone) {
@@ -178,18 +168,13 @@ impl RcloneUI {
self.state = AppState::Ready; self.state = AppState::Ready;
} }
Err(e) => { Err(e) => {
let error_msg = e.clone(); self.error_message = Some(e.clone());
self.error_message = Some(e); self.state = AppState::Error(e);
self.state = AppState::Error(error_msg);
} }
} }
} }
} }
/// Загружает содержимое каталога.
///
/// # Аргументы
/// * `path` – путь к удалённому хранилищу (например, "remote:" или "remote:папка/подпапка").
fn load_files(&mut self, path: &str) { fn load_files(&mut self, path: &str) {
if let Some(rclone) = &self.rclone { if let Some(rclone) = &self.rclone {
match operations::files::list(rclone, path) { match operations::files::list(rclone, path) {
@@ -199,6 +184,55 @@ impl RcloneUI {
self.search_results.clear(); self.search_results.clear();
self.search_pattern.clear(); self.search_pattern.clear();
} }
Err(e) => self.error_message = Some(e),
}
}
}
fn start_background_operation<F>(&mut self, f: F) -> u32
where
F: FnOnce() -> Result<String, String> + Send + 'static,
{
self.background_working = true;
self.operation_id = self.operation_id.wrapping_add(1);
let op_id = self.operation_id;
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(&current_path);
}
self.error_message = None;
}
Err(e) => { Err(e) => {
self.error_message = Some(e); self.error_message = Some(e);
} }
@@ -206,98 +240,120 @@ impl RcloneUI {
} }
} }
/// Выполняет операцию копирования из источника в приёмник. fn delete_remote(&mut self, name: &str) {
/// if let Some(rclone) = self.rclone.clone() {
/// Запускается в отдельном потоке, чтобы не блокировать UI. let name = name.to_string();
fn perform_copy(&mut self, source: &str, dest: &str) { self.start_background_operation(move || {
if let Some(rclone) = &self.rclone { operations::remotes::delete(&rclone, &name)
self.state = AppState::Copying; .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.clone() {
self.state = AppState::Copying;
let options = CopyOptions { let options = CopyOptions {
verbose: true, verbose: true,
dry_run: false, dry_run: false,
bandwidth_limit: self.settings.bandwidth_limit.clone(), bandwidth_limit: self.settings.bandwidth_limit.clone(),
no_traverse: false, no_traverse: false,
}; };
let source = source.to_string();
// Копирование в отдельном потоке let dest = dest.to_string();
let rclone_clone = rclone.clone(); self.start_background_operation(move || {
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(); let rt = Runtime::new().unwrap();
rt.block_on(async { 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) { fn search_files(&mut self, pattern: &str) {
if let Some(rclone) = &self.rclone { if let Some(rclone) = &self.rclone {
let options = FindOptions { let options = FindOptions {
recursive: true, recursive: true,
max_results: 100, max_results: 100,
}; };
match operations::search::by_name(rclone, &self.current_path, pattern, &options) { match operations::search::by_name(rclone, &self.current_path, pattern, &options) {
Ok(results) => { Ok(results) => self.search_results = results,
self.search_results = results; Err(e) => self.error_message = Some(e),
}
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 { fn format_size(bytes: u64) -> String {
const KB: u64 = 1024; const KB: u64 = 1024;
const MB: u64 = KB * 1024; const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024; const GB: u64 = MB * 1024;
if bytes >= GB { format!("{:.2} ГБ", bytes as f64 / GB as f64) }
if bytes >= GB { else if bytes >= MB { format!("{:.2} МБ", bytes as f64 / MB as f64) }
format!("{:.2} ГБ", bytes as f64 / GB as f64) else if bytes >= KB { format!("{:.2} КБ", bytes as f64 / KB as f64) }
} else if bytes >= MB { else { format!("{} Б", bytes) }
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 { fn format_speed(speed: f64) -> String {
if speed < 1024.0 { if speed < 1024.0 { format!("{:.0} Б/с", speed) }
format!("{:.0} Б/с", speed) else if speed < 1024.0 * 1024.0 { format!("{:.1} КБ/с", speed / 1024.0) }
} else if speed < 1024.0 * 1024.0 { else if speed < 1024.0 * 1024.0 * 1024.0 { format!("{:.1} МБ/с", speed / (1024.0 * 1024.0)) }
format!("{:.1} КБ/с", speed / 1024.0) else { format!("{:.2} ГБ/с", speed / (1024.0 * 1024.0 * 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 { impl eframe::App for RcloneUI {
/// Вызывается каждый кадр для обновления интерфейса.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 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() { if matches!(self.state, AppState::Initializing) && self.rclone.is_none() {
self.init_rclone(); self.init_rclone();
} }
@@ -306,11 +362,8 @@ impl eframe::App for RcloneUI {
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("Файл", |ui| { ui.menu_button("Файл", |ui| {
if ui.button("Выход").clicked() { if ui.button("Выход").clicked() { std::process::exit(0); }
std::process::exit(0);
}
}); });
ui.menu_button("Хранилища", |ui| { ui.menu_button("Хранилища", |ui| {
if ui.button("Добавить новое хранилище").clicked() { if ui.button("Добавить новое хранилище").clicked() {
self.show_new_remote_dialog = true; self.show_new_remote_dialog = true;
@@ -319,45 +372,29 @@ impl eframe::App for RcloneUI {
self.load_remotes(); self.load_remotes();
} }
}); });
ui.menu_button("Настройки", |ui| { ui.menu_button("Настройки", |ui| {
ui.checkbox(&mut self.settings.show_hidden, "Показывать скрытые файлы"); ui.checkbox(&mut self.settings.show_hidden, "Показывать скрытые файлы");
ui.checkbox(&mut self.settings.confirm_before_transfer, "Подтверждать перед передачей"); ui.checkbox(&mut self.settings.confirm_before_transfer, "Подтверждать перед передачей");
ui.add(egui::Slider::new(&mut self.settings.max_concurrent_transfers, 1..=10).text("Макс. одновременных передач")); ui.add(egui::Slider::new(&mut self.settings.max_concurrent_transfers, 1..=10).text("Макс. одновременных передач"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Ограничение скорости:"); 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() { if ui.text_edit_singleline(&mut limit_str).changed() {
if limit_str.is_empty() { self.settings.bandwidth_limit = if limit_str.is_empty() { None } else { Some(limit_str) };
self.settings.bandwidth_limit = None;
} else {
self.settings.bandwidth_limit = Some(limit_str);
}
} }
}); });
}); });
ui.separator(); ui.separator();
if self.background_working {
ui.colored_label(Color32::YELLOW, "⟳ Работа в фоне...");
} else {
match &self.state { match &self.state {
AppState::Ready => { AppState::Ready => { ui.colored_label(Color32::GREEN, "● Готов"); }
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 { if let Some(rclone) = &self.rclone {
ui.separator(); ui.separator();
ui.label(format!("rclone: {}", rclone.get_rclone_path().display())); ui.label(format!("rclone: {}", rclone.get_rclone_path().display()));
@@ -365,78 +402,67 @@ impl eframe::App for RcloneUI {
}); });
}); });
// Левая панель — список хранилищ // Левая панель со списком хранилищ
egui::SidePanel::left("remotes_panel") egui::SidePanel::left("remotes_panel")
.default_width(200.0) .default_width(220.0)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.heading("Хранилища"); ui.heading("Хранилища");
ui.separator(); ui.separator();
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
let remotes = self.remote_list.clone(); let remotes = self.remote_list.clone();
if remotes.is_empty() && matches!(self.state, AppState::Ready) { if remotes.is_empty() && matches!(self.state, AppState::Ready) {
ui.colored_label(Color32::GRAY, "Хранилища не найдены"); ui.colored_label(Color32::GRAY, "Хранилища не найдены");
ui.add_space(5.0);
if ui.button("➕ Добавить хранилище").clicked() { if ui.button("➕ Добавить хранилище").clicked() {
self.show_new_remote_dialog = true; self.show_new_remote_dialog = true;
} }
} else { } else {
for remote in &remotes { for remote in &remotes {
let button_text = format!("📡 {}", remote.name); ui.horizontal(|ui| {
if ui.button(button_text).clicked() { if ui.button(format!("📡 {}", remote.name)).clicked() {
let path = format!("{}:", remote.name); self.pending_load_path = Some(format!("{}:", remote.name));
self.load_files(&path);
} }
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); ui.add_space(2.0);
} }
} }
}); });
}); });
// Центральная панель — основное содержимое // Центральная панель
CentralPanel::default().show(ctx, |ui| { 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.horizontal(|ui| {
ui.label("📍 Путь:"); ui.label("📍 Путь:");
let path_text = if self.current_path.is_empty() { ui.label(if current_path.is_empty() { "Выберите хранилище" } else { &current_path });
"Выберите хранилище из левой панели".to_string()
} else {
self.current_path.clone()
};
ui.label(path_text);
ui.separator(); ui.separator();
ui.label("🔍 Поиск:"); ui.label("🔍 Поиск:");
let mut new_search = self.search_pattern.clone();
if ui.text_edit_singleline(&mut new_search).changed() { if ui.text_edit_singleline(&mut new_search).changed() {
self.search_pattern = new_search.clone(); self.search_pattern = new_search.clone();
if !new_search.is_empty() && !self.current_path.is_empty() { if !self.search_pattern.is_empty() && !self.current_path.is_empty() {
self.search_files(&new_search); self.pending_search_pattern = Some(self.search_pattern.clone());
} else { } else {
self.search_results.clear(); self.search_results.clear();
} }
} }
if ui.button("🔄 Обновить").clicked() && !self.current_path.is_empty() {
let current_path_clone = self.current_path.clone(); self.pending_load_path = Some(self.current_path.clone());
if ui.button("🔄 Обновить").clicked() {
if !current_path_clone.is_empty() {
self.load_files(&current_path_clone);
}
} }
}); });
ui.separator(); ui.separator();
// Состояние загрузки if self.background_working {
if matches!(self.state, AppState::Loading) || matches!(self.state, AppState::Initializing) {
ui.centered_and_justified(|ui| { ui.centered_and_justified(|ui| {
ui.label("Загрузка..."); ui.colored_label(Color32::YELLOW, "⟳ Выполняется операция... Пожалуйста, подождите.");
}); });
return; return;
} }
// Состояние ошибки
if let AppState::Error(msg) = &self.state { if let AppState::Error(msg) = &self.state {
ui.centered_and_justified(|ui| { ui.centered_and_justified(|ui| {
ui.colored_label(Color32::RED, format!("Ошибка: {}", msg)); ui.colored_label(Color32::RED, format!("Ошибка: {}", msg));
@@ -444,67 +470,42 @@ impl eframe::App for RcloneUI {
return; return;
} }
// Результаты поиска или список файлов
if !self.search_results.is_empty() { if !self.search_results.is_empty() {
let search_results = self.search_results.clone(); let search_results = self.search_results.clone();
let current_path = self.current_path.clone(); let current_path = self.current_path.clone();
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
ui.heading("Результаты поиска"); ui.heading("Результаты поиска");
ui.separator();
for file in &search_results { for file in &search_results {
let file_clone = file.clone();
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(file.icon()); ui.label(file.icon());
if ui.link(&file.name).clicked() { if ui.link(&file.name).clicked() && file.is_dir {
if file_clone.is_dir {
let new_path = if current_path.ends_with(':') { let new_path = if current_path.ends_with(':') {
format!("{}{}", current_path, file_clone.name) format!("{}{}", current_path, file.name)
} else { } else {
format!("{}/{}", current_path, file_clone.name) format!("{}/{}", current_path, file.name)
}; };
self.load_files(&new_path); self.pending_load_path = Some(new_path);
}
}
ui.label(Self::format_size(file_clone.size));
if let Some(modified) = &file_clone.modified {
ui.label(modified);
} }
ui.label(Self::format_size(file.size));
}); });
} }
}); });
} else if !self.current_path.is_empty() { } else if !self.current_path.is_empty() {
// Файловый менеджер
let current_files = self.current_files.clone(); let current_files = self.current_files.clone();
let current_path = self.current_path.clone(); let current_path = self.current_path.clone();
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
// Кнопка "Наверх" if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() {
if current_path.contains('/') {
if ui.button("📁 .. (Родительская папка)").clicked() {
let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); let parent = current_path.rsplit_once('/').map(|(p, _)| p).unwrap_or("");
self.load_files(parent); self.pending_load_path = Some(parent.to_string());
} }
ui.add_space(5.0);
}
ui.heading("Файлы и папки"); ui.heading("Файлы и папки");
ui.separator();
if current_files.is_empty() {
ui.colored_label(Color32::GRAY, "Папка пуста");
} else {
for file in &current_files { for file in &current_files {
let file_clone = file.clone();
let selected = self.selected_paths.contains(&file.path); let selected = self.selected_paths.contains(&file.path);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.selectable_label(selected, file.icon()).clicked() { if ui.selectable_label(selected, file.icon()).clicked() {
if selected { if selected { self.selected_paths.retain(|p| p != &file.path); }
self.selected_paths.retain(|p| p != &file_clone.path); else { self.selected_paths.push(file.path.clone()); }
} else {
self.selected_paths.push(file_clone.path.clone());
} }
}
if file.is_dir { if file.is_dir {
if ui.link(&file.name).clicked() { if ui.link(&file.name).clicked() {
let new_path = if current_path.ends_with(':') { let new_path = if current_path.ends_with(':') {
@@ -512,113 +513,85 @@ impl eframe::App for RcloneUI {
} else { } else {
format!("{}/{}", current_path, file.name) format!("{}/{}", current_path, file.name)
}; };
self.load_files(&new_path); self.pending_load_path = Some(new_path);
} }
} else { } else {
ui.label(&file.name); ui.label(&file.name);
} }
ui.label(Self::format_size(file.size)); ui.label(Self::format_size(file.size));
if let Some(modified) = &file.modified {
ui.label(modified);
}
}); });
} }
}
}); });
} else { } else {
// Приветственный экран
ui.centered_and_justified(|ui| { ui.centered_and_justified(|ui| {
ui.vertical(|ui| {
ui.add_space(50.0);
ui.heading("Добро пожаловать в Rclone UI Manager"); ui.heading("Добро пожаловать в Rclone UI Manager");
ui.add_space(20.0); ui.label("Выберите хранилище из левой панели или добавьте новое");
ui.label("Выберите хранилище из левой панели, чтобы начать");
ui.add_space(10.0);
ui.label("Или добавьте новое хранилище через меню «Хранилища»");
ui.add_space(30.0);
if ui.button("➕ Добавить новое хранилище").clicked() { if ui.button("➕ Добавить новое хранилище").clicked() {
self.show_new_remote_dialog = true; self.show_new_remote_dialog = true;
} }
}); });
});
} }
ui.separator(); ui.separator();
// Кнопки действий
if !self.selected_paths.is_empty() { if !self.selected_paths.is_empty() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("Выбрано: {} элемент(ов)", self.selected_paths.len())); ui.label(format!("Выбрано: {}", self.selected_paths.len()));
ui.separator();
if ui.button("📋 Копировать").clicked() { if ui.button("📋 Копировать").clicked() {
self.transfer_source = self.selected_paths[0].clone(); self.transfer_source = self.selected_paths[0].clone();
self.show_transfer_dialog = true; self.show_transfer_dialog = true;
} }
if ui.button("✂️ Переместить").clicked() { if ui.button("✂️ Переместить").clicked() {
self.transfer_source = self.selected_paths[0].clone(); self.transfer_source = self.selected_paths[0].clone();
self.show_transfer_dialog = true; self.show_transfer_dialog = true;
} }
if ui.button("🗑️ Удалить").clicked() { if ui.button("🗑️ Удалить").clicked() {
if self.settings.confirm_before_transfer { if let Some(rclone) = self.rclone.clone() {
self.show_transfer_dialog = true; 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 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 { if self.show_transfer_dialog {
let transfer_source = self.transfer_source.clone(); let source = self.transfer_source.clone();
let transfer_dest = self.transfer_dest.clone(); let 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(format!("Источник: {}", source));
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.label("Назначение:");
ui.text_edit_singleline(&mut new_transfer_dest); let mut dest_edit = dest.clone();
if ui.text_edit_singleline(&mut dest_edit).changed() {
ui.add_space(10.0); self.transfer_dest = dest_edit;
}
ui.separator(); ui.separator();
let current_dest = self.transfer_dest.clone();
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("✅ Начать передачу").clicked() { if ui.button("✅ Копировать").clicked() {
let source = transfer_source.clone(); self.perform_copy(&source, &current_dest);
let dest = new_transfer_dest.clone(); self.show_transfer_dialog = false;
self.transfer_dest = dest.clone(); self.selected_paths.clear();
self.perform_copy(&source, &dest); }
if ui.button("✂️ Переместить").clicked() {
self.perform_move(&source, &current_dest);
self.show_transfer_dialog = false; self.show_transfer_dialog = false;
self.selected_paths.clear(); self.selected_paths.clear();
} }
if ui.button("❌ Отмена").clicked() { if ui.button("❌ Отмена").clicked() {
self.show_transfer_dialog = false; self.show_transfer_dialog = false;
} }
@@ -626,54 +599,38 @@ impl eframe::App for RcloneUI {
}); });
} }
// Диалог добавления хранилища
if self.show_new_remote_dialog { if self.show_new_remote_dialog {
let mut new_remote_name = self.new_remote_name.clone(); let mut name = self.new_remote_name.clone();
let mut new_remote_type = self.new_remote_type.clone(); let mut typ = self.new_remote_type.clone();
Window::new("Добавление хранилища") let available_types = self.available_remote_types.clone();
.collapsible(false) Window::new("Добавление хранилища").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
.resizable(false)
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.label("Имя хранилища:"); ui.label("Имя хранилища:");
ui.text_edit_singleline(&mut new_remote_name); if ui.text_edit_singleline(&mut name).changed() {
self.new_remote_name = name;
ui.add_space(5.0); }
ui.label("Тип хранилища:"); ui.label("Тип хранилища:");
ui.text_edit_singleline(&mut new_remote_type); egui::ComboBox::from_label("").selected_text(&typ).show_ui(ui, |ui| {
for t in &available_types {
ui.add_space(10.0); if ui.selectable_value(&mut typ, t.clone(), t).clicked() {
self.new_remote_type = typ.clone();
ui.label("Распространённые типы: s3, dropbox, google drive, local"); }
ui.add_space(10.0); }
});
ui.separator(); ui.label("⚠️ Некоторые типы (Google Drive, Dropbox и др.) откроют браузер для авторизации.");
let name_clone = self.new_remote_name.clone();
let type_clone = self.new_remote_type.clone();
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("✅ Создать").clicked() { if ui.button("✅ Создать").clicked() {
if let Some(rclone) = &self.rclone { if !Self::validate_remote_name(&name_clone) {
let config = std::collections::HashMap::new(); self.error_message = Some("Недопустимое имя хранилища".to_string());
match operations::remotes::create( } else if type_clone.is_empty() {
rclone, self.error_message = Some("Выберите тип".to_string());
&new_remote_name, } else {
&new_remote_type, self.create_remote_with_warning(&name_clone, &type_clone);
&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.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() { if ui.button("❌ Отмена").clicked() {
self.show_new_remote_dialog = false; self.show_new_remote_dialog = false;
self.new_remote_name.clear(); self.new_remote_name.clear();
@@ -683,18 +640,57 @@ 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);
}
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 { if let Some(error) = &self.error_message {
let error_clone = error.clone(); let error_clone = error.clone();
Window::new("Ошибка") Window::new("Ошибка").collapsible(false).resizable(false).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
.collapsible(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.colored_label(Color32::RED, "⚠️ Произошла ошибка:"); ui.colored_label(Color32::RED, "⚠️ Произошла ошибка:");
ui.add_space(5.0);
ui.label(&error_clone); ui.label(&error_clone);
ui.add_space(10.0);
if ui.button("OK").clicked() { if ui.button("OK").clicked() {
self.error_message = None; self.error_message = None;
if let AppState::Error(_) = &self.state { if let AppState::Error(_) = &self.state {
@@ -704,7 +700,6 @@ impl eframe::App for RcloneUI {
}); });
} }
// Запрос перерисовки для анимаций
ctx.request_repaint(); ctx.request_repaint();
} }
} }