feat(ui): to lockal directory feature

This commit is contained in:
Black_Raven committed 2026-03-29 22:57:07 +03:00
1 parent c2b42a2dd9
commit e77f051149
4 files changed
+234 -128

No files matched your search

+2
View File
@@ -35,6 +35,8 @@ pkgs.mkShell {
pkgs.xorg.libX11 pkgs.xorg.libX11
pkgs.mesa pkgs.mesa
pkgs.libglvnd pkgs.libglvnd
pkgs.gtk3
pkgs.glib
]; ];
# (Необязательно) принудительно использовать X11 backend # (Необязательно) принудительно использовать X11 backend
+2 -1
View File
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Serialize, Deserialize};
pub mod local_fs;
/// === Модуль для работы с удаленными хранилищами === /// === Модуль для работы с удаленными хранилищами ===
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
+30
View File
@@ -0,0 +1,30 @@
use std::fs;
use std::path::{Path, PathBuf};
use super::*;
pub fn list_directory(path: &str) -> Result<Vec<FileInfo>, String> {
let p = if path.is_empty() {
if cfg!(windows) { PathBuf::from("C:\\") } else { PathBuf::from("/") }
} else {
PathBuf::from(path)
};
let entries = fs::read_dir(&p).map_err(|e| e.to_string())?;
let mut files = Vec::new();
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata() {
files.push(FileInfo {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
size: meta.len(),
is_dir: meta.is_dir(),
modified: None,
});
}
}
// Сначала папки, потом файлы, по алфавиту
files.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
Ok(files)
}
+200 -127
View File
@@ -1,13 +1,19 @@
//! Графический интерфейс для операций rclone. //! Графический интерфейс для операций rclone с интегрированным Local FS и поддержкой кастомных шрифтов.
use crate::operations::{
self, CopyOptions, DeleteOptions, FileInfo, MoveOptions, Remote,
};
use crate::rclone_install::RcloneApp;
use eframe::egui; use eframe::egui;
use egui::{CentralPanel, ScrollArea, Window, Align2, Color32, Spinner, ProgressBar, SidePanel}; use egui::{
Align2, CentralPanel, Color32, ProgressBar, ScrollArea, SidePanel, Spinner, Window,
FontDefinitions, FontData, FontFamily,
};
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc::{channel, Sender, Receiver}; use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::rclone_install::RcloneApp;
use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum AppState { pub enum AppState {
@@ -16,6 +22,12 @@ pub enum AppState {
Error(String), Error(String),
} }
#[derive(Debug, PartialEq, Clone, Copy)]
enum TransferTab {
Remote,
Local,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Operation { pub struct Operation {
pub id: u32, pub id: u32,
@@ -26,8 +38,8 @@ pub struct Operation {
} }
enum OperationResult { enum OperationResult {
Success(u32, String), Success(u32, String),
Failure(u32, String), Failure(u32, String),
FileList(Vec<FileInfo>), FileList(Vec<FileInfo>),
RemoteList(Vec<Remote>), RemoteList(Vec<Remote>),
ProgressUpdate(u32, f32, String), ProgressUpdate(u32, f32, String),
@@ -39,7 +51,6 @@ pub struct RcloneUI {
error_message: Option<String>, error_message: Option<String>,
current_path: String, current_path: String,
// История переходов
history_back: Vec<String>, history_back: Vec<String>,
history_forward: Vec<String>, history_forward: Vec<String>,
@@ -49,18 +60,24 @@ pub struct RcloneUI {
transfer_source_info: Vec<(String, bool)>, transfer_source_info: Vec<(String, bool)>,
transfer_dest: String, transfer_dest: String,
active_transfer_tab: TransferTab,
show_transfer_dialog: bool, show_transfer_dialog: bool,
is_move_mode: bool, show_local_browser: bool,
local_browser_path: String,
local_browser_files: Vec<FileInfo>,
is_move_mode: bool,
#[allow(dead_code)]
settings: AppSettings, settings: AppSettings,
active_task_count: u32, active_task_count: u32,
operation_tx: Sender<OperationResult>, operation_tx: Sender<OperationResult>,
operation_rx: Receiver<OperationResult>, operation_rx: Receiver<OperationResult>,
pending_load_path: Option<String>, pending_load_path: Option<String>,
rclone_init_receiver: Option<Receiver<Result<Arc<RcloneApp>, String>>>, rclone_init_receiver: Option<Receiver<Result<Arc<RcloneApp>, String>>>,
active_operations: Vec<Operation>, active_operations: Vec<Operation>,
next_op_id: u32, next_op_id: u32,
} }
@@ -72,7 +89,25 @@ pub struct AppSettings {
} }
impl RcloneUI { impl RcloneUI {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { 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 (init_tx, init_rx) = channel();
let (op_tx, op_rx) = channel(); let (op_tx, op_rx) = channel();
@@ -98,7 +133,11 @@ impl RcloneUI {
selected_paths: Vec::new(), selected_paths: Vec::new(),
transfer_source_info: Vec::new(), transfer_source_info: Vec::new(),
transfer_dest: String::new(), transfer_dest: String::new(),
active_transfer_tab: TransferTab::Remote,
show_transfer_dialog: false, 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, is_move_mode: false,
settings: AppSettings::default(), settings: AppSettings::default(),
active_task_count: 0, active_task_count: 0,
@@ -111,43 +150,56 @@ impl RcloneUI {
} }
} }
// --- Логика навигации --- // --- Валидация ---
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) { fn navigate_to(&mut self, new_path: String) {
if self.current_path == new_path { return; } if self.current_path == new_path { return; }
if !self.current_path.is_empty() { if !self.current_path.is_empty() { self.history_back.push(self.current_path.clone()); }
self.history_back.push(self.current_path.clone());
}
self.history_forward.clear(); self.history_forward.clear();
self.pending_load_path = Some(new_path); self.pending_load_path = Some(new_path);
} }
fn go_back(&mut self) { fn go_back(&mut self) {
if let Some(prev_path) = self.history_back.pop() { if let Some(prev) = self.history_back.pop() {
self.history_forward.push(self.current_path.clone()); self.history_forward.push(self.current_path.clone());
self.pending_load_path = Some(prev_path); self.pending_load_path = Some(prev);
} }
} }
fn go_forward(&mut self) { fn go_forward(&mut self) {
if let Some(next_path) = self.history_forward.pop() { if let Some(next) = self.history_forward.pop() {
self.history_back.push(self.current_path.clone()); self.history_back.push(self.current_path.clone());
self.pending_load_path = Some(next_path); 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) { fn poll_rclone_init(&mut self) {
if let Some(receiver) = &self.rclone_init_receiver { if let Some(rx) = &self.rclone_init_receiver {
if let Ok(result) = receiver.try_recv() { if let Ok(res) = rx.try_recv() {
match result { match res {
Ok(rclone) => { Ok(rclone) => {
self.rclone = Some(rclone); self.rclone = Some(rclone);
self.load_remotes(); self.load_remotes();
self.state = AppState::Ready; self.state = AppState::Ready;
} }
Err(e) => { Err(e) => {
self.error_message = Some(format!("Критическая ошибка: {}", e)); self.error_message = Some(e.clone());
self.state = AppState::Error(e); self.state = AppState::Error(e);
} }
} }
self.rclone_init_receiver = None; self.rclone_init_receiver = None;
@@ -162,7 +214,7 @@ impl RcloneUI {
std::thread::spawn(move || { std::thread::spawn(move || {
let _ = tx.send(match operations::remotes::list(&rclone) { let _ = tx.send(match operations::remotes::list(&rclone) {
Ok(list) => OperationResult::RemoteList(list), Ok(list) => OperationResult::RemoteList(list),
Err(e) => OperationResult::Failure(0, format!("Ошибка списка хранилищ: {}", e)), Err(e) => OperationResult::Failure(0, e.to_string()),
}); });
}); });
} }
@@ -176,7 +228,7 @@ impl RcloneUI {
std::thread::spawn(move || { std::thread::spawn(move || {
let _ = tx.send(match operations::files::list(&rclone, &path) { let _ = tx.send(match operations::files::list(&rclone, &path) {
Ok(list) => OperationResult::FileList(list), Ok(list) => OperationResult::FileList(list),
Err(e) => OperationResult::Failure(0, format!("Ошибка доступа к папке: {}", e)), Err(e) => OperationResult::Failure(0, e.to_string()),
}); });
}); });
} }
@@ -213,75 +265,59 @@ impl RcloneUI {
} }
} }
// --- Операции --- fn perform_transfer(&mut self, is_move: bool) {
fn perform_copy_multiple(&mut self, sources: Vec<(String, bool)>, dest: &str) { if self.active_task_count > 0 { return; }
if let Some(rclone) = self.rclone.clone() { if let Some(rclone) = self.rclone.clone() {
let options = CopyOptions { verbose: true, dry_run: false, bandwidth_limit: self.settings.bandwidth_limit.clone(), no_traverse: false }; let sources = self.transfer_source_info.clone();
let dest_base = dest.to_string(); let dest_base = self.transfer_dest.clone();
let op_id = self.add_operation(format!("Копирование {} эл.", sources.len())); let op_id = self.add_operation(format!("{} {} эл.", if is_move { "Перенос" } else { "Копия" }, sources.len()));
self.active_task_count += 1; self.active_task_count += 1;
let tx = self.operation_tx.clone(); let tx = self.operation_tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let total = sources.len() as f32; let total = sources.len() as f32;
for (idx, (source, is_dir)) in sources.iter().enumerate() { for (idx, (source, is_dir)) in sources.iter().enumerate() {
let name = source.split([':', '/']).last().unwrap_or(""); let name = source.split([':', '/', '\\']).last().unwrap_or("");
let final_dest = if *is_dir { format!("{}/{}", dest_base.trim_end_matches('/'), name) } else { dest_base.clone() }; let separator = if dest_base.contains(':') && !dest_base.ends_with(':') && !dest_base.ends_with('/') { "/" } else { "" };
if let Err(e) = operations::sync::copy(&rclone, source, &final_dest, &options) {
let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка копирования {}: {}", source, e))); return; 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::ProgressUpdate(op_id, (idx + 1) as f32 / total, format!("{}/{}", idx+1, total)));
} }
let _ = tx.send(OperationResult::Success(op_id, "Завершено".into())); let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
}); });
} }
} }
fn perform_move_multiple(&mut self, sources: Vec<(String, bool)>, dest: &str) { fn delete_selected(&mut self) {
if self.active_task_count > 0 { return; }
if let Some(rclone) = self.rclone.clone() { if let Some(rclone) = self.rclone.clone() {
let options = MoveOptions { verbose: true, dry_run: false, delete_empty_src_dirs: true }; let paths = self.get_selected_info();
let dest_base = dest.to_string();
let op_id = self.add_operation(format!("Перемещение {} эл.", 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 final_dest = if *is_dir { format!("{}/{}", dest_base.trim_end_matches('/'), name) } else { dest_base.clone() };
if let Err(e) = operations::sync::move_files(&rclone, source, &final_dest, &options) {
let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка перемещения {}: {}", source, 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_paths(&mut self, paths: Vec<String>) {
if let Some(rclone) = self.rclone.clone() {
let mut path_info = Vec::new();
for path in &paths {
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_info.push((path.clone(), is_dir));
}
let op_id = self.add_operation(format!("Удаление {} эл.", paths.len())); let op_id = self.add_operation(format!("Удаление {} эл.", paths.len()));
self.active_task_count += 1; self.active_task_count += 1;
let tx = self.operation_tx.clone(); let tx = self.operation_tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let total = path_info.len() as f32; for (p, is_dir) in paths.iter() {
for (idx, (p, is_dir)) in path_info.iter().enumerate() { let opts = DeleteOptions { recursive: *is_dir, ..Default::default() };
let opts = DeleteOptions { recursive: *is_dir, verbose: false, dry_run: false };
if let Err(e) = operations::sync::delete(&rclone, p, &opts) { if let Err(e) = operations::sync::delete(&rclone, p, &opts) {
let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка удаления {}: {}", p, e))); return; let _ = tx.send(OperationResult::Failure(op_id, e.to_string())); 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())); let _ = tx.send(OperationResult::Success(op_id, "Ок".into()));
}); });
} }
} }
@@ -293,18 +329,10 @@ impl RcloneUI {
id id
} }
fn format_size(bytes: u64) -> String {
if bytes >= 1073741824 { format!("{:.2} ГБ", bytes as f64 / 1073741824.0) }
else if bytes >= 1048576 { format!("{:.2} МБ", bytes as f64 / 1048576.0) }
else if bytes >= 1024 { format!("{:.2} КБ", bytes as f64 / 1024.0) }
else { format!("{} Б", bytes) }
}
fn get_selected_info(&self) -> Vec<(String, bool)> { fn get_selected_info(&self) -> Vec<(String, bool)> {
self.selected_paths.iter().map(|path| { self.selected_paths.iter().map(|path| {
let is_dir = self.current_files.iter().find(|f| { let is_dir = self.current_files.iter().find(|f| {
let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, f.name) } let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, f.name) } else { format!("{}/{}", self.current_path, f.name) };
else { format!("{}/{}", self.current_path, f.name) };
&full == path &full == path
}).map(|f| f.is_dir).unwrap_or(false); }).map(|f| f.is_dir).unwrap_or(false);
(path.clone(), is_dir) (path.clone(), is_dir)
@@ -317,26 +345,21 @@ impl eframe::App for RcloneUI {
self.poll_rclone_init(); self.poll_rclone_init();
self.poll_background_operation(); self.poll_background_operation();
let is_busy = !self.active_operations.is_empty();
egui::TopBottomPanel::top("top").show(ctx, |ui| { egui::TopBottomPanel::top("top").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.label("🚀 UI rclone"); ui.label("🚀 Rclone GUI");
ui.separator(); ui.separator();
// Навигация
ui.add_enabled_ui(!self.history_back.is_empty(), |ui| { ui.add_enabled_ui(!self.history_back.is_empty(), |ui| {
if ui.button("⬅").clicked() { self.go_back(); } if ui.button("⬅").clicked() { self.go_back(); }
}); });
ui.add_enabled_ui(!self.history_forward.is_empty(), |ui| { ui.add_enabled_ui(!self.history_forward.is_empty(), |ui| {
if ui.button("➡").clicked() { self.go_forward(); } if ui.button("➡").clicked() { self.go_forward(); }
}); });
if self.active_task_count > 0 { ui.add(Spinner::new().size(16.0)); } if self.active_task_count > 0 { ui.add(Spinner::new().size(16.0)); }
}); });
}); });
SidePanel::left("left").default_width(200.0).show(ctx, |ui| { SidePanel::left("left").default_width(180.0).show(ctx, |ui| {
ui.add_space(10.0); ui.add_space(10.0);
ui.heading("Хранилища"); ui.heading("Хранилища");
ui.separator(); ui.separator();
@@ -349,8 +372,8 @@ impl eframe::App for RcloneUI {
}); });
}); });
SidePanel::right("right").default_width(220.0).show(ctx, |ui| { SidePanel::right("right").default_width(200.0).show(ctx, |ui| {
ui.heading("Активность"); ui.heading("Задачи");
ui.separator(); ui.separator();
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
for op in &self.active_operations { for op in &self.active_operations {
@@ -360,13 +383,18 @@ impl eframe::App for RcloneUI {
ui.small(&op.status); ui.small(&op.status);
}); });
} }
if self.active_operations.is_empty() {
ui.weak("Нет активных задач");
}
}); });
}); });
CentralPanel::default().show(ctx, |ui| { CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("📍 {}", self.current_path)); ui.label(format!("📍 {}", self.current_path));
if ui.button("🔄").clicked() { self.pending_load_path = Some(self.current_path.clone()); } 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(); ui.separator();
@@ -374,11 +402,11 @@ impl eframe::App for RcloneUI {
if !self.current_path.is_empty() { if !self.current_path.is_empty() {
if self.current_path.contains('/') && ui.button("📁 .. (Вверх)").clicked() { if self.current_path.contains('/') && ui.button("📁 .. (Вверх)").clicked() {
if let Some((p, _)) = self.current_path.rsplit_once('/') { self.navigate_to(p.into()); } 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() { for file in self.current_files.clone() {
let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, file.name) } let full = if self.current_path.ends_with(':') { format!("{}{}", self.current_path, file.name) } else { format!("{}/{}", self.current_path, file.name) };
else { format!("{}/{}", self.current_path, file.name) };
let sel = self.selected_paths.contains(&full); let sel = self.selected_paths.contains(&full);
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -390,12 +418,12 @@ impl eframe::App for RcloneUI {
if ui.link(&file.name).clicked() { self.navigate_to(full); } if ui.link(&file.name).clicked() { self.navigate_to(full); }
} else { ui.label(&file.name); } } else { ui.label(&file.name); }
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.small(Self::format_size(file.size)); ui.small(file.format_size());
}); });
}); });
} }
} else { } else {
ui.centered_and_justified(|ui| { ui.label("Выберите диск в левой панели"); }); ui.centered_and_justified(|ui| { ui.label("Выберите хранилище слева"); });
} }
}); });
@@ -403,7 +431,7 @@ impl eframe::App for RcloneUI {
ui.separator(); ui.separator();
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("Выбрано: {}", self.selected_paths.len())); ui.label(format!("Выбрано: {}", self.selected_paths.len()));
ui.add_enabled_ui(!is_busy, |ui| { ui.add_enabled_ui(self.active_task_count == 0, |ui| {
if ui.button("📋 Копировать").clicked() { if ui.button("📋 Копировать").clicked() {
self.transfer_source_info = self.get_selected_info(); self.transfer_source_info = self.get_selected_info();
self.is_move_mode = false; self.is_move_mode = false;
@@ -415,7 +443,7 @@ impl eframe::App for RcloneUI {
self.show_transfer_dialog = true; self.show_transfer_dialog = true;
} }
if ui.button("🗑 Удалить").clicked() { if ui.button("🗑 Удалить").clicked() {
self.delete_selected_paths(self.selected_paths.clone()); self.delete_selected();
self.selected_paths.clear(); self.selected_paths.clear();
} }
}); });
@@ -423,38 +451,83 @@ impl eframe::App for RcloneUI {
} }
}); });
// Модальные окна // Модальное окно трансфера
if self.show_transfer_dialog { if self.show_transfer_dialog {
Window::new(if self.is_move_mode { "Перемещение" } else { "Копирование" }) Window::new("Трансфер").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).collapsible(false).show(ctx, |ui| {
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { ui.horizontal(|ui| {
ui.label("Куда отправить:"); ui.selectable_value(&mut self.active_transfer_tab, TransferTab::Remote, "☁ Облако");
ui.text_edit_singleline(&mut self.transfer_dest); ui.selectable_value(&mut self.active_transfer_tab, TransferTab::Local, "💻 ПК");
if !self.transfer_dest.contains(':') && !self.transfer_dest.is_empty() {
ui.colored_label(Color32::YELLOW, "⚠ Путь rclone должен содержать ':'");
}
ui.horizontal(|ui| {
if ui.button("ОК").clicked() && !self.transfer_dest.is_empty() {
let (s, d) = (self.transfer_source_info.clone(), self.transfer_dest.clone());
if self.is_move_mode { self.perform_move_multiple(s, &d); }
else { self.perform_copy_multiple(s, &d); }
self.show_transfer_dialog = false;
}
if ui.button("Отмена").clicked() { self.show_transfer_dialog = false; }
});
}); });
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 { if let Some(msg) = &self.error_message {
let mut close = false; let mut close = false;
Window::new("Сообщение").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { Window::new("Внимание").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| {
ui.colored_label(Color32::LIGHT_RED, "⚠️ Событие:");
ui.label(msg); ui.label(msg);
if ui.button("Закрыть").clicked() { close = true; } if ui.button("ОК").clicked() { close = true; }
}); });
if close { self.error_message = None; } if close { self.error_message = None; }
} }
// Выполнение отложенной загрузки
if let Some(path) = self.pending_load_path.take() { if let Some(path) = self.pending_load_path.take() {
self.current_path = path.clone(); self.current_path = path.clone();
self.selected_paths.clear(); self.selected_paths.clear();