feat(Ui): First stable ui
This commit is contained in:
1 parent
a5e4244ed4
commit
65698c0a2a
3 files changed
+391
-330
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -24,3 +24,20 @@ pub fn stats(app: &RcloneApp) -> Result<Value, String> {
|
||||
serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?;
|
||||
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)
|
||||
}
|
||||
@@ -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<Arc<RcloneApp>>,
|
||||
/// Текущее состояние приложения.
|
||||
state: AppState,
|
||||
/// Глобальное сообщение об ошибке.
|
||||
error_message: Option<String>,
|
||||
|
||||
// Навигация
|
||||
/// Текущий отображаемый путь.
|
||||
current_path: String,
|
||||
/// Список доступных удалённых хранилищ.
|
||||
remote_list: Vec<Remote>,
|
||||
/// Файлы и папки в текущем пути.
|
||||
current_files: Vec<FileInfo>,
|
||||
|
||||
// Выделение
|
||||
/// Выбранные пользователем пути.
|
||||
selected_paths: Vec<String>,
|
||||
|
||||
// Передача
|
||||
/// Исходный путь для операции передачи.
|
||||
transfer_source: String,
|
||||
/// Целевой путь для операции передачи.
|
||||
transfer_dest: String,
|
||||
/// Прогресс текущей передачи.
|
||||
transfer_progress: Option<TransferProgress>,
|
||||
|
||||
// Поиск
|
||||
/// Текущий шаблон поиска.
|
||||
search_pattern: String,
|
||||
/// Результаты последнего поиска.
|
||||
search_results: Vec<FileInfo>,
|
||||
|
||||
// Диалоги UI
|
||||
/// Открыт ли диалог передачи.
|
||||
show_transfer_dialog: bool,
|
||||
/// Открыт ли диалог создания нового хранилища.
|
||||
show_new_remote_dialog: bool,
|
||||
/// Имя нового хранилища.
|
||||
new_remote_name: String,
|
||||
/// Тип нового хранилища (например, "s3", "dropbox").
|
||||
new_remote_type: String,
|
||||
/// Конфигурационные параметры нового хранилища.
|
||||
available_remote_types: Vec<String>,
|
||||
new_remote_config: std::collections::HashMap<String, String>,
|
||||
|
||||
// Настройки
|
||||
/// Настройки пользователя.
|
||||
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)]
|
||||
pub struct AppSettings {
|
||||
/// Показывать скрытые файлы.
|
||||
pub show_hidden: bool,
|
||||
/// Запрашивать подтверждение перед началом передачи.
|
||||
pub confirm_before_transfer: bool,
|
||||
/// Максимальное количество одновременных передач.
|
||||
pub max_concurrent_transfers: u32,
|
||||
/// Ограничение пропускной способности (например, "1M").
|
||||
pub bandwidth_limit: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<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(¤t_path);
|
||||
}
|
||||
self.error_message = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error_message = Some(e);
|
||||
}
|
||||
@@ -206,98 +240,120 @@ impl RcloneUI {
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет операцию копирования из источника в приёмник.
|
||||
///
|
||||
/// Запускается в отдельном потоке, чтобы не блокировать UI.
|
||||
fn perform_copy(&mut self, source: &str, dest: &str) {
|
||||
if let Some(rclone) = &self.rclone {
|
||||
self.state = AppState::Copying;
|
||||
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.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();
|
||||
|
||||
if self.background_working {
|
||||
ui.colored_label(Color32::YELLOW, "⟳ Работа в фоне...");
|
||||
} else {
|
||||
match &self.state {
|
||||
AppState::Ready => {
|
||||
ui.colored_label(Color32::GREEN, "● Готов");
|
||||
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,67 +470,42 @@ 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 {
|
||||
if ui.link(&file.name).clicked() && file.is_dir {
|
||||
let new_path = if current_path.ends_with(':') {
|
||||
format!("{}{}", current_path, file_clone.name)
|
||||
format!("{}{}", current_path, file.name)
|
||||
} else {
|
||||
format!("{}/{}", current_path, file_clone.name)
|
||||
format!("{}/{}", current_path, file.name)
|
||||
};
|
||||
self.load_files(&new_path);
|
||||
}
|
||||
}
|
||||
ui.label(Self::format_size(file_clone.size));
|
||||
if let Some(modified) = &file_clone.modified {
|
||||
ui.label(modified);
|
||||
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() {
|
||||
if current_path.contains('/') && ui.button("📁 .. (Родительская папка)").clicked() {
|
||||
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.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 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(':') {
|
||||
@@ -512,113 +513,85 @@ impl eframe::App for RcloneUI {
|
||||
} else {
|
||||
format!("{}/{}", current_path, file.name)
|
||||
};
|
||||
self.load_files(&new_path);
|
||||
self.pending_load_path = Some(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);
|
||||
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 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 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);
|
||||
|
||||
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("Назначение:");
|
||||
ui.text_edit_singleline(&mut new_transfer_dest);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
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() {
|
||||
let source = transfer_source.clone();
|
||||
let dest = new_transfer_dest.clone();
|
||||
self.transfer_dest = dest.clone();
|
||||
self.perform_copy(&source, &dest);
|
||||
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;
|
||||
}
|
||||
@@ -626,54 +599,38 @@ impl eframe::App for RcloneUI {
|
||||
});
|
||||
}
|
||||
|
||||
// Диалог добавления хранилища
|
||||
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| {
|
||||
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("Имя хранилища:");
|
||||
ui.text_edit_singleline(&mut new_remote_name);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if ui.text_edit_singleline(&mut name).changed() {
|
||||
self.new_remote_name = name;
|
||||
}
|
||||
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();
|
||||
|
||||
egui::ComboBox::from_label("").selected_text(&typ).show_ui(ui, |ui| {
|
||||
for t in &available_types {
|
||||
if ui.selectable_value(&mut typ, t.clone(), t).clicked() {
|
||||
self.new_remote_type = typ.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.label("⚠️ Некоторые типы (Google Drive, Dropbox и др.) откроют браузер для авторизации.");
|
||||
let name_clone = self.new_remote_name.clone();
|
||||
let type_clone = self.new_remote_type.clone();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("✅ Создать").clicked() {
|
||||
if 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();
|
||||
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();
|
||||
}
|
||||
Err(e) => {
|
||||
self.error_message = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("❌ Отмена").clicked() {
|
||||
self.show_new_remote_dialog = false;
|
||||
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 {
|
||||
let error_clone = error.clone();
|
||||
Window::new("Ошибка")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ctx, |ui| {
|
||||
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 {
|
||||
@@ -704,7 +700,6 @@ impl eframe::App for RcloneUI {
|
||||
});
|
||||
}
|
||||
|
||||
// Запрос перерисовки для анимаций
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user