Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b3242052 | ||
|
|
8d213972d6 | ||
|
|
1679c8b08e | ||
|
|
1f5ec2dede | ||
|
|
0a504966ee | ||
|
|
c1ac7b28bb | ||
|
|
65939bd501 | ||
|
|
9fb0e7fb19 | ||
|
|
b90ff7f18d | ||
|
|
3c64c39cf3 |
No files matched your search
@@ -7,7 +7,9 @@
|
||||
## Возможности
|
||||
|
||||
- **Автоматическая установка rclone** – обнаруживает системный rclone или загружает последнюю версию автоматически.
|
||||
- **Управление удалёнными хранилищами (remotes)** – просмотр, проверка и использование всех настроенных в rclone подключений.
|
||||
- **Управление удалёнными хранилищами (remotes)** – просмотр, создание, проверка, удаление и просмотр информации обо всех подключениях rclone.
|
||||
- **Создание хранилищ через GUI** – мастер добавления с пошаговыми вопросами rclone (`--non-interactive`): укажите имя и тип, а для облаков с авторизацией (Google Drive, Dropbox и др.) автоматически откроется браузер для входа в аккаунт (OAuth).
|
||||
- **Контекстное меню хранилищ** – по правой кнопке мыши: открыть, проверить доступность, посмотреть использование (объём/число файлов), удалить с подтверждением.
|
||||
- **Локальный файловый менеджер** – навигация по локальной файловой системе, выбор папок назначения.
|
||||
- **Основные файловые операции**:
|
||||
- Копирование / Перемещение / Удаление нескольких файлов или папок.
|
||||
@@ -64,20 +66,29 @@ sudo dnf install libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel
|
||||
## Использование
|
||||
|
||||
1. Запустите приложение.
|
||||
2. На левой боковой панели отобразится список ваших rclone remote (если они есть). Если remotes не настроены, используйте командную строку rclone для их добавления – в будущих версиях появится графический интерфейс для настройки.
|
||||
3. Нажмите на имя remote, чтобы просмотреть его содержимое.
|
||||
4. Выберите файлы/папки, щёлкая по их иконке (📁 или 📄). Можно выбирать несколько элементов.
|
||||
5. После выбора внизу появятся кнопки: **Копировать**, **Переместить**, **Удалить**.
|
||||
6. Для копирования/перемещения укажите путь назначения:
|
||||
2. На левой боковой панели отобразится список ваших rclone remote. Чтобы добавить новое хранилище, нажмите кнопку **«➕ Добавить облако»** и заполните мастер:
|
||||
- **Имя** – например, `mydrive`.
|
||||
- **Тип** – например, `drive`, `dropbox`, `s3`, `webdav`.
|
||||
- Если выбранный тип требует авторизации (OAuth), после нажатия «Создать» автоматически откроется браузер для входа в аккаунт. При необходимости rclone задаст дополнительные вопросы (например, о Team Drive) – ответьте в окне мастера.
|
||||
3. Нажмите на имя remote **левой кнопкой мыши**, чтобы просмотреть его содержимое.
|
||||
4. **Правая кнопка мыши** по хранилищу открывает контекстное меню:
|
||||
- **📂 Открыть** – перейти в хранилище.
|
||||
- **🔍 Проверить доступность** – проверить подключение.
|
||||
- **📊 Использование** – показать общий объём, занятое и свободное место, число файлов.
|
||||
- **🗑️ Удалить** – удалить хранилище из конфигурации rclone (файлы в облаке при этом не удаляются; требуется подтверждение).
|
||||
5. Выберите файлы/папки, щёлкая по их иконке (📁 или 📄). Можно выбирать несколько элементов.
|
||||
6. После выбора внизу появятся кнопки: **Копировать**, **Переместить**, **Удалить**.
|
||||
7. Для копирования/перемещения укажите путь назначения:
|
||||
- **Облако** – введите путь в формате rclone (например, `gdrive:/backup`).
|
||||
- **ПК** – выберите локальную папку через встроенный файловый браузер или введите путь вручную.
|
||||
7. Нажмите **Начать** – операция запустится в фоне. Прогресс отображается на правой панели.
|
||||
8. Нажмите **Начать** – операция запустится в фоне. Прогресс отображается на правой панели.
|
||||
|
||||
### Навигация
|
||||
|
||||
- **⬅ / ➡** – переход назад / вперёд по истории.
|
||||
- **🔄** – обновить текущую директорию.
|
||||
- **📁 .. (Вверх)** – подняться на уровень выше.
|
||||
- **Правая кнопка мыши по хранилищу** – контекстное меню хранилища.
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
@@ -104,9 +115,9 @@ src/
|
||||
│ ├── app.rs # Реализация egui::App
|
||||
│ └── rcloneui.rs # Логика UI, фоновые задачи
|
||||
├── operations/
|
||||
│ ├── files.rs # Обёртка над rclone lsjson
|
||||
│ ├── files.rs # Обёртка над rclone lsjson / about
|
||||
│ ├── sync.rs # copy/move/delete/purge
|
||||
│ ├── remotes.rs # list/get/create/update/delete remotes
|
||||
│ ├── remotes.rs # list/get/check, интерактивное создание (config create --non-interactive)
|
||||
│ ├── local_fs.rs # Чтение локальных директорий
|
||||
│ ├── fileinfo.rs # Вспомогательные методы FileInfo
|
||||
│ └── ... # info, search, check
|
||||
@@ -153,12 +164,11 @@ cargo test
|
||||
|
||||
## Планы на будущее
|
||||
|
||||
- [ ] Графическая настройка remote (создание/изменение).
|
||||
- [ ] Графическое изменение конфигурации существующих remote.
|
||||
- [ ] Предварительный просмотр синхронизации (dry-run со списком файлов).
|
||||
- [ ] Ползунок ограничения скорости.
|
||||
- [ ] Поиск файлов по имени/пути.
|
||||
- [ ] Поддержка фильтров rclone (`--include`, `--exclude`).
|
||||
- [ ] Отображение информации о ёмкости хранилища (rclone about).
|
||||
- [ ] Переключение светлой/тёмной темы.
|
||||
|
||||
## Лицензия
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
use crate::operations::remotes::{config_create_step, ConfigStep};
|
||||
use crate::operations::{FileInfo, SyncStats};
|
||||
use crate::rclone_install::RcloneApp;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn write_mock_rclone(script: &str) -> (tempfile::TempDir, std::path::PathBuf) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let temp_dir = tempdir().expect("Failed to create temp dir");
|
||||
let path = temp_dir.path().join("rclone");
|
||||
fs::write(&path, script).unwrap();
|
||||
let mut perms = fs::metadata(&path).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&path, perms).unwrap();
|
||||
(temp_dir, path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_info_formatting() {
|
||||
@@ -27,3 +43,131 @@ fn test_sync_stats_formatting() {
|
||||
|
||||
assert_eq!(stats.format_speed(), "2.4 MB/s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn test_config_create_step_returns_question() {
|
||||
let script = r#"#!/bin/sh
|
||||
cat <<'EOF'
|
||||
{
|
||||
"State": "some-state",
|
||||
"Option": {
|
||||
"Name": "config_is_local",
|
||||
"Help": "Use web browser to automatically authenticate rclone with remote?",
|
||||
"Default": true,
|
||||
"DefaultStr": "true",
|
||||
"IsPassword": false,
|
||||
"Examples": [
|
||||
{"Value": "true", "Help": "Yes"},
|
||||
{"Value": "false", "Help": "No"}
|
||||
]
|
||||
},
|
||||
"Error": "",
|
||||
"Result": ""
|
||||
}
|
||||
EOF
|
||||
"#;
|
||||
let (_dir, path) = write_mock_rclone(script);
|
||||
let app = RcloneApp::new_with_path(path, false);
|
||||
|
||||
let mut stderr_lines: Vec<String> = Vec::new();
|
||||
let step = config_create_step(&app, "mydrive", "drive", None, None, &mut |l| {
|
||||
stderr_lines.push(l.to_string())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match step {
|
||||
ConfigStep::Question { state, question } => {
|
||||
assert_eq!(state, "some-state");
|
||||
assert_eq!(question.name, "config_is_local");
|
||||
assert!(question.help.contains("web browser"));
|
||||
assert_eq!(question.default, "true");
|
||||
assert!(!question.is_password);
|
||||
assert_eq!(question.examples.len(), 2);
|
||||
assert_eq!(question.examples[0], ("true".to_string(), "Yes".to_string()));
|
||||
}
|
||||
_ => panic!("Ожидался вопрос, получено другое"),
|
||||
}
|
||||
assert!(stderr_lines.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn test_config_create_step_done() {
|
||||
let script = r#"#!/bin/sh
|
||||
cat <<'EOF'
|
||||
{
|
||||
"State": "",
|
||||
"Option": null,
|
||||
"Error": "",
|
||||
"Result": ""
|
||||
}
|
||||
EOF
|
||||
"#;
|
||||
let (_dir, path) = write_mock_rclone(script);
|
||||
let app = RcloneApp::new_with_path(path, false);
|
||||
|
||||
let step = config_create_step(&app, "local1", "local", None, None, &mut |_| {}).unwrap();
|
||||
assert!(matches!(step, ConfigStep::Done));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn test_config_create_step_passes_continue_args() {
|
||||
let script = r#"#!/bin/sh
|
||||
if [ "$1" = "config" ] && printf '%s\n' "$@" | grep -q -- "--continue" \
|
||||
&& printf '%s\n' "$@" | grep -q -- "--state" \
|
||||
&& printf '%s\n' "$@" | grep -q -- "--result"; then
|
||||
cat <<'EOF'
|
||||
{"State": "next", "Option": null, "Error": "", "Result": ""}
|
||||
EOF
|
||||
else
|
||||
echo "no continue args" >&2
|
||||
exit 1
|
||||
fi
|
||||
"#;
|
||||
let (_dir, path) = write_mock_rclone(script);
|
||||
let app = RcloneApp::new_with_path(path, false);
|
||||
|
||||
let step = config_create_step(
|
||||
&app,
|
||||
"mydrive",
|
||||
"drive",
|
||||
Some("st"),
|
||||
Some("answer"),
|
||||
&mut |_| {},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(step, ConfigStep::Done));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn test_config_create_step_error_from_stderr() {
|
||||
let script = r#"#!/bin/sh
|
||||
echo "Error: couldn't find backend for type \"bogus\"" >&2
|
||||
exit 1
|
||||
"#;
|
||||
let (_dir, path) = write_mock_rclone(script);
|
||||
let app = RcloneApp::new_with_path(path, false);
|
||||
|
||||
let result = config_create_step(&app, "x", "bogus", None, None, &mut |_| {});
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("couldn't find backend"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn test_config_create_step_error_from_json() {
|
||||
let script = r#"#!/bin/sh
|
||||
cat <<'EOF'
|
||||
{"State": "", "Option": null, "Error": "config already exists", "Result": ""}
|
||||
EOF
|
||||
"#;
|
||||
let (_dir, path) = write_mock_rclone(script);
|
||||
let app = RcloneApp::new_with_path(path, false);
|
||||
|
||||
let result = config_create_step(&app, "x", "drive", None, None, &mut |_| {});
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("already exists"));
|
||||
}
|
||||
+89
-13
@@ -71,24 +71,100 @@ pub fn get_config(app: &RcloneApp, remote_name: &str) -> Result<Remote, String>
|
||||
}
|
||||
}
|
||||
|
||||
/// Создать новое удаленное хранилище
|
||||
pub fn create(
|
||||
/// Вопрос, который rclone задает в процессе создания remote
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfigQuestion {
|
||||
pub name: String,
|
||||
pub help: String,
|
||||
pub default: String,
|
||||
pub is_password: bool,
|
||||
pub examples: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigStep {
|
||||
/// Создание завершено
|
||||
Done,
|
||||
/// rclone задает вопрос, нужен ответ пользователя
|
||||
Question {
|
||||
state: String,
|
||||
question: ConfigQuestion,
|
||||
},
|
||||
}
|
||||
|
||||
/// Один шаг интерактивного создания remote через `config create --non-interactive`.
|
||||
/// Вызывается в цикле: без state — первый вопрос, далее с state и ответом
|
||||
/// пользователя через `--continue`. `on_stderr` получает строки stderr
|
||||
/// (например, ссылку для авторизации в браузере).
|
||||
pub fn config_create_step(
|
||||
app: &RcloneApp,
|
||||
name: &str,
|
||||
r#type: &str,
|
||||
config: &HashMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
log::info!("Создание нового remote: {} типа {}", name, r#type);
|
||||
let mut args = vec!["config", "create", name, r#type];
|
||||
|
||||
for (key, value) in config {
|
||||
args.push(key.as_str());
|
||||
args.push(value.as_str());
|
||||
state: Option<&str>,
|
||||
result: Option<&str>,
|
||||
on_stderr: &mut dyn FnMut(&str),
|
||||
) -> Result<ConfigStep, String> {
|
||||
log::debug!("config create шаг: remote={} тип={} state={:?}", name, r#type, state);
|
||||
let mut args = vec!["config", "create", name, r#type, "--non-interactive"];
|
||||
if let Some(s) = state {
|
||||
args.extend([
|
||||
"--continue",
|
||||
"--state",
|
||||
s,
|
||||
"--result",
|
||||
result.unwrap_or_default(),
|
||||
]);
|
||||
}
|
||||
|
||||
app.run_command(&args)?;
|
||||
log::info!("Remote {} успешно создан", name);
|
||||
Ok(())
|
||||
let output = app.run_command_with_stderr_feed(&args, on_stderr)?;
|
||||
|
||||
let value: Value = serde_json::from_str(&output)
|
||||
.map_err(|e| format!("Ошибка парсинга ответа rclone: {}", e))?;
|
||||
|
||||
if let Some(err) = value.get("Error").and_then(|v| v.as_str()) {
|
||||
if !err.is_empty() {
|
||||
log::error!("rclone вернул ошибку: {}", err);
|
||||
return Err(err.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if value["Option"].is_null() {
|
||||
return Ok(ConfigStep::Done);
|
||||
}
|
||||
|
||||
let opt = &value["Option"];
|
||||
let examples = opt
|
||||
.get("Examples")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|e| {
|
||||
let val = match e.get("Value") {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(other) => other.to_string(),
|
||||
None => return None,
|
||||
};
|
||||
let help = e
|
||||
.get("Help")
|
||||
.and_then(|h| h.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
Some((val, help))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ConfigStep::Question {
|
||||
state: value["State"].as_str().unwrap_or("").to_string(),
|
||||
question: ConfigQuestion {
|
||||
name: opt["Name"].as_str().unwrap_or("").to_string(),
|
||||
help: opt["Help"].as_str().unwrap_or("").to_string(),
|
||||
default: opt["DefaultStr"].as_str().unwrap_or("").to_string(),
|
||||
is_password: opt["IsPassword"].as_bool().unwrap_or(false),
|
||||
examples,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Обновить конфигурацию remote
|
||||
|
||||
@@ -249,6 +249,65 @@ impl RcloneApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Запуск команды с передачей строк stderr в колбэк (например, для показа
|
||||
/// ссылки авторизации rclone во время OAuth).
|
||||
pub fn run_command_with_stderr_feed(
|
||||
&self,
|
||||
args: &[&str],
|
||||
mut on_stderr: impl FnMut(&str),
|
||||
) -> Result<String, String> {
|
||||
use std::io::{BufRead, Read};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
log::debug!("Выполнение команды rclone (с потоком stderr): {} {:?}", self.rclone_path.display(), args);
|
||||
|
||||
let mut child = Command::new(&self.rclone_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| {
|
||||
log::error!("Ошибка запуска rclone: {}", e);
|
||||
format!("Ошибка запуска: {}", e)
|
||||
})?;
|
||||
|
||||
let stderr = child.stderr.take().expect("stderr должен быть доступен");
|
||||
let mut stderr_text = String::new();
|
||||
{
|
||||
let reader = std::io::BufReader::new(stderr);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(l) => {
|
||||
on_stderr(l.trim());
|
||||
stderr_text.push_str(&l);
|
||||
stderr_text.push('\n');
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait().map_err(|e| format!("Ошибка ожидания rclone: {}", e))?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
if let Some(mut out) = child.stdout.take() {
|
||||
let _ = out.read_to_string(&mut stdout);
|
||||
}
|
||||
|
||||
if status.success() {
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let err = if stderr_text.trim().is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
stderr_text
|
||||
};
|
||||
log::error!("Ошибка выполнения команды: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version(&self) -> Result<String, String> {
|
||||
log::debug!("Получение версии rclone");
|
||||
self.run_command(&["version"])
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Графический интерфейс для операций rclone с интегрированным Local FS и поддержкой кастомных шрифтов.
|
||||
use crate::operations::remotes::ConfigQuestion;
|
||||
use crate::operations::{FileInfo, Remote};
|
||||
use crate::rclone_install::RcloneApp;
|
||||
use eframe::egui;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::Instant;
|
||||
@@ -32,9 +35,39 @@ enum OperationResult {
|
||||
Failure(u32, String),
|
||||
FileList(Vec<FileInfo>),
|
||||
RemoteList(Vec<Remote>),
|
||||
RemoteAdded(u32, Vec<Remote>),
|
||||
ConfigQuestion(u32, String, ConfigQuestion),
|
||||
RemoteCheck(u32, String, bool),
|
||||
RemoteAbout(u32, String, HashMap<String, u64>),
|
||||
RemoteDeleted(u32, String),
|
||||
ProgressUpdate(u32, f32, String),
|
||||
}
|
||||
|
||||
/// Результат проверки или информации о хранилище для окна
|
||||
pub struct RemoteInfoView {
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// Шаг мастера добавления облака
|
||||
#[derive(Clone)]
|
||||
pub enum AddRemoteStep {
|
||||
/// Ввод имени и типа
|
||||
Form,
|
||||
/// rclone задал вопрос
|
||||
Question(ConfigQuestion),
|
||||
/// Команда выполняется в фоне (например, ожидание авторизации в браузере)
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Популярные типы удаленных хранилищ rclone
|
||||
pub const REMOTE_TYPES: &[&str] = &[
|
||||
"drive", "dropbox", "onedrive", "s3", "gcs", "b2", "azureblob",
|
||||
"webdav", "sftp", "ftp", "smb", "local", "crypt", "http", "mega",
|
||||
"pcloud", "box", "googlephotos", "jottacloud", "koofr",
|
||||
];
|
||||
|
||||
pub struct RcloneUI {
|
||||
rclone: Option<Arc<RcloneApp>>,
|
||||
state: AppState,
|
||||
@@ -61,6 +94,19 @@ pub struct RcloneUI {
|
||||
#[allow(dead_code)]
|
||||
settings: AppSettings,
|
||||
|
||||
show_add_remote_dialog: bool,
|
||||
new_remote_name: String,
|
||||
new_remote_type: String,
|
||||
add_remote_step: AddRemoteStep,
|
||||
add_remote_state: Option<String>,
|
||||
add_remote_answer: String,
|
||||
add_remote_status: String,
|
||||
|
||||
remote_to_delete: Option<String>,
|
||||
remote_info: Option<RemoteInfoView>,
|
||||
context_menu: Option<(String, egui::Pos2)>,
|
||||
context_menu_requested: bool,
|
||||
|
||||
active_task_count: u32,
|
||||
operation_tx: Sender<OperationResult>,
|
||||
operation_rx: Receiver<OperationResult>,
|
||||
|
||||
+280
-8
@@ -7,6 +7,14 @@ use egui::{
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Действия контекстного меню хранилища
|
||||
enum RemoteMenuAction {
|
||||
Open,
|
||||
Check,
|
||||
About,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl eframe::App for RcloneUI {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_rclone_init();
|
||||
@@ -42,16 +50,28 @@ impl eframe::App for RcloneUI {
|
||||
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);
|
||||
let selected = self.current_path.starts_with(&remote.name);
|
||||
let response =
|
||||
ui.selectable_label(selected, format!("📡 {}", remote.name));
|
||||
if response.clicked() {
|
||||
log::debug!("Открытие remote: {}", remote.name);
|
||||
self.navigate_to(format!("{}:", remote.name));
|
||||
}
|
||||
if response.secondary_clicked() {
|
||||
log::debug!("Контекстное меню remote: {}", remote.name);
|
||||
let pos = ctx
|
||||
.input(|i| i.pointer.interact_pos())
|
||||
.unwrap_or_default();
|
||||
self.context_menu = Some((remote.name.clone(), pos));
|
||||
self.context_menu_requested = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.add_enabled_ui(self.rclone.is_some(), |ui| {
|
||||
if ui.button("➕ Добавить облако").clicked() {
|
||||
log::debug!("Открытие диалога добавления облака");
|
||||
self.show_add_remote_dialog = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -170,6 +190,134 @@ impl eframe::App for RcloneUI {
|
||||
}
|
||||
});
|
||||
|
||||
// Диалог добавления нового облака
|
||||
if self.show_add_remote_dialog {
|
||||
Window::new("Новое облако")
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.fixed_size([480.0, 420.0])
|
||||
.show(ctx, |ui| {
|
||||
match self.add_remote_step.clone() {
|
||||
AddRemoteStep::Form => {
|
||||
ui.label("Имя:");
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.new_remote_name)
|
||||
.hint_text("например: mydrive")
|
||||
.desired_width(240.0),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label("Тип:");
|
||||
egui::ComboBox::from_id_source("remote_type")
|
||||
.selected_text(self.new_remote_type.as_str())
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for t in REMOTE_TYPES {
|
||||
ui.selectable_value(
|
||||
&mut self.new_remote_type,
|
||||
t.to_string(),
|
||||
*t,
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
"Для облаков с авторизацией (Google Drive, Dropbox и др.) при создании откроется браузер для входа в аккаунт.",
|
||||
)
|
||||
.wrap(true),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
let can_create = !self.new_remote_name.trim().is_empty()
|
||||
&& !self.new_remote_type.trim().is_empty()
|
||||
&& self.active_task_count == 0;
|
||||
if ui
|
||||
.add_enabled(
|
||||
can_create,
|
||||
egui::Button::new("✅ Создать"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
log::debug!("Кнопка 'Создать' нажата");
|
||||
self.start_add_remote();
|
||||
}
|
||||
if ui.button("Отмена").clicked() {
|
||||
self.show_add_remote_dialog = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
AddRemoteStep::Question(question) => {
|
||||
ui.heading(format!("❓ {}", question.name));
|
||||
ui.add(egui::Label::new(&question.help).wrap(true));
|
||||
if !question.default.is_empty() {
|
||||
ui.weak(format!("По умолчанию: {}", question.default));
|
||||
}
|
||||
if !question.examples.is_empty() {
|
||||
ui.add_space(6.0);
|
||||
ui.label("Варианты:");
|
||||
for (value, help) in &question.examples {
|
||||
let selected = self.add_remote_answer == *value;
|
||||
let label = if help.is_empty() {
|
||||
value.clone()
|
||||
} else {
|
||||
format!("{} — {}", value, help)
|
||||
};
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
self.add_remote_answer = value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.add_remote_answer)
|
||||
.password(question.is_password)
|
||||
.hint_text(if question.is_password { "секрет" } else { "ответ" })
|
||||
.desired_width(320.0),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(
|
||||
self.active_task_count == 0,
|
||||
egui::Button::new("✅ Ответить"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
log::debug!("Ответ на вопрос rclone отправлен");
|
||||
self.answer_add_remote();
|
||||
}
|
||||
if ui.button("Отмена").clicked() {
|
||||
self.show_add_remote_dialog = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
AddRemoteStep::Busy => {
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::Spinner::new());
|
||||
ui.label(&self.add_remote_status);
|
||||
});
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
"Если страница авторизации не открылась — скопируйте ссылку из статуса ниже в браузер.",
|
||||
)
|
||||
.wrap(true),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Отмена").clicked() {
|
||||
self.show_add_remote_dialog = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Модальное окно трансфера
|
||||
if self.show_transfer_dialog {
|
||||
Window::new("Трансфер")
|
||||
@@ -345,6 +493,130 @@ impl eframe::App for RcloneUI {
|
||||
}
|
||||
}
|
||||
|
||||
// Подтверждение удаления хранилища
|
||||
if let Some(remote_name) = self.remote_to_delete.clone() {
|
||||
let mut close = false;
|
||||
let mut confirm = false;
|
||||
Window::new("Удаление хранилища")
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.fixed_size([430.0, 170.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(format!("Удалить хранилище «{}»?", remote_name));
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
"Файлы в облаке не удаляются — удаляется только запись из конфигурации rclone.",
|
||||
)
|
||||
.wrap(true),
|
||||
);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("🗑️ Удалить").clicked() {
|
||||
confirm = true;
|
||||
}
|
||||
if ui.button("Отмена").clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
if close {
|
||||
self.remote_to_delete = None;
|
||||
}
|
||||
if confirm {
|
||||
self.remote_to_delete = None;
|
||||
self.delete_remote(remote_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Окно с результатом проверки или информацией о хранилище
|
||||
if let Some(info) = &self.remote_info {
|
||||
let mut close = false;
|
||||
Window::new(&info.title)
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.fixed_size([430.0, 260.0])
|
||||
.show(ctx, |ui| {
|
||||
let color = if info.success {
|
||||
Color32::LIGHT_GREEN
|
||||
} else {
|
||||
Color32::KHAKI
|
||||
};
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(&info.content).color(color))
|
||||
.wrap(true),
|
||||
);
|
||||
ui.separator();
|
||||
if ui.button("ОК").clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
if close {
|
||||
self.remote_info = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Контекстное меню хранилища (правая кнопка мыши)
|
||||
if let Some((menu_name, menu_pos)) = self.context_menu.clone() {
|
||||
let remote_type = self
|
||||
.remote_list
|
||||
.iter()
|
||||
.find(|r| r.name == menu_name)
|
||||
.map(|r| r.r#type.clone())
|
||||
.unwrap_or_default();
|
||||
let mut action: Option<RemoteMenuAction> = None;
|
||||
Window::new("remote_menu")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.fixed_pos(menu_pos)
|
||||
.show(ctx, |ui| {
|
||||
ui.weak(format!("Тип: {}", remote_type));
|
||||
ui.separator();
|
||||
if ui.button("📂 Открыть").clicked() {
|
||||
action = Some(RemoteMenuAction::Open);
|
||||
}
|
||||
if ui.button("🔍 Проверить доступность").clicked() {
|
||||
action = Some(RemoteMenuAction::Check);
|
||||
}
|
||||
if ui.button("📊 Использование").clicked() {
|
||||
action = Some(RemoteMenuAction::About);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("🗑️ Удалить").clicked() {
|
||||
action = Some(RemoteMenuAction::Delete);
|
||||
}
|
||||
});
|
||||
if let Some(action) = action {
|
||||
self.context_menu = None;
|
||||
match action {
|
||||
RemoteMenuAction::Open => {
|
||||
log::debug!("Открытие remote из меню: {}", menu_name);
|
||||
self.navigate_to(format!("{}:", menu_name));
|
||||
}
|
||||
RemoteMenuAction::Check => {
|
||||
log::debug!("Проверка remote из меню: {}", menu_name);
|
||||
self.check_remote(menu_name.clone());
|
||||
}
|
||||
RemoteMenuAction::About => {
|
||||
log::debug!("Использование remote из меню: {}", menu_name);
|
||||
self.about_remote(menu_name.clone());
|
||||
}
|
||||
RemoteMenuAction::Delete => {
|
||||
log::debug!("Запрос удаления remote из меню: {}", menu_name);
|
||||
self.remote_to_delete = Some(menu_name.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let just_opened = std::mem::take(&mut self.context_menu_requested);
|
||||
if !just_opened && ctx.input(|i| i.pointer.any_click()) {
|
||||
log::debug!("Контекстное меню закрыто");
|
||||
self.context_menu = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = self.pending_load_path.take() {
|
||||
self.current_path = path.clone();
|
||||
self.selected_paths.clear();
|
||||
|
||||
+268
-1
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::operations::remotes::ConfigStep;
|
||||
use crate::operations::{self, CopyOptions, DeleteOptions, MoveOptions};
|
||||
use crate::rclone_install::RcloneApp;
|
||||
use eframe::egui;
|
||||
@@ -9,6 +10,19 @@ use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/// Форматирование размера в человекочитаемый вид
|
||||
pub fn format_bytes(bytes: u64) -> String {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
impl RcloneUI {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
log::debug!("Инициализация RcloneUI");
|
||||
@@ -88,6 +102,17 @@ impl RcloneUI {
|
||||
local_browser_files: Vec::new(),
|
||||
is_move_mode: false,
|
||||
settings: AppSettings::default(),
|
||||
show_add_remote_dialog: false,
|
||||
new_remote_name: String::new(),
|
||||
new_remote_type: "drive".to_string(),
|
||||
add_remote_step: AddRemoteStep::Form,
|
||||
add_remote_state: None,
|
||||
add_remote_answer: String::new(),
|
||||
add_remote_status: String::new(),
|
||||
remote_to_delete: None,
|
||||
remote_info: None,
|
||||
context_menu: None,
|
||||
context_menu_requested: false,
|
||||
active_task_count: 0,
|
||||
operation_tx: op_tx,
|
||||
operation_rx: op_rx,
|
||||
@@ -173,8 +198,174 @@ impl RcloneUI {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_remotes(&mut self) {
|
||||
/// Фоновый цикл создания remote: rclone задает вопросы (--non-interactive),
|
||||
/// на вопрос config_is_local автоматически отвечаем "true" — открывается
|
||||
/// браузер для авторизации; остальные вопросы передаются в UI.
|
||||
fn run_add_remote_loop(
|
||||
rclone: Arc<RcloneApp>,
|
||||
name: String,
|
||||
rtype: String,
|
||||
initial_state: Option<String>,
|
||||
initial_answer: Option<String>,
|
||||
op_id: u32,
|
||||
tx: Sender<OperationResult>,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let mut state = initial_state;
|
||||
let mut answer = initial_answer;
|
||||
loop {
|
||||
let feed_tx = tx.clone();
|
||||
let step = operations::remotes::config_create_step(
|
||||
&rclone,
|
||||
&name,
|
||||
&rtype,
|
||||
state.as_deref(),
|
||||
answer.as_deref(),
|
||||
&mut |line: &str| {
|
||||
if !line.is_empty() {
|
||||
let _ = feed_tx.send(OperationResult::ProgressUpdate(
|
||||
op_id,
|
||||
0.0,
|
||||
line.to_string(),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
match step {
|
||||
Ok(ConfigStep::Done) => {
|
||||
log::info!("Remote {} успешно создан", name);
|
||||
let _ = tx.send(match operations::remotes::list(&rclone) {
|
||||
Ok(list) => OperationResult::RemoteAdded(op_id, list),
|
||||
Err(e) => OperationResult::Failure(
|
||||
op_id,
|
||||
format!("Облако создано, но список обновить не удалось: {}", e),
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
Ok(ConfigStep::Question { state: s, question: q }) => {
|
||||
if q.name == "config_is_local" {
|
||||
log::info!("Открытие браузера для авторизации {}", rtype);
|
||||
state = Some(s);
|
||||
answer = Some("true".to_string());
|
||||
let _ = tx.send(OperationResult::ProgressUpdate(
|
||||
op_id,
|
||||
0.0,
|
||||
"Открыт браузер для авторизации. Ожидание входа...".into(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
log::debug!("rclone задал вопрос: {}", q.name);
|
||||
let _ = tx.send(OperationResult::ConfigQuestion(op_id, s, q));
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Ошибка создания remote {}: {}", name, e);
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_add_remote(&mut self) {
|
||||
let name = self.new_remote_name.trim().to_string();
|
||||
let rtype = self.new_remote_type.trim().to_string();
|
||||
if name.is_empty() || rtype.is_empty() {
|
||||
log::warn!("Не удалось создать remote: пустое имя или тип");
|
||||
return;
|
||||
}
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Создание нового remote: {} типа {}", name, rtype);
|
||||
self.active_task_count += 1;
|
||||
self.add_remote_step = AddRemoteStep::Busy;
|
||||
self.add_remote_status = "Создание облака...".into();
|
||||
let op_id = self.add_operation(format!("Создание облака {}", name));
|
||||
Self::run_add_remote_loop(rclone, name, rtype, None, None, op_id, self.operation_tx.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn answer_add_remote(&mut self) {
|
||||
if let (Some(rclone), Some(state)) = (self.rclone.clone(), self.add_remote_state.clone()) {
|
||||
let answer = self.add_remote_answer.clone();
|
||||
let name = self.new_remote_name.clone();
|
||||
let rtype = self.new_remote_type.clone();
|
||||
log::info!("Ответ на вопрос rclone: {}", answer);
|
||||
self.active_task_count += 1;
|
||||
self.add_remote_step = AddRemoteStep::Busy;
|
||||
self.add_remote_status = "Отправка ответа...".into();
|
||||
let op_id = self.add_operation(format!("Создание облака {}", name));
|
||||
Self::run_add_remote_loop(rclone, name, rtype, Some(state), Some(answer), op_id, self.operation_tx.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверить доступность хранилища
|
||||
pub fn check_remote(&mut self, name: String) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Проверка доступности remote: {}", name);
|
||||
self.active_task_count += 1;
|
||||
let op_id = self.add_operation(format!("Проверка {}", name));
|
||||
let tx = self.operation_tx.clone();
|
||||
let remote_path = format!("{}:", name);
|
||||
std::thread::spawn(move || {
|
||||
match operations::remotes::check(&rclone, &remote_path) {
|
||||
Ok(true) => {
|
||||
let _ = tx.send(OperationResult::RemoteCheck(op_id, name, true));
|
||||
}
|
||||
Ok(false) => {
|
||||
let _ = tx.send(OperationResult::RemoteCheck(op_id, name, false));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить информацию об использовании хранилища
|
||||
pub fn about_remote(&mut self, name: String) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Получение информации об использовании remote: {}", name);
|
||||
self.active_task_count += 1;
|
||||
let op_id = self.add_operation(format!("Использование {}", name));
|
||||
let tx = self.operation_tx.clone();
|
||||
let remote_path = format!("{}:", name);
|
||||
std::thread::spawn(move || {
|
||||
match operations::files::about(&rclone, &remote_path) {
|
||||
Ok(info) => {
|
||||
let _ = tx.send(OperationResult::RemoteAbout(op_id, name, info));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Удалить хранилище из конфигурации rclone
|
||||
pub fn delete_remote(&mut self, name: String) {
|
||||
if let Some(rclone) = self.rclone.clone() {
|
||||
log::info!("Удаление remote: {}", name);
|
||||
self.active_task_count += 1;
|
||||
let op_id = self.add_operation(format!("Удаление {}", name));
|
||||
let tx = self.operation_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
match operations::remotes::delete(&rclone, &name) {
|
||||
Ok(()) => {
|
||||
let _ = tx.send(OperationResult::RemoteDeleted(op_id, name));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(OperationResult::Failure(op_id, e));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -229,6 +420,9 @@ impl RcloneUI {
|
||||
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);
|
||||
if matches!(self.add_remote_step, AddRemoteStep::Busy) {
|
||||
self.add_remote_step = AddRemoteStep::Form;
|
||||
}
|
||||
self.error_message = Some(e);
|
||||
}
|
||||
OperationResult::FileList(files) => {
|
||||
@@ -243,12 +437,85 @@ impl RcloneUI {
|
||||
self.remote_list = remotes;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
}
|
||||
OperationResult::RemoteAdded(op_id, remotes) => {
|
||||
let count = remotes.len();
|
||||
log::info!("Облако добавлено, список обновлен: {} элементов", count);
|
||||
self.remote_list = remotes;
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.show_add_remote_dialog = false;
|
||||
self.add_remote_step = AddRemoteStep::Form;
|
||||
self.add_remote_state = None;
|
||||
self.new_remote_name.clear();
|
||||
self.add_remote_answer.clear();
|
||||
}
|
||||
OperationResult::ConfigQuestion(op_id, state, question) => {
|
||||
log::debug!("Получен вопрос rclone: {}", question.name);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.add_remote_state = Some(state);
|
||||
self.add_remote_answer = question.default.clone();
|
||||
self.add_remote_step = AddRemoteStep::Question(question);
|
||||
}
|
||||
OperationResult::RemoteCheck(op_id, name, ok) => {
|
||||
log::debug!("Проверка remote {} завершена: доступен={}", name, ok);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.remote_info = Some(RemoteInfoView {
|
||||
title: format!("Проверка {}", name),
|
||||
content: if ok {
|
||||
"Хранилище доступно".to_string()
|
||||
} else {
|
||||
"Хранилище недоступно".to_string()
|
||||
},
|
||||
success: ok,
|
||||
});
|
||||
}
|
||||
OperationResult::RemoteAbout(op_id, name, info) => {
|
||||
log::debug!("Информация о remote {} получена", name);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
let mut lines = Vec::new();
|
||||
for (key, value) in &info {
|
||||
match key.as_str() {
|
||||
"total" => lines.push(format!("Общий объем: {}", format_bytes(*value))),
|
||||
"used" => lines.push(format!("Использовано: {}", format_bytes(*value))),
|
||||
"free" => lines.push(format!("Свободно: {}", format_bytes(*value))),
|
||||
"trashed" => lines.push(format!("В корзине: {}", format_bytes(*value))),
|
||||
"objects" => lines.push(format!("Файлов: {}", value)),
|
||||
"versions" => lines.push(format!("Версий: {}", value)),
|
||||
_ => lines.push(format!("{}: {}", key, format_bytes(*value))),
|
||||
}
|
||||
}
|
||||
self.remote_info = Some(RemoteInfoView {
|
||||
title: format!("Использование {}", name),
|
||||
content: lines.join("\n"),
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
OperationResult::RemoteDeleted(op_id, name) => {
|
||||
log::info!("Remote {} удален", name);
|
||||
self.active_task_count = self.active_task_count.saturating_sub(1);
|
||||
self.active_operations.retain(|op| op.id != op_id);
|
||||
self.remote_to_delete = None;
|
||||
if self.current_path.starts_with(&name) {
|
||||
self.current_path.clear();
|
||||
self.current_files.clear();
|
||||
self.selected_paths.clear();
|
||||
self.history_back.clear();
|
||||
self.history_forward.clear();
|
||||
}
|
||||
self.load_remotes();
|
||||
}
|
||||
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);
|
||||
}
|
||||
if matches!(&self.add_remote_step, AddRemoteStep::Busy) {
|
||||
self.add_remote_status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user