From e77f0511490845254f0aafe30f621cd946380ba5 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 29 Mar 2026 20:53:00 +0300 Subject: [PATCH 1/6] feat(ui): to lockal directory feature --- shell.nix | 2 + src/operations.rs | 3 +- src/operations/local_fs.rs | 30 ++++ src/ui.rs | 327 +++++++++++++++++++++++-------------- 4 files changed, 234 insertions(+), 128 deletions(-) create mode 100644 src/operations/local_fs.rs diff --git a/shell.nix b/shell.nix index 33722b9..a8a213e 100644 --- a/shell.nix +++ b/shell.nix @@ -35,6 +35,8 @@ pkgs.mkShell { pkgs.xorg.libX11 pkgs.mesa pkgs.libglvnd + pkgs.gtk3 + pkgs.glib ]; # (Необязательно) принудительно использовать X11 backend diff --git a/src/operations.rs b/src/operations.rs index 6532ede..d9fbfe0 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -1,6 +1,7 @@ +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use serde::{Serialize, Deserialize}; +pub mod local_fs; /// === Модуль для работы с удаленными хранилищами === #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/operations/local_fs.rs b/src/operations/local_fs.rs new file mode 100644 index 0000000..a7f8ab9 --- /dev/null +++ b/src/operations/local_fs.rs @@ -0,0 +1,30 @@ + +use std::fs; +use std::path::{Path, PathBuf}; +use super::*; + +pub fn list_directory(path: &str) -> Result, 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) +} diff --git a/src/ui.rs b/src/ui.rs index 6287c73..af23452 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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 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::mpsc::{channel, Sender, Receiver}; +use std::sync::mpsc::{Receiver, Sender, channel}; use std::time::{Duration, Instant}; use tokio::runtime::Runtime; -use crate::rclone_install::RcloneApp; -use crate::operations::{self, FileInfo, Remote, CopyOptions, MoveOptions, DeleteOptions}; #[derive(Clone, PartialEq)] pub enum AppState { @@ -16,6 +22,12 @@ pub enum AppState { Error(String), } +#[derive(Debug, PartialEq, Clone, Copy)] +enum TransferTab { + Remote, + Local, +} + #[derive(Clone)] pub struct Operation { pub id: u32, @@ -26,8 +38,8 @@ pub struct Operation { } enum OperationResult { - Success(u32, String), - Failure(u32, String), + Success(u32, String), + Failure(u32, String), FileList(Vec), RemoteList(Vec), ProgressUpdate(u32, f32, String), @@ -39,7 +51,6 @@ pub struct RcloneUI { error_message: Option, current_path: String, - // История переходов history_back: Vec, history_forward: Vec, @@ -49,18 +60,24 @@ pub struct RcloneUI { transfer_source_info: Vec<(String, bool)>, transfer_dest: String, + active_transfer_tab: TransferTab, show_transfer_dialog: bool, - is_move_mode: bool, + show_local_browser: bool, + local_browser_path: String, + local_browser_files: Vec, + + is_move_mode: bool, + #[allow(dead_code)] settings: AppSettings, active_task_count: u32, operation_tx: Sender, operation_rx: Receiver, - + pending_load_path: Option, rclone_init_receiver: Option, String>>>, - + active_operations: Vec, next_op_id: u32, } @@ -72,7 +89,25 @@ pub struct AppSettings { } 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 (op_tx, op_rx) = channel(); @@ -98,7 +133,11 @@ impl RcloneUI { 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, @@ -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) { if self.current_path == new_path { return; } - if !self.current_path.is_empty() { - self.history_back.push(self.current_path.clone()); - } + 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_path) = self.history_back.pop() { + if let Some(prev) = self.history_back.pop() { 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) { - 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.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) { - if let Some(receiver) = &self.rclone_init_receiver { - if let Ok(result) = receiver.try_recv() { - match result { - Ok(rclone) => { - self.rclone = Some(rclone); - self.load_remotes(); - self.state = AppState::Ready; + 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(format!("Критическая ошибка: {}", e)); - self.state = AppState::Error(e); + Err(e) => { + self.error_message = Some(e.clone()); + self.state = AppState::Error(e); } } self.rclone_init_receiver = None; @@ -162,7 +214,7 @@ impl RcloneUI { std::thread::spawn(move || { let _ = tx.send(match operations::remotes::list(&rclone) { 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 || { let _ = tx.send(match operations::files::list(&rclone, &path) { 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_copy_multiple(&mut self, sources: Vec<(String, bool)>, dest: &str) { + fn perform_transfer(&mut self, is_move: bool) { + if self.active_task_count > 0 { return; } 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 dest_base = dest.to_string(); - let op_id = self.add_operation(format!("Копирование {} эл.", sources.len())); + 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 final_dest = if *is_dir { format!("{}/{}", dest_base.trim_end_matches('/'), name) } else { dest_base.clone() }; - if let Err(e) = operations::sync::copy(&rclone, source, &final_dest, &options) { - let _ = tx.send(OperationResult::Failure(op_id, format!("Ошибка копирования {}: {}", source, e))); return; + 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())); + 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() { - let options = MoveOptions { verbose: true, dry_run: false, delete_empty_src_dirs: true }; - 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) { - 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 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 || { - let total = path_info.len() as f32; - for (idx, (p, is_dir)) in path_info.iter().enumerate() { - let opts = DeleteOptions { recursive: *is_dir, verbose: false, dry_run: false }; + 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, 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 } - 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)> { 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) }; + 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) @@ -317,26 +345,21 @@ impl eframe::App for RcloneUI { self.poll_rclone_init(); self.poll_background_operation(); - let is_busy = !self.active_operations.is_empty(); - egui::TopBottomPanel::top("top").show(ctx, |ui| { egui::menu::bar(ui, |ui| { - ui.label("🚀 UI rclone"); + 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(200.0).show(ctx, |ui| { + SidePanel::left("left").default_width(180.0).show(ctx, |ui| { ui.add_space(10.0); ui.heading("Хранилища"); ui.separator(); @@ -349,8 +372,8 @@ impl eframe::App for RcloneUI { }); }); - SidePanel::right("right").default_width(220.0).show(ctx, |ui| { - ui.heading("Активность"); + 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 { @@ -360,13 +383,18 @@ impl eframe::App for RcloneUI { 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)); - 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(); @@ -374,11 +402,11 @@ impl eframe::App for RcloneUI { 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 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| { @@ -390,12 +418,12 @@ impl eframe::App for RcloneUI { 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(Self::format_size(file.size)); + ui.small(file.format_size()); }); }); } } 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.horizontal(|ui| { 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() { self.transfer_source_info = self.get_selected_info(); self.is_move_mode = false; @@ -415,7 +443,7 @@ impl eframe::App for RcloneUI { self.show_transfer_dialog = true; } if ui.button("🗑 Удалить").clicked() { - self.delete_selected_paths(self.selected_paths.clone()); + self.delete_selected(); self.selected_paths.clear(); } }); @@ -423,38 +451,83 @@ impl eframe::App for RcloneUI { } }); - // Модальные окна + // Модальное окно трансфера if self.show_transfer_dialog { - Window::new(if self.is_move_mode { "Перемещение" } else { "Копирование" }) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { - ui.label("Куда отправить:"); - ui.text_edit_singleline(&mut self.transfer_dest); - 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; } - }); + 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.colored_label(Color32::LIGHT_RED, "⚠️ Событие:"); + Window::new("Внимание").anchor(Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { ui.label(msg); - if ui.button("Закрыть").clicked() { close = true; } + 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(); From 25080e3cca15a45dfe241cfb7d6d21c64efe623d Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 13:57:34 +0300 Subject: [PATCH 2/6] feat(Ui): restrict UI by models --- Cargo.lock | 32 ++++ Cargo.toml | 4 + src/ui.rs | 469 +-------------------------------------------- src/ui/app.rs | 356 ++++++++++++++++++++++++++++++++++ src/ui/rcloneui.rs | 420 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 818 insertions(+), 463 deletions(-) create mode 100644 src/ui/app.rs create mode 100644 src/ui/rcloneui.rs diff --git a/Cargo.lock b/Cargo.lock index 6731dd0..09bd83a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 1a0f012..58ab050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/ui.rs b/src/ui.rs index af23452..76a014d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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, } - -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)); - } -} diff --git a/src/ui/app.rs b/src/ui/app.rs new file mode 100644 index 0000000..4195f02 --- /dev/null +++ b/src/ui/app.rs @@ -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)); + } +} diff --git a/src/ui/rcloneui.rs b/src/ui/rcloneui.rs new file mode 100644 index 0000000..a0920e3 --- /dev/null +++ b/src/ui/rcloneui.rs @@ -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() + } +} From dfa7c463acd3a559457f95b51a1bc2cc61378bff Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 13:58:33 +0300 Subject: [PATCH 3/6] feat(all): Add loging --- src/main.rs | 14 +++++ src/operations.rs | 20 +------ src/operations/check.rs | 2 - src/operations/fileinfo.rs | 19 +++++++ src/operations/info.rs | 1 - src/operations/local_fs.rs | 11 ++-- src/operations/remotes.rs | 23 +++++++- src/operations/sync.rs | 1 - src/rclone_install/rclone_install_real.rs | 61 ++++++++++++++++------ src/rclone_install/rclone_install_tests.rs | 1 - 10 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 src/operations/fileinfo.rs diff --git a/src/main.rs b/src/main.rs index 2e31609..59ca8c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,13 @@ use eframe::egui; use egui::{FontData, FontDefinitions, FontFamily}; fn main() -> Result<(), eframe::Error> { + // Инициализация логгера + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp_millis() + .init(); + + log::info!("Запуск Rclone UI Manager"); + let viewport = egui::ViewportBuilder::default() .with_inner_size([1200.0, 800.0]) .with_min_inner_size([800.0, 600.0]) @@ -20,11 +27,15 @@ fn main() -> Result<(), eframe::Error> { "Rclone UI Manager", options, Box::new(|cc| { + log::debug!("Инициализация шрифтов"); 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()); + log::debug!("Загружен шрифт Sans.otf"); + } else { + log::warn!("Не найден файл шрифта assets/fonts/Sans.otf"); } if let Ok(emoji_data) = std::fs::read("assets/fonts/NotoColorEmoji.ttf") { @@ -33,6 +44,9 @@ fn main() -> Result<(), eframe::Error> { if !prop.contains(&"emoji".to_owned()) { prop.push("emoji".to_owned()); } + log::debug!("Загружен шрифт NotoColorEmoji.ttf"); + } else { + log::warn!("Не найден файл шрифта assets/fonts/NotoColorEmoji.ttf"); } cc.egui_ctx.set_fonts(fonts); diff --git a/src/operations.rs b/src/operations.rs index d9fbfe0..45e15bf 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -24,25 +24,7 @@ pub struct FileInfo { pub modified: Option, } -impl FileInfo { - pub fn format_size(&self) -> String { - let bytes = self.size; - if bytes < 1024 { - format!("{} B", bytes) - } else if bytes < 1024 * 1024 { - format!("{:.1} KB", bytes as f64 / 1024.0) - } else if bytes < 1024 * 1024 * 1024 { - format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) - } else { - format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) - } - } - - pub fn icon(&self) -> &'static str { - if self.is_dir { "📁" } else { "📄" } - } -} - +pub mod fileinfo; pub mod files; /// === Модуль для операций синхронизации === diff --git a/src/operations/check.rs b/src/operations/check.rs index 423b808..22f8d0c 100644 --- a/src/operations/check.rs +++ b/src/operations/check.rs @@ -1,6 +1,4 @@ -use super::*; use crate::rclone_install::RcloneApp; -use serde_json::Value; /// Проверить разницу между двумя директориями pub fn diff(app: &RcloneApp, source: &str, dest: &str) -> Result, String> { diff --git a/src/operations/fileinfo.rs b/src/operations/fileinfo.rs new file mode 100644 index 0000000..bc42b6e --- /dev/null +++ b/src/operations/fileinfo.rs @@ -0,0 +1,19 @@ +use super::*; +impl FileInfo { + pub fn format_size(&self) -> String { + let bytes = self.size; + if bytes < 1024 { + format!("{} B", bytes) + } else if bytes < 1024 * 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else if bytes < 1024 * 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) + } else { + format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } + } + + pub fn icon(&self) -> &'static str { + if self.is_dir { "📁" } else { "📄" } + } +} diff --git a/src/operations/info.rs b/src/operations/info.rs index 0454b18..bcd5e9f 100644 --- a/src/operations/info.rs +++ b/src/operations/info.rs @@ -1,4 +1,3 @@ -use super::*; use crate::rclone_install::RcloneApp; use serde_json::Value; use std::collections::HashMap; diff --git a/src/operations/local_fs.rs b/src/operations/local_fs.rs index a7f8ab9..6e471cd 100644 --- a/src/operations/local_fs.rs +++ b/src/operations/local_fs.rs @@ -1,11 +1,14 @@ - -use std::fs; -use std::path::{Path, PathBuf}; use super::*; +use std::fs; +use std::path::PathBuf; pub fn list_directory(path: &str) -> Result, String> { let p = if path.is_empty() { - if cfg!(windows) { PathBuf::from("C:\\") } else { PathBuf::from("/") } + if cfg!(windows) { + PathBuf::from("C:\\") + } else { + PathBuf::from("/") + } } else { PathBuf::from(path) }; diff --git a/src/operations/remotes.rs b/src/operations/remotes.rs index f9a5767..571858e 100644 --- a/src/operations/remotes.rs +++ b/src/operations/remotes.rs @@ -4,6 +4,7 @@ use serde_json::Value; /// Получить список всех удаленных хранилищ pub fn list(app: &RcloneApp) -> Result, String> { + log::debug!("Получение списка удаленных хранилищ"); let output = app.run_command(&["listremotes"])?; let mut remotes = Vec::new(); @@ -14,6 +15,7 @@ pub fn list(app: &RcloneApp) -> Result, String> { if let Ok(config) = get_config(app, name) { remotes.push(config); } else { + log::warn!("Не удалось получить конфигурацию для remote '{}'", name); // Если не удалось получить конфиг, добавляем базовую информацию remotes.push(Remote { name: name.to_string(), @@ -24,11 +26,13 @@ pub fn list(app: &RcloneApp) -> Result, String> { } } + log::info!("Найдено {} удаленных хранилищ", remotes.len()); Ok(remotes) } /// Получить конфигурацию конкретного remote pub fn get_config(app: &RcloneApp, remote_name: &str) -> Result { + log::debug!("Получение конфигурации для remote: {}", remote_name); let output = app.run_command(&["config", "dump"])?; let configs: Value = serde_json::from_str(&output) @@ -54,13 +58,16 @@ pub fn get_config(app: &RcloneApp, remote_name: &str) -> Result } } + log::debug!("Получена конфигурация для {} типа {}", remote_name, r#type); Ok(Remote { name: remote_name.to_string(), r#type, config: config_map, }) } else { - Err(format!("Remote '{}' не найден", remote_name)) + let error_msg = format!("Remote '{}' не найден", remote_name); + log::error!("{}", error_msg); + Err(error_msg) } } @@ -71,6 +78,7 @@ pub fn create( r#type: &str, config: &HashMap, ) -> Result<(), String> { + log::info!("Создание нового remote: {} типа {}", name, r#type); let mut args = vec!["config", "create", name, r#type]; for (key, value) in config { @@ -79,11 +87,13 @@ pub fn create( } app.run_command(&args)?; + log::info!("Remote {} успешно создан", name); Ok(()) } /// Обновить конфигурацию remote pub fn update(app: &RcloneApp, name: &str, config: &HashMap) -> Result<(), String> { + log::info!("Обновление конфигурации remote: {}", name); let mut args = vec!["config", "update", name]; for (key, value) in config { @@ -92,24 +102,33 @@ pub fn update(app: &RcloneApp, name: &str, config: &HashMap) -> } app.run_command(&args)?; + log::info!("Конфигурация remote {} успешно обновлена", name); Ok(()) } /// Удалить remote pub fn delete(app: &RcloneApp, name: &str) -> Result<(), String> { + log::info!("Удаление remote: {}", name); app.run_command(&["config", "delete", name])?; + log::info!("Remote {} успешно удален", name); Ok(()) } /// Проверить доступность remote pub fn check(app: &RcloneApp, remote_name: &str) -> Result { + log::debug!("Проверка доступности remote: {}", remote_name); let result = app.run_command(&["lsd", remote_name, "--max-depth", "1"]); match result { - Ok(_) => Ok(true), + Ok(_) => { + log::debug!("Remote {} доступен", remote_name); + Ok(true) + } Err(e) => { if e.contains("directory not found") { + log::warn!("Remote {} существует, но пуст", remote_name); Ok(true) // Remote существует, но пустой } else { + log::error!("Ошибка доступа к remote {}: {}", remote_name, e); Err(e) } } diff --git a/src/operations/sync.rs b/src/operations/sync.rs index 70a8e8d..e01323e 100644 --- a/src/operations/sync.rs +++ b/src/operations/sync.rs @@ -1,6 +1,5 @@ use super::*; use crate::rclone_install::RcloneApp; -use serde_json::Value; /// Скопировать файлы/директории pub fn copy( diff --git a/src/rclone_install/rclone_install_real.rs b/src/rclone_install/rclone_install_real.rs index e487400..8453998 100644 --- a/src/rclone_install/rclone_install_real.rs +++ b/src/rclone_install/rclone_install_real.rs @@ -1,19 +1,18 @@ -use flate2::read::GzDecoder; use reqwest; use std::fs; use std::io::Cursor; use std::path::PathBuf; use std::process::Command; -use tar::Archive; -use tokio; use super::RcloneApp; impl RcloneApp { pub async fn new() -> Result> { + log::info!("Инициализация RcloneApp"); + // Сначала проверяем системный rclone if let Some(system_rclone) = Self::check_system_rclone() { - println!("Найден системный rclone: {:?}", system_rclone); + log::info!("Найден системный rclone: {:?}", system_rclone); return Ok(Self { rclone_path: system_rclone, using_system_rclone: true, @@ -32,7 +31,7 @@ impl RcloneApp { }; if rclone_path.exists() { - println!("Найден rclone в директории приложения: {:?}", rclone_path); + log::info!("Найден rclone в директории приложения: {:?}", rclone_path); return Ok(Self { rclone_path, using_system_rclone: false, @@ -40,12 +39,13 @@ impl RcloneApp { } // Если ничего не найдено - устанавливаем - println!("rclone не найден. Начинаю установку..."); + log::warn!("rclone не найден. Начинаю установку..."); Self::install_latest_rclone().await } // Публичный конструктор для тестов и создания экземпляра с заданными параметрами pub fn new_with_path(rclone_path: PathBuf, using_system_rclone: bool) -> Self { + log::debug!("Создание RcloneApp с путем {:?}, системный: {}", rclone_path, using_system_rclone); Self { rclone_path, using_system_rclone, @@ -63,30 +63,36 @@ impl RcloneApp { // Сеттеры pub fn set_rclone_path(&mut self, path: PathBuf) { + log::debug!("Изменение пути rclone с {:?} на {:?}", self.rclone_path, path); self.rclone_path = path; } pub fn set_using_system_rclone(&mut self, value: bool) { + log::debug!("Изменение флага using_system_rclone с {} на {}", self.using_system_rclone, value); self.using_system_rclone = value; } pub fn check_system_rclone() -> Option { + log::debug!("Проверка системного rclone"); if let Ok(path) = which::which("rclone") { + log::debug!("Найден системный rclone по пути: {:?}", path); return Some(path); } #[cfg(windows)] { if let Ok(path) = which::which("rclone.exe") { + log::debug!("Найден системный rclone.exe по пути: {:?}", path); return Some(path); } } + log::debug!("Системный rclone не найден"); None } async fn install_latest_rclone() -> Result> { - println!("Начинаю установку последней версии rclone..."); + log::info!("Начинаю установку последней версии rclone..."); // Определяем ОС и архитектуру let os = if cfg!(target_os = "linux") { @@ -109,8 +115,12 @@ impl RcloneApp { return Err("Неподдерживаемая архитектура".into()); }; + log::info!("ОС: {}, архитектура: {}", os, arch); + // Получаем информацию о последнем релизе let client = reqwest::Client::new(); + log::debug!("Запрос последней версии rclone с GitHub API"); + let latest_release: serde_json::Value = client .get("https://api.github.com/repos/rclone/rclone/releases/latest") .header("User-Agent", "rclone-ui-installer") @@ -124,27 +134,33 @@ impl RcloneApp { .ok_or("Не удалось получить версию")? .trim_start_matches('v'); + log::info!("Найдена последняя версия rclone: {}", version); + // Формируем URL для скачивания let download_url = format!( "https://github.com/rclone/rclone/releases/download/v{}/rclone-v{}-{}-{}.zip", version, version, os, arch ); - println!("Скачиваю rclone версии {} с: {}", version, download_url); + log::info!("Скачиваю rclone с: {}", download_url); // Скачиваем архив let response = client.get(&download_url).send().await?; if !response.status().is_success() { - return Err(format!("Ошибка скачивания: HTTP {}", response.status()).into()); + let error_msg = format!("Ошибка скачивания: HTTP {}", response.status()); + log::error!("{}", error_msg); + return Err(error_msg.into()); } let bytes = response.bytes().await?; + log::info!("Архив скачан, размер: {} байт", bytes.len()); // Создаем директорию приложения let app_dir = dirs::data_dir() .ok_or("Не удалось найти папку для данных")? .join("rclone-ui"); fs::create_dir_all(&app_dir)?; + log::debug!("Директория приложения: {:?}", app_dir); // Определяем имя исполняемого файла let rclone_filename = if cfg!(windows) { @@ -165,16 +181,18 @@ impl RcloneApp { let file_name = file.name().to_string(); if file_name.ends_with("rclone") || file_name.ends_with("rclone.exe") { + log::debug!("Найден исполняемый файл в архиве: {}", file_name); let mut contents = Vec::new(); std::io::copy(&mut file, &mut contents)?; fs::write(&rclone_path, contents)?; extracted = true; - println!("Найден и распакован: {}", file_name); + log::info!("Распакован: {} в {:?}", file_name, rclone_path); break; } } if !extracted { + log::error!("Не удалось найти исполняемый файл rclone в архиве"); return Err("Не удалось найти исполняемый файл rclone в архиве".into()); } @@ -185,9 +203,10 @@ impl RcloneApp { let mut perms = fs::metadata(&rclone_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&rclone_path, perms)?; + log::debug!("Установлены права на выполнение для {:?}", rclone_path); } - println!("rclone успешно установлен в {:?}", rclone_path); + log::info!("rclone успешно установлен в {:?}", rclone_path); // Проверяем установку let output = Command::new(&rclone_path) @@ -197,7 +216,9 @@ impl RcloneApp { if output.status.success() { let version_output = String::from_utf8_lossy(&output.stdout); - println!("Установленная версия rclone:\n{}", version_output); + log::info!("Установленная версия rclone:\n{}", version_output); + } else { + log::warn!("Не удалось проверить версию установленного rclone"); } Ok(Self { @@ -207,19 +228,29 @@ impl RcloneApp { } pub fn run_command(&self, args: &[&str]) -> Result { + log::debug!("Выполнение команды rclone: {} {:?}", self.rclone_path.display(), args); + let output = Command::new(&self.rclone_path) .args(args) .output() - .map_err(|e| format!("Ошибка запуска: {}", e))?; + .map_err(|e| { + log::error!("Ошибка запуска rclone: {}", e); + format!("Ошибка запуска: {}", e) + })?; if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).to_string()) + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + log::debug!("Команда выполнена успешно, вывод: {} байт", stdout.len()); + Ok(stdout) } else { - Err(String::from_utf8_lossy(&output.stderr).to_string()) + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + log::error!("Ошибка выполнения команды: {}", stderr); + Err(stderr) } } pub fn version(&self) -> Result { + log::debug!("Получение версии rclone"); self.run_command(&["version"]) } } diff --git a/src/rclone_install/rclone_install_tests.rs b/src/rclone_install/rclone_install_tests.rs index a86ea9e..845cbe1 100644 --- a/src/rclone_install/rclone_install_tests.rs +++ b/src/rclone_install/rclone_install_tests.rs @@ -1,6 +1,5 @@ use super::RcloneApp; use serial_test::serial; -use std::env; use std::fs; use tempfile::tempdir; From f15ee0a413c884216e1889a10b6ef8da41bd00b5 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 13:59:03 +0300 Subject: [PATCH 4/6] ci: fix ci --- .forgejo/workflows/main.yaml | 164 +++++++++++++++++------------------ 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/.forgejo/workflows/main.yaml b/.forgejo/workflows/main.yaml index 25226dc..b8bd800 100644 --- a/.forgejo/workflows/main.yaml +++ b/.forgejo/workflows/main.yaml @@ -1,101 +1,95 @@ -name: Rust CI +name: Rust CI (Native Install + Cache) on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: - check: + build_and_check: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - true - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build - run: cargo check --verbose - - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build - run: cargo build --verbose + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Cargo Check & Build + run: | + cargo check --verbose + cargo build --verbose test: - needs: build + needs: build_and_check runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Test - run: cargo test --verbose + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run Tests + run: cargo test --verbose docs: - needs: build + needs: build_and_check runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build documentation - run: cargo doc --no-deps --document-private-items - - - name: Create docs directory - run: mkdir -p ./docs-output && cp -r target/doc/* ./docs-output/ - - - name: Upload documentation as artifact - uses: actions/upload-artifact@v3 - with: - name: rust-documentation - path: ./docs-output/ - retention-days: 30 - if-no-files-found: error - + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build docs + run: | + cargo doc --no-deps --document-private-items + mkdir -p ./docs-output && cp -r target/doc/* ./docs-output/ + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: rust-documentation + path: ./docs-output/ + retention-days: 7 From 141199141707f915caaa9643e089aacddd8ff9d0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 14:26:47 +0300 Subject: [PATCH 5/6] ci: Added release ci --- .forgejo/workflows/main.yaml | 8 +++- .forgejo/workflows/release.yaml | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/release.yaml diff --git a/.forgejo/workflows/main.yaml b/.forgejo/workflows/main.yaml index b8bd800..8925cd8 100644 --- a/.forgejo/workflows/main.yaml +++ b/.forgejo/workflows/main.yaml @@ -1,5 +1,11 @@ name: Rust CI (Native Install + Cache) -on: [push, pull_request] +on: + push: + branches-ignore: + - release + pull_request: + branches-ignore: + - release env: CARGO_TERM_COLOR: always diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml new file mode 100644 index 0000000..876fc70 --- /dev/null +++ b/.forgejo/workflows/release.yaml @@ -0,0 +1,83 @@ +name: Release Build +on: + push: + branches: + - release + pull_request: + branches: + - release + +env: + CARGO_TERM_COLOR: always + # Название вашего приложения из Cargo.toml + APP_NAME: "UIrclone" + +jobs: + build: + name: Build for ${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + suffix: "" + binary_path: "target/x86_64-unknown-linux-gnu/release" + - target: x86_64-pc-windows-gnu + suffix: ".exe" + binary_path: "target/x86_64-pc-windows-gnu/release" + - target: x86_64-apple-darwin + suffix: "" + binary_path: "target/x86_64-apple-darwin/release" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install System Dependencies + run: | + sudo apt-get update + # Зависимости для egui/eframe на Linux + sudo apt-get install -y libwayland-dev libxkbcommon-dev libegl1-mesa-dev libgbm-dev + # Инструменты кросс-компиляции + if [[ "${{ matrix.target }}" == *"windows"* ]]; then + sudo apt-get install -y gcc-mingw-w64 + fi + + - name: Install Rust & Target + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + source $HOME/.cargo/env + fi + rustup target add ${{ matrix.target }} + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ matrix.target }}-release- + + - name: Build Binary + run: cargo build --release --target ${{ matrix.target }} + + - name: Prepare Artifact + run: | + mkdir -p ./out + # Копируем бинарник + cp "${{ matrix.binary_path }}/${{ env.APP_NAME }}${{ matrix.suffix }}" ./out/ + # Копируем ассеты (шрифты), так как они нужны приложению в рантайме + cp -r assets ./out/assets + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ env.APP_NAME }}-${{ matrix.target }} + path: ./out/ + retention-days: 5 From 0bb876915b853e4f79165490489e0d75ac89f47b Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 14:42:04 +0300 Subject: [PATCH 6/6] ci: Added Ci to github --- .github/workflows/main.yaml | 171 +++++++++++++++++---------------- .github/workflows/release.yaml | 83 ++++++++++++++++ 2 files changed, 169 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 52053f7..8925cd8 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,100 +1,101 @@ -name: Rust CI -on: [push, pull_request] +name: Rust CI (Native Install + Cache) +on: + push: + branches-ignore: + - release + pull_request: + branches-ignore: + - release env: CARGO_TERM_COLOR: always jobs: - check: + build_and_check: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build - run: cargo check --verbose - - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build - run: cargo build --verbose + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Cargo Check & Build + run: | + cargo check --verbose + cargo build --verbose test: - needs: [check, build] + needs: build_and_check runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Test - run: cargo test --verbose + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run Tests + run: cargo test --verbose docs: - needs: [check, build] + needs: build_and_check runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Verify Rust installation - run: | - rustc --version - cargo --version - - - name: Build documentation - run: cargo doc --no-deps --document-private-items - - - name: Create docs directory - run: mkdir -p ./docs-output && cp -r target/doc/* ./docs-output/ - - - name: Upload documentation as artifact - uses: actions/upload-artifact@v4 - with: - name: rust-documentation - path: ./docs-output/ - retention-days: 30 - if-no-files-found: error - + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build docs + run: | + cargo doc --no-deps --document-private-items + mkdir -p ./docs-output && cp -r target/doc/* ./docs-output/ + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: rust-documentation + path: ./docs-output/ + retention-days: 7 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..876fc70 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,83 @@ +name: Release Build +on: + push: + branches: + - release + pull_request: + branches: + - release + +env: + CARGO_TERM_COLOR: always + # Название вашего приложения из Cargo.toml + APP_NAME: "UIrclone" + +jobs: + build: + name: Build for ${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + suffix: "" + binary_path: "target/x86_64-unknown-linux-gnu/release" + - target: x86_64-pc-windows-gnu + suffix: ".exe" + binary_path: "target/x86_64-pc-windows-gnu/release" + - target: x86_64-apple-darwin + suffix: "" + binary_path: "target/x86_64-apple-darwin/release" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install System Dependencies + run: | + sudo apt-get update + # Зависимости для egui/eframe на Linux + sudo apt-get install -y libwayland-dev libxkbcommon-dev libegl1-mesa-dev libgbm-dev + # Инструменты кросс-компиляции + if [[ "${{ matrix.target }}" == *"windows"* ]]; then + sudo apt-get install -y gcc-mingw-w64 + fi + + - name: Install Rust & Target + run: | + if ! command -v rustc &> /dev/null; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + source $HOME/.cargo/env + fi + rustup target add ${{ matrix.target }} + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ matrix.target }}-release- + + - name: Build Binary + run: cargo build --release --target ${{ matrix.target }} + + - name: Prepare Artifact + run: | + mkdir -p ./out + # Копируем бинарник + cp "${{ matrix.binary_path }}/${{ env.APP_NAME }}${{ matrix.suffix }}" ./out/ + # Копируем ассеты (шрифты), так как они нужны приложению в рантайме + cp -r assets ./out/assets + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ env.APP_NAME }}-${{ matrix.target }} + path: ./out/ + retention-days: 5