feat(Ui): restrict UI by models
This commit is contained in:
1 parent
e77f051149
commit
25080e3cca
5 files changed
+818
-463
No files matched your search
Generated
+32
@@ -11,8 +11,10 @@ dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"flate2",
|
||||
"futures",
|
||||
"log",
|
||||
"mockito",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
@@ -1362,6 +1364,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.27.2"
|
||||
@@ -2009,6 +2024,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
@@ -2278,6 +2299,17 @@ version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
|
||||
@@ -22,6 +22,10 @@ eframe = { version = "0.27", features = ["default"] }
|
||||
egui = "0.27"
|
||||
egui_extras = { version = "0.27" }
|
||||
|
||||
# Логирование
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8"
|
||||
serial_test = "2.0"
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
//! Графический интерфейс для операций rclone с интегрированным Local FS и поддержкой кастомных шрифтов.
|
||||
|
||||
use crate::operations::{
|
||||
self, CopyOptions, DeleteOptions, FileInfo, MoveOptions, Remote,
|
||||
};
|
||||
use crate::operations::{FileInfo, Remote};
|
||||
use crate::rclone_install::RcloneApp;
|
||||
use eframe::egui;
|
||||
use egui::{
|
||||
Align2, CentralPanel, Color32, ProgressBar, ScrollArea, SidePanel, Spinner, Window,
|
||||
FontDefinitions, FontData, FontFamily,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::runtime::Runtime;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum AppState {
|
||||
@@ -82,458 +72,11 @@ pub struct RcloneUI {
|
||||
next_op_id: u32,
|
||||
}
|
||||
|
||||
pub mod app;
|
||||
pub mod rcloneui;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AppSettings {
|
||||
pub show_hidden: bool,
|
||||
pub bandwidth_limit: Option<String>,
|
||||
}
|
||||
|
||||
impl RcloneUI {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// --- ВОССТАНОВЛЕНИЕ ШРИФТОВ ---
|
||||
let mut fonts = FontDefinitions::default();
|
||||
|
||||
if let Ok(font_data) = std::fs::read("assets/fonts/Sans.otf") {
|
||||
fonts.font_data.insert("sans".to_owned(), FontData::from_owned(font_data));
|
||||
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "sans".to_owned());
|
||||
fonts.families.entry(FontFamily::Monospace).or_default().push("sans".to_owned());
|
||||
}
|
||||
|
||||
if let Ok(emoji_data) = std::fs::read("assets/fonts/NotoColorEmoji.ttf") {
|
||||
fonts.font_data.insert("emoji".to_owned(), FontData::from_owned(emoji_data));
|
||||
let prop = fonts.families.entry(FontFamily::Proportional).or_default();
|
||||
if !prop.contains(&"emoji".to_owned()) {
|
||||
prop.push("emoji".to_owned());
|
||||
}
|
||||
}
|
||||
cc.egui_ctx.set_fonts(fonts);
|
||||
|
||||
let (init_tx, init_rx) = channel();
|
||||
let (op_tx, op_rx) = channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let result = rt.block_on(async { RcloneApp::new().await });
|
||||
let res = match result {
|
||||
Ok(app) => Ok(Arc::new(app)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
};
|
||||
let _ = init_tx.send(res);
|
||||
});
|
||||
|
||||
Self {
|
||||
rclone: None,
|
||||
state: AppState::Initializing,
|
||||
error_message: None,
|
||||
current_path: String::new(),
|
||||
history_back: Vec::new(),
|
||||
history_forward: Vec::new(),
|
||||
remote_list: Vec::new(),
|
||||
current_files: Vec::new(),
|
||||
selected_paths: Vec::new(),
|
||||
transfer_source_info: Vec::new(),
|
||||
transfer_dest: String::new(),
|
||||
active_transfer_tab: TransferTab::Remote,
|
||||
show_transfer_dialog: false,
|
||||
show_local_browser: false,
|
||||
local_browser_path: std::env::var("HOME").unwrap_or_else(|_| "/".into()),
|
||||
local_browser_files: Vec::new(),
|
||||
is_move_mode: false,
|
||||
settings: AppSettings::default(),
|
||||
active_task_count: 0,
|
||||
operation_tx: op_tx,
|
||||
operation_rx: op_rx,
|
||||
pending_load_path: None,
|
||||
rclone_init_receiver: Some(init_rx),
|
||||
active_operations: Vec::new(),
|
||||
next_op_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Валидация ---
|
||||
fn is_path_valid(&self) -> bool {
|
||||
if self.transfer_dest.is_empty() { return false; }
|
||||
match self.active_transfer_tab {
|
||||
TransferTab::Remote => self.transfer_dest.contains(':'),
|
||||
TransferTab::Local => !self.transfer_dest.contains(':') || Path::new(&self.transfer_dest).is_absolute(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Навигация ---
|
||||
fn navigate_to(&mut self, new_path: String) {
|
||||
if self.current_path == new_path { return; }
|
||||
if !self.current_path.is_empty() { self.history_back.push(self.current_path.clone()); }
|
||||
self.history_forward.clear();
|
||||
self.pending_load_path = Some(new_path);
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Some(prev) = self.history_back.pop() {
|
||||
self.history_forward.push(self.current_path.clone());
|
||||
self.pending_load_path = Some(prev);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Some(next) = self.history_forward.pop() {
|
||||
self.history_back.push(self.current_path.clone());
|
||||
self.pending_load_path = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_local_list(&mut self) {
|
||||
if let Ok(files) = operations::local_fs::list_directory(&self.local_browser_path) {
|
||||
self.local_browser_files = files;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Работа с Rclone ---
|
||||
fn poll_rclone_init(&mut self) {
|
||||
if let Some(rx) = &self.rclone_init_receiver {
|
||||
if let Ok(res) = rx.try_recv() {
|
||||
match res {
|
||||
Ok(rclone) => {
|
||||
self.rclone = Some(rclone);
|
||||
self.load_remotes();
|
||||
self.state = AppState::Ready;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error_message = Some(e.clone());
|
||||
self.state = AppState::Error(e);
|
||||
}
|
||||
}
|
||||
self.rclone_init_receiver = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_remotes(&mut self) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(match operations::remotes::list(&rclone) {
|
||||
Ok(list) => OperationResult::RemoteList(list),
|
||||
Err(e) => OperationResult::Failure(0, e.to_string()),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn load_files(&mut self, path: &str) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
self.active_task_count += 1;
|
||||
let path = path.to_string();
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(match operations::files::list(&rclone, &path) {
|
||||
Ok(list) => OperationResult::FileList(list),
|
||||
Err(e) => OperationResult::Failure(0, e.to_string()),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_background_operation(&mut self) {
|
||||
while let Ok(result) = self.operation_rx.try_recv() {
|
||||
match result {
|
||||
OperationResult::Success(op_id, _) => {
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.pending_load_path = Some(self.current_path.clone());
|
||||
}
|
||||
OperationResult::Failure(op_id, e) => {
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.error_message = Some(e);
|
||||
}
|
||||
OperationResult::FileList(files) => {
|
||||
self.current_files = files;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
}
|
||||
OperationResult::RemoteList(remotes) => {
|
||||
self.remote_list = remotes;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
}
|
||||
OperationResult::ProgressUpdate(op_id, progress, status) => {
|
||||
if let Some(op) = self.active_operations.iter_mut().find(|op| op.id == op_id) {
|
||||
op.progress = progress;
|
||||
op.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_transfer(&mut self, is_move: bool) {
|
||||
if self.active_task_count > 0 { return; }
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
let sources = self.transfer_source_info.clone();
|
||||
let dest_base = self.transfer_dest.clone();
|
||||
let op_id = self.add_operation(format!("{} {} эл.", if is_move { "Перенос" } else { "Копия" }, sources.len()));
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let total = sources.len() as f32;
|
||||
for (idx, (source, is_dir)) in sources.iter().enumerate() {
|
||||
let name = source.split([':', '/', '\\']).last().unwrap_or("");
|
||||
let separator = if dest_base.contains(':') && !dest_base.ends_with(':') && !dest_base.ends_with('/') { "/" } else { "" };
|
||||
|
||||
let final_dest = if *is_dir {
|
||||
format!("{}{}{}", dest_base.trim_end_matches(['/', '\\']), separator, name)
|
||||
} else {
|
||||
dest_base.clone()
|
||||
};
|
||||
|
||||
let res = if is_move {
|
||||
operations::sync::move_files(&rclone, source, &final_dest, &MoveOptions { verbose: true, ..Default::default() })
|
||||
} else {
|
||||
operations::sync::copy(&rclone, source, &final_dest, &CopyOptions { verbose: true, ..Default::default() })
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка на {}: {}", name, e)));
|
||||
return;
|
||||
}
|
||||
let _ = tx.send(OperationResult::ProgressUpdate(op_id, (idx + 1) as f32 / total, format!("{}/{}", idx+1, total)));
|
||||
}
|
||||
let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_selected(&mut self) {
|
||||
if self.active_task_count > 0 { return; }
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
let paths = self.get_selected_info();
|
||||
let op_id = self.add_operation(format!("Удаление {} эл.", paths.len()));
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
for (p, is_dir) in paths.iter() {
|
||||
let opts = DeleteOptions { recursive: *is_dir, ..Default::default() };
|
||||
if let Err(e) = operations::sync::delete(&rclone, p, &opts) {
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e.to_string())); return;
|
||||
}
|
||||
}
|
||||
let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn add_operation(&mut self, description: String) -> u32 {
|
||||
let id = self.next_op_id;
|
||||
self.next_op_id += 1;
|
||||
self.active_operations.push(Operation { id, description, progress: 0.0, status: "В очереди".into(), start_time: Instant::now() });
|
||||
id
|
||||
}
|
||||
|
||||
fn get_selected_info(&self) -> Vec<(String, bool)> {
|
||||
self.selected_paths.iter().map(|path| {
|
||||
let is_dir = self.current_files.iter().find(|f| {
|
||||
let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, f.name) } else { format!("{}/{}", self.current_path, f.name) };
|
||||
&full == path
|
||||
}).map(|f| f.is_dir).unwrap_or(false);
|
||||
(path.clone(), is_dir)
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for RcloneUI {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_rclone_init();
|
||||
self.poll_background_operation();
|
||||
|
||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.label("🚀 Rclone GUI");
|
||||
ui.separator();
|
||||
ui.add_enabled_ui(!self.history_back.is_empty(), |ui| {
|
||||
if ui.button("⬅").clicked() { self.go_back(); }
|
||||
});
|
||||
ui.add_enabled_ui(!self.history_forward.is_empty(), |ui| {
|
||||
if ui.button("➡").clicked() { self.go_forward(); }
|
||||
});
|
||||
if self.active_task_count > 0 { ui.add(Spinner::new().size(16.0)); }
|
||||
});
|
||||
});
|
||||
|
||||
SidePanel::left("left").default_width(180.0).show(ctx, |ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Хранилища");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for remote in self.remote_list.clone() {
|
||||
if ui.selectable_label(self.current_path.starts_with(&remote.name), format!("📡 {}", remote.name)).clicked() {
|
||||
self.navigate_to(format!("{}:", remote.name));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
SidePanel::right("right").default_width(200.0).show(ctx, |ui| {
|
||||
ui.heading("Задачи");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for op in &self.active_operations {
|
||||
ui.group(|ui| {
|
||||
ui.label(&op.description);
|
||||
ui.add(ProgressBar::new(op.progress).text(format!("{:.0}%", op.progress * 100.0)));
|
||||
ui.small(&op.status);
|
||||
});
|
||||
}
|
||||
if self.active_operations.is_empty() {
|
||||
ui.weak("Нет активных задач");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("📍 {}", self.current_path));
|
||||
ui.add_enabled_ui(self.active_task_count == 0, |ui| {
|
||||
if ui.button("🔄").clicked() { self.pending_load_path = Some(self.current_path.clone()); }
|
||||
});
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
ScrollArea::vertical().id_source("files").show(ui, |ui| {
|
||||
if !self.current_path.is_empty() {
|
||||
if self.current_path.contains('/') && ui.button("📁 .. (Вверх)").clicked() {
|
||||
if let Some((p, _)) = self.current_path.rsplit_once('/') { self.navigate_to(p.into()); }
|
||||
else if let Some((p, _)) = self.current_path.rsplit_once(':') { self.navigate_to(format!("{}:", p)); }
|
||||
}
|
||||
|
||||
for file in self.current_files.clone() {
|
||||
let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, file.name) } else { format!("{}/{}", self.current_path, file.name) };
|
||||
let sel = self.selected_paths.contains(&full);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(sel, file.icon()).clicked() {
|
||||
if sel { self.selected_paths.retain(|p| p != &full); }
|
||||
else { self.selected_paths.push(full.clone()); }
|
||||
}
|
||||
if file.is_dir {
|
||||
if ui.link(&file.name).clicked() { self.navigate_to(full); }
|
||||
} else { ui.label(&file.name); }
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.small(file.format_size());
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ui.centered_and_justified(|ui| { ui.label("Выберите хранилище слева"); });
|
||||
}
|
||||
});
|
||||
|
||||
if !self.selected_paths.is_empty() {
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Выбрано: {}", self.selected_paths.len()));
|
||||
ui.add_enabled_ui(self.active_task_count == 0, |ui| {
|
||||
if ui.button("📋 Копировать").clicked() {
|
||||
self.transfer_source_info = self.get_selected_info();
|
||||
self.is_move_mode = false;
|
||||
self.show_transfer_dialog = true;
|
||||
}
|
||||
if ui.button("✂ Переместить").clicked() {
|
||||
self.transfer_source_info = self.get_selected_info();
|
||||
self.is_move_mode = true;
|
||||
self.show_transfer_dialog = true;
|
||||
}
|
||||
if ui.button("🗑 Удалить").clicked() {
|
||||
self.delete_selected();
|
||||
self.selected_paths.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Модальное окно трансфера
|
||||
if self.show_transfer_dialog {
|
||||
Window::new("Трансфер").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).collapsible(false).show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.active_transfer_tab, TransferTab::Remote, "☁ Облако");
|
||||
ui.selectable_value(&mut self.active_transfer_tab, TransferTab::Local, "💻 ПК");
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut self.transfer_dest).hint_text("Путь назначения..."));
|
||||
if self.active_transfer_tab == TransferTab::Local && ui.button("📂 Обзор").clicked() {
|
||||
self.refresh_local_list();
|
||||
self.show_local_browser = true;
|
||||
}
|
||||
});
|
||||
|
||||
if !self.transfer_dest.is_empty() && !self.is_path_valid() {
|
||||
ui.colored_label(Color32::KHAKI,
|
||||
if self.active_transfer_tab == TransferTab::Remote { "⚠ Путь облака должен содержать ':'" }
|
||||
else { "⚠ Укажите локальный путь без ':'" });
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
let can_start = self.is_path_valid() && self.active_task_count == 0;
|
||||
if ui.add_enabled(can_start, egui::Button::new("🚀 Начать")).clicked() {
|
||||
self.perform_transfer(self.is_move_mode);
|
||||
self.show_transfer_dialog = false;
|
||||
}
|
||||
if ui.button("Отмена").clicked() { self.show_transfer_dialog = false; }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Внутренний браузер папок
|
||||
if self.show_local_browser {
|
||||
Window::new("Выбор локальной папки").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).fixed_size([400.0, 300.0]).show(ctx, |ui| {
|
||||
ui.label(format!("📍 {}", self.local_browser_path));
|
||||
if ui.button("⬅ Наверх").clicked() {
|
||||
if let Some(parent) = Path::new(&self.local_browser_path).parent() {
|
||||
self.local_browser_path = parent.to_string_lossy().into();
|
||||
self.refresh_local_list();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
||||
let files = self.local_browser_files.clone();
|
||||
for file in files {
|
||||
if file.is_dir && ui.button(format!("📁 {}", file.name)).clicked() {
|
||||
self.local_browser_path = file.path;
|
||||
self.refresh_local_list();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("✅ Выбрать").clicked() {
|
||||
self.transfer_dest = self.local_browser_path.clone();
|
||||
self.show_local_browser = false;
|
||||
}
|
||||
if ui.button("Отмена").clicked() { self.show_local_browser = false; }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(msg) = &self.error_message {
|
||||
let mut close = false;
|
||||
Window::new("Внимание").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
|
||||
ui.label(msg);
|
||||
if ui.button("ОК").clicked() { close = true; }
|
||||
});
|
||||
if close { self.error_message = None; }
|
||||
}
|
||||
|
||||
if let Some(path) = self.pending_load_path.take() {
|
||||
self.current_path = path.clone();
|
||||
self.selected_paths.clear();
|
||||
self.load_files(&path);
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
use super::*;
|
||||
use eframe::egui;
|
||||
use egui::{
|
||||
Align2, CentralPanel, Color32, ProgressBar, ScrollArea,
|
||||
SidePanel, Spinner, Window,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
impl eframe::App for RcloneUI {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_rclone_init();
|
||||
self.poll_background_operation();
|
||||
|
||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.label("🚀 Rclone GUI");
|
||||
ui.separator();
|
||||
ui.add_enabled_ui(!self.history_back.is_empty(), |ui| {
|
||||
if ui.button("⬅").clicked() {
|
||||
log::debug!("Кнопка 'Назад' нажата");
|
||||
self.go_back();
|
||||
}
|
||||
});
|
||||
ui.add_enabled_ui(!self.history_forward.is_empty(), |ui| {
|
||||
if ui.button("➡").clicked() {
|
||||
log::debug!("Кнопка 'Вперед' нажата");
|
||||
self.go_forward();
|
||||
}
|
||||
});
|
||||
if self.active_task_count > 0 {
|
||||
ui.add(Spinner::new().size(16.0));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
SidePanel::left("left")
|
||||
.default_width(180.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Хранилища");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for remote in self.remote_list.clone() {
|
||||
if ui
|
||||
.selectable_label(
|
||||
self.current_path.starts_with(&remote.name),
|
||||
format!("📡 {}", remote.name),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
log::debug!("Выбрано удаленное хранилище: {}", remote.name);
|
||||
self.navigate_to(format!("{}:", remote.name));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
SidePanel::right("right")
|
||||
.default_width(200.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Задачи");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for op in &self.active_operations {
|
||||
ui.group(|ui| {
|
||||
ui.label(&op.description);
|
||||
ui.add(
|
||||
ProgressBar::new(op.progress)
|
||||
.text(format!("{:.0}%", op.progress * 100.0)),
|
||||
);
|
||||
ui.small(&op.status);
|
||||
});
|
||||
}
|
||||
if self.active_operations.is_empty() {
|
||||
ui.weak("Нет активных задач");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("📍 {}", self.current_path));
|
||||
ui.add_enabled_ui(self.active_task_count == 0, |ui| {
|
||||
if ui.button("🔄").clicked() {
|
||||
log::debug!("Обновление текущего пути: {}", self.current_path);
|
||||
self.pending_load_path = Some(self.current_path.clone());
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
ScrollArea::vertical().id_source("files").show(ui, |ui| {
|
||||
if !self.current_path.is_empty() {
|
||||
if self.current_path.contains('/') && ui.button("📁 .. (Вверх)").clicked()
|
||||
{
|
||||
log::debug!("Переход вверх по директории");
|
||||
if let Some((p, _)) = self.current_path.rsplit_once('/') {
|
||||
self.navigate_to(p.into());
|
||||
} else if let Some((p, _)) = self.current_path.rsplit_once(':') {
|
||||
self.navigate_to(format!("{}:", p));
|
||||
}
|
||||
}
|
||||
|
||||
for file in self.current_files.clone() {
|
||||
let full = if self.current_path.ends_with(':') {
|
||||
format!("{}{}", self.current_path, file.name)
|
||||
} else {
|
||||
format!("{}/{}", self.current_path, file.name)
|
||||
};
|
||||
let sel = self.selected_paths.contains(&full);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(sel, file.icon()).clicked() {
|
||||
if sel {
|
||||
log::debug!("Снят выбор с {}", full);
|
||||
self.selected_paths.retain(|p| p != &full);
|
||||
} else {
|
||||
log::debug!("Выбран {}", full);
|
||||
self.selected_paths.push(full.clone());
|
||||
}
|
||||
}
|
||||
if file.is_dir {
|
||||
if ui.link(&file.name).clicked() {
|
||||
log::debug!("Открыта директория: {}", full);
|
||||
self.navigate_to(full);
|
||||
}
|
||||
} else {
|
||||
ui.label(&file.name);
|
||||
}
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.small(file.format_size());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("Выберите хранилище слева");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if !self.selected_paths.is_empty() {
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Выбрано: {}", self.selected_paths.len()));
|
||||
ui.add_enabled_ui(self.active_task_count == 0, |ui| {
|
||||
if ui.button("📋 Копировать").clicked() {
|
||||
log::info!("Начало операции копирования выбранных элементов");
|
||||
self.transfer_source_info = self.get_selected_info();
|
||||
self.is_move_mode = false;
|
||||
self.show_transfer_dialog = true;
|
||||
}
|
||||
if ui.button("✂ Переместить").clicked() {
|
||||
log::info!("Начало операции перемещения выбранных элементов");
|
||||
self.transfer_source_info = self.get_selected_info();
|
||||
self.is_move_mode = true;
|
||||
self.show_transfer_dialog = true;
|
||||
}
|
||||
if ui.button("🗑 Удалить").clicked() {
|
||||
log::info!("Начало операции удаления выбранных элементов");
|
||||
self.delete_selected();
|
||||
self.selected_paths.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Модальное окно трансфера
|
||||
if self.show_transfer_dialog {
|
||||
Window::new("Трансфер")
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(
|
||||
&mut self.active_transfer_tab,
|
||||
TransferTab::Remote,
|
||||
"☁ Облако",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.active_transfer_tab,
|
||||
TransferTab::Local,
|
||||
"💻 ПК",
|
||||
);
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.transfer_dest)
|
||||
.hint_text("Путь назначения..."),
|
||||
);
|
||||
if self.active_transfer_tab == TransferTab::Local
|
||||
&& ui.button("📂 Обзор").clicked()
|
||||
{
|
||||
log::debug!("Открыт локальный браузер файлов");
|
||||
self.refresh_local_list();
|
||||
self.show_local_browser = true;
|
||||
}
|
||||
});
|
||||
|
||||
if !self.transfer_dest.is_empty() && !self.is_path_valid() {
|
||||
ui.colored_label(
|
||||
Color32::KHAKI,
|
||||
if self.active_transfer_tab == TransferTab::Remote {
|
||||
"⚠ Путь облака должен содержать ':'"
|
||||
} else {
|
||||
"⚠ Укажите локальный путь без ':'"
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
let can_start = self.is_path_valid() && self.active_task_count == 0;
|
||||
if ui
|
||||
.add_enabled(can_start, egui::Button::new("🚀 Начать"))
|
||||
.clicked()
|
||||
{
|
||||
log::info!("Запуск трансфера в {}", self.transfer_dest);
|
||||
self.perform_transfer(self.is_move_mode);
|
||||
self.show_transfer_dialog = false;
|
||||
}
|
||||
if ui.button("Отмена").clicked() {
|
||||
log::debug!("Отмена трансфера");
|
||||
self.show_transfer_dialog = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Внутренний браузер папок
|
||||
if self.show_local_browser {
|
||||
Window::new("Выбор локальной папки")
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.fixed_size([400.0, 300.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(format!("📍 {}", self.local_browser_path));
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⬅ Наверх").clicked() {
|
||||
if let Some(parent) = Path::new(&self.local_browser_path).parent() {
|
||||
self.local_browser_path = parent.to_string_lossy().into();
|
||||
self.refresh_local_list();
|
||||
}
|
||||
}
|
||||
if ui.button("🔄").clicked() {
|
||||
self.refresh_local_list();
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
// Проверяем, пуста ли папка
|
||||
let is_empty = self.local_browser_files.is_empty();
|
||||
|
||||
// Создаем рамку для контента
|
||||
egui::Frame::group(ui.style())
|
||||
.inner_margin(egui::Margin::same(8.0))
|
||||
.show(ui, |ui| {
|
||||
if is_empty {
|
||||
// Если папка пуста, показываем сообщение по центру с иконкой
|
||||
let available_height = ui.available_height();
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.add_space(available_height / 3.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label("📂");
|
||||
ui.add_space(8.0);
|
||||
ui.colored_label(
|
||||
egui::Color32::GRAY,
|
||||
"Эта папка пуста"
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Если есть файлы, показываем список
|
||||
ScrollArea::vertical()
|
||||
.max_height(200.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let files = self.local_browser_files.clone();
|
||||
for file in files {
|
||||
if file.is_dir {
|
||||
ui.horizontal(|ui| {
|
||||
let response = ui.button("📁");
|
||||
ui.add_space(4.0);
|
||||
let link_response = ui.link(&file.name);
|
||||
|
||||
if response.clicked() || link_response.clicked() {
|
||||
self.local_browser_path = file.path;
|
||||
self.refresh_local_list();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("📄");
|
||||
ui.add_space(4.0);
|
||||
ui.label(&file.name);
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.small(file.format_size());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("✅ Выбрать").clicked() {
|
||||
log::debug!("Выбрана локальная папка: {}", self.local_browser_path);
|
||||
self.transfer_dest = self.local_browser_path.clone();
|
||||
self.show_local_browser = false;
|
||||
}
|
||||
if ui.button("Отмена").clicked() {
|
||||
self.show_local_browser = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(msg) = &self.error_message {
|
||||
let mut close = false;
|
||||
Window::new("Внимание")
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(msg);
|
||||
if ui.button("ОК").clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
if close {
|
||||
log::debug!("Ошибка закрыта: {}", msg);
|
||||
self.error_message = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = self.pending_load_path.take() {
|
||||
self.current_path = path.clone();
|
||||
self.selected_paths.clear();
|
||||
self.load_files(&path);
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
use super::*;
|
||||
use crate::operations::{self, CopyOptions, DeleteOptions, MoveOptions};
|
||||
use crate::rclone_install::RcloneApp;
|
||||
use eframe::egui;
|
||||
use egui::{FontData, FontDefinitions, FontFamily};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
impl RcloneUI {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
log::debug!("Инициализация RcloneUI");
|
||||
|
||||
// --- ВОССТАНОВЛЕНИЕ ШРИФТОВ ---
|
||||
let mut fonts = FontDefinitions::default();
|
||||
|
||||
if let Ok(font_data) = std::fs::read("assets/fonts/Sans.otf") {
|
||||
fonts
|
||||
.font_data
|
||||
.insert("sans".to_owned(), FontData::from_owned(font_data));
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "sans".to_owned());
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push("sans".to_owned());
|
||||
log::debug!("Шрифт Sans.otf загружен");
|
||||
} else {
|
||||
log::warn!("Файл шрифта Sans.otf не найден");
|
||||
}
|
||||
|
||||
if let Ok(emoji_data) = std::fs::read("assets/fonts/NotoColorEmoji.ttf") {
|
||||
fonts
|
||||
.font_data
|
||||
.insert("emoji".to_owned(), FontData::from_owned(emoji_data));
|
||||
let prop = fonts.families.entry(FontFamily::Proportional).or_default();
|
||||
if !prop.contains(&"emoji".to_owned()) {
|
||||
prop.push("emoji".to_owned());
|
||||
}
|
||||
log::debug!("Шрифт NotoColorEmoji.ttf загружен");
|
||||
} else {
|
||||
log::warn!("Файл шрифта NotoColorEmoji.ttf не найден");
|
||||
}
|
||||
cc.egui_ctx.set_fonts(fonts);
|
||||
|
||||
let (init_tx, init_rx) = channel();
|
||||
let (op_tx, op_rx) = channel();
|
||||
|
||||
log::info!("Запуск фоновой инициализации rclone");
|
||||
std::thread::spawn(move || {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let result = rt.block_on(async { RcloneApp::new().await });
|
||||
let res = match result {
|
||||
Ok(app) => {
|
||||
log::info!("Rclone успешно инициализирован");
|
||||
Ok(Arc::new(app))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Ошибка инициализации rclone: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
};
|
||||
let _ = init_tx.send(res);
|
||||
});
|
||||
|
||||
Self {
|
||||
rclone: None,
|
||||
state: AppState::Initializing,
|
||||
error_message: None,
|
||||
current_path: String::new(),
|
||||
history_back: Vec::new(),
|
||||
history_forward: Vec::new(),
|
||||
remote_list: Vec::new(),
|
||||
current_files: Vec::new(),
|
||||
selected_paths: Vec::new(),
|
||||
transfer_source_info: Vec::new(),
|
||||
transfer_dest: String::new(),
|
||||
active_transfer_tab: TransferTab::Remote,
|
||||
show_transfer_dialog: false,
|
||||
show_local_browser: false,
|
||||
local_browser_path: std::env::var("HOME").unwrap_or_else(|_| "/".into()),
|
||||
local_browser_files: Vec::new(),
|
||||
is_move_mode: false,
|
||||
settings: AppSettings::default(),
|
||||
active_task_count: 0,
|
||||
operation_tx: op_tx,
|
||||
operation_rx: op_rx,
|
||||
pending_load_path: None,
|
||||
rclone_init_receiver: Some(init_rx),
|
||||
active_operations: Vec::new(),
|
||||
next_op_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Валидация ---
|
||||
pub fn is_path_valid(&self) -> bool {
|
||||
if self.transfer_dest.is_empty() {
|
||||
return false;
|
||||
}
|
||||
match self.active_transfer_tab {
|
||||
TransferTab::Remote => self.transfer_dest.contains(':'),
|
||||
TransferTab::Local => {
|
||||
!self.transfer_dest.contains(':') || Path::new(&self.transfer_dest).is_absolute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Навигация ---
|
||||
pub fn navigate_to(&mut self, new_path: String) {
|
||||
log::debug!("Навигация: {} -> {}", self.current_path, new_path);
|
||||
if self.current_path == new_path {
|
||||
return;
|
||||
}
|
||||
if !self.current_path.is_empty() {
|
||||
self.history_back.push(self.current_path.clone());
|
||||
}
|
||||
self.history_forward.clear();
|
||||
self.pending_load_path = Some(new_path);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self) {
|
||||
if let Some(prev) = self.history_back.pop() {
|
||||
log::debug!("Назад: {} -> {}", self.current_path, prev);
|
||||
self.history_forward.push(self.current_path.clone());
|
||||
self.pending_load_path = Some(prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_forward(&mut self) {
|
||||
if let Some(next) = self.history_forward.pop() {
|
||||
log::debug!("Вперед: {} -> {}", self.current_path, next);
|
||||
self.history_back.push(self.current_path.clone());
|
||||
self.pending_load_path = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_local_list(&mut self) {
|
||||
log::debug!("Обновление списка локальной директории: {}", self.local_browser_path);
|
||||
if let Ok(files) = operations::local_fs::list_directory(&self.local_browser_path) {
|
||||
let count = files.len();
|
||||
self.local_browser_files = files;
|
||||
log::debug!("Найдено {} элементов в локальной директории", count);
|
||||
} else {
|
||||
log::warn!("Не удалось прочитать локальную директорию: {}", self.local_browser_path);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Работа с Rclone ---
|
||||
pub fn poll_rclone_init(&mut self) {
|
||||
if let Some(rx) = &self.rclone_init_receiver {
|
||||
if let Ok(res) = rx.try_recv() {
|
||||
match res {
|
||||
Ok(rclone) => {
|
||||
log::info!("Rclone инициализирован, загрузка удаленных хранилищ");
|
||||
self.rclone = Some(rclone);
|
||||
self.load_remotes();
|
||||
self.state = AppState::Ready;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Ошибка инициализации: {}", e);
|
||||
self.error_message = Some(e.clone());
|
||||
self.state = AppState::Error(e);
|
||||
}
|
||||
}
|
||||
self.rclone_init_receiver = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_remotes(&mut self) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Загрузка списка удаленных хранилищ");
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("Фоновый поток: получение списка remotes");
|
||||
let _ = tx.send(match operations::remotes::list(&rclone) {
|
||||
Ok(list) => {
|
||||
log::info!("Загружено {} удаленных хранилищ", list.len());
|
||||
OperationResult::RemoteList(list)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Ошибка загрузки remotes: {}", e);
|
||||
OperationResult::Failure(0, e.to_string())
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_files(&mut self, path: &str) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Загрузка файлов из пути: {}", path);
|
||||
self.active_task_count += 1;
|
||||
let path = path.to_string();
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("Фоновый поток: загрузка файлов из {}", path);
|
||||
let _ = tx.send(match operations::files::list(&rclone, &path) {
|
||||
Ok(list) => {
|
||||
log::info!("Загружено {} файлов из {}", list.len(), path);
|
||||
OperationResult::FileList(list)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Ошибка загрузки файлов из {}: {}", path, e);
|
||||
OperationResult::Failure(0, e.to_string())
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_background_operation(&mut self) {
|
||||
while let Ok(result) = self.operation_rx.try_recv() {
|
||||
match result {
|
||||
OperationResult::Success(op_id, _) => {
|
||||
log::debug!("Операция {} успешно завершена", op_id);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.pending_load_path = Some(self.current_path.clone());
|
||||
}
|
||||
OperationResult::Failure(op_id, e) => {
|
||||
log::error!("Операция {} завершена с ошибкой: {}", op_id, e);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.error_message = Some(e);
|
||||
}
|
||||
OperationResult::FileList(files) => {
|
||||
let count = files.len();
|
||||
log::debug!("Получен список файлов: {} элементов", count);
|
||||
self.current_files = files;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
}
|
||||
OperationResult::RemoteList(remotes) => {
|
||||
let count = remotes.len();
|
||||
log::debug!("Получен список remotes: {} элементов", count);
|
||||
self.remote_list = remotes;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
}
|
||||
OperationResult::ProgressUpdate(op_id, progress, status) => {
|
||||
if let Some(op) = self.active_operations.iter_mut().find(|op| op.id == op_id) {
|
||||
op.progress = progress;
|
||||
op.status = status.clone();
|
||||
log::trace!("Операция {}: прогресс {:.1}%, статус: {}", op_id, progress * 100.0, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_transfer(&mut self, is_move: bool) {
|
||||
if self.active_task_count > 0 {
|
||||
log::warn!("Не удалось начать трансфер: активны другие операции ({})", self.active_task_count);
|
||||
return;
|
||||
}
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
let sources = self.transfer_source_info.clone();
|
||||
let dest_base = self.transfer_dest.clone();
|
||||
let op_id = self.add_operation(format!(
|
||||
"{} {} эл.",
|
||||
if is_move {
|
||||
"Перенос"
|
||||
} else {
|
||||
"Копия"
|
||||
},
|
||||
sources.len()
|
||||
));
|
||||
|
||||
log::info!("Начало операции {}: {} элементов в {}",
|
||||
if is_move { "перемещения" } else { "копирования" },
|
||||
sources.len(),
|
||||
dest_base
|
||||
);
|
||||
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let total = sources.len() as f32;
|
||||
for (idx, (source, is_dir)) in sources.iter().enumerate() {
|
||||
let name = source.split([':', '/', '\\']).last().unwrap_or("");
|
||||
let separator = if dest_base.contains(':')
|
||||
&& !dest_base.ends_with(':')
|
||||
&& !dest_base.ends_with('/')
|
||||
{
|
||||
"/"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let final_dest = if *is_dir {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
dest_base.trim_end_matches(['/', '\\']),
|
||||
separator,
|
||||
name
|
||||
)
|
||||
} else {
|
||||
dest_base.clone()
|
||||
};
|
||||
|
||||
log::debug!("Операция {}: обработка {}/{}: {} -> {}", op_id, idx + 1, total, source, final_dest);
|
||||
|
||||
let res = if is_move {
|
||||
operations::sync::move_files(
|
||||
&rclone,
|
||||
source,
|
||||
&final_dest,
|
||||
&MoveOptions {
|
||||
verbose: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
operations::sync::copy(
|
||||
&rclone,
|
||||
source,
|
||||
&final_dest,
|
||||
&CopyOptions {
|
||||
verbose: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
log::error!("Операция {}: ошибка при обработке {}: {}", op_id, name, e);
|
||||
let _ = tx.send(OperationResult::Failure(
|
||||
op_id,
|
||||
format!("Ошибка на {}: {}", name, e),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = tx.send(OperationResult::ProgressUpdate(
|
||||
op_id,
|
||||
(idx + 1) as f32 / total,
|
||||
format!("{}/{}", idx + 1, total),
|
||||
));
|
||||
}
|
||||
log::info!("Операция {} успешно завершена", op_id);
|
||||
let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_selected(&mut self) {
|
||||
if self.active_task_count > 0 {
|
||||
log::warn!("Не удалось начать удаление: активны другие операции ({})", self.active_task_count);
|
||||
return;
|
||||
}
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
let paths = self.get_selected_info();
|
||||
let op_id = self.add_operation(format!("Удаление {} эл.", paths.len()));
|
||||
|
||||
log::info!("Начало операции удаления {} элементов", paths.len());
|
||||
|
||||
self.active_task_count += 1;
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
for (p, is_dir) in paths.iter() {
|
||||
log::debug!("Операция {}: удаление {}", op_id, p);
|
||||
let opts = DeleteOptions {
|
||||
recursive: *is_dir,
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(e) = operations::sync::delete(&rclone, p, &opts) {
|
||||
log::error!("Операция {}: ошибка при удалении {}: {}", op_id, p, e);
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e.to_string()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::info!("Операция удаления {} успешно завершена", op_id);
|
||||
let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_operation(&mut self, description: String) -> u32 {
|
||||
let id = self.next_op_id;
|
||||
self.next_op_id += 1;
|
||||
log::debug!("Добавлена операция {}: {}", id, description);
|
||||
self.active_operations.push(Operation {
|
||||
id,
|
||||
description,
|
||||
progress: 0.0,
|
||||
status: "В очереди".into(),
|
||||
start_time: Instant::now(),
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get_selected_info(&self) -> Vec<(String, bool)> {
|
||||
self.selected_paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let is_dir = self
|
||||
.current_files
|
||||
.iter()
|
||||
.find(|f| {
|
||||
let full = if self.current_path.ends_with(':') {
|
||||
format!("{}{}", self.current_path, f.name)
|
||||
} else {
|
||||
format!("{}/{}", self.current_path, f.name)
|
||||
};
|
||||
&full == path
|
||||
})
|
||||
.map(|f| f.is_dir)
|
||||
.unwrap_or(false);
|
||||
(path.clone(), is_dir)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user