mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat(auto-backup): implement centralized auto-backup manager and UI (#5374)
* feat(auto-backup): implement centralized auto-backup manager and UI - Introduced AutoBackupManager to handle verge settings, run a background scheduler, debounce change-driven backups, and trim auto-labeled archives (keeps 20); wired into startup and config refresh hooks (src-tauri/src/module/auto_backup.rs:28-209, src-tauri/src/utils/resolve/mod.rs:64-136, src-tauri/src/feat/config.rs:102-238) - Extended verge schema and backup helpers so scheduled/change-based settings persist, create_local_backup can rename archives, and profile/global-extend mutations now trigger backups (src-tauri/src/config/verge.rs:162-536, src/types/types.d.ts:857-859, src-tauri/src/feat/backup.rs:125-189, src-tauri/src/cmd/profile.rs:66-476, src-tauri/src/cmd/save_profile.rs:21-82) - Added Auto Backup settings panel in backup dialog with dual toggles + interval selector; localized new strings across all locales (src/components/setting/mods/auto-backup-settings.tsx:1-138, src/components/setting/mods/backup-viewer.tsx:28-309, src/locales/en/settings.json:312-326 and mirrored entries) - Regenerated typed i18n resources for strong typing in React (src/types/generated/i18n-keys.ts, src/types/generated/i18n-resources.ts) * refactor(setting/backup): restructure backup dialog for consistent layout * refactor(ui): unify settings dialog style * fix(backup): only trigger auto-backup on valid saves & restore restarts app safely * fix(backup): scrub console.log leak and rewire WebDAV dialog to actually probe server * refactor: rename SubscriptionChange to ProfileChange * chore: update i18n * chore: WebDAV i18n improvements * refactor(backup): error handling * refactor(auto-backup): wrap scheduler startup with maybe_start_runner * refactor: remove the redundant throw in handleExport * feat(backup-history-viewer): improve WebDAV handling and UI fallback * feat(auto-backup): trigger backups on all profile edits & improve interval input UX * refactor: use InputAdornment * docs: Changelog.md
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
- **Mihomo(Meta) 内核升级至 v1.19.16**
|
- **Mihomo(Meta) 内核升级至 v1.19.16**
|
||||||
- 支持连接页面各个项目的排序
|
- 支持连接页面各个项目的排序
|
||||||
|
- 实现可选的自动备份
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
- 优化后端内存和性能表现
|
- 优化后端内存和性能表现
|
||||||
- 防止退出时可能的禁用 TUN 失败
|
- 防止退出时可能的禁用 TUN 失败
|
||||||
- i18n 支持
|
- i18n 支持
|
||||||
|
- 优化备份设置布局
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
||||||
feat, logging,
|
feat, logging,
|
||||||
|
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
ret_err,
|
ret_err,
|
||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
@@ -90,6 +91,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
|
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
|
||||||
|
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
|||||||
handle::Handle::notify_profile_changed(uid.clone());
|
handle::Handle::notify_profile_changed(uid.clone());
|
||||||
}
|
}
|
||||||
Config::profiles().await.apply();
|
Config::profiles().await.apply();
|
||||||
|
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -164,6 +167,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
// 发送配置变更通知
|
// 发送配置变更通知
|
||||||
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
|
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
|
||||||
handle::Handle::notify_profile_changed(index);
|
handle::Handle::notify_profile_changed(index);
|
||||||
|
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Cmd, "{}", e);
|
logging!(error, Type::Cmd, "{}", e);
|
||||||
@@ -460,6 +464,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
config::{Config, PrfItem},
|
config::{Config, PrfItem},
|
||||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||||
logging,
|
logging,
|
||||||
|
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||||
utils::{dirs, logging::Type},
|
utils::{dirs, logging::Type},
|
||||||
};
|
};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
@@ -17,6 +18,12 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let backup_trigger = match index.as_str() {
|
||||||
|
"Merge" => Some(AutoBackupTrigger::GlobalMerge),
|
||||||
|
"Script" => Some(AutoBackupTrigger::GlobalScript),
|
||||||
|
_ => Some(AutoBackupTrigger::ProfileChange),
|
||||||
|
};
|
||||||
|
|
||||||
// 在异步操作前获取必要元数据并释放锁
|
// 在异步操作前获取必要元数据并释放锁
|
||||||
let (rel_path, is_merge_file) = {
|
let (rel_path, is_merge_file) = {
|
||||||
let profiles = Config::profiles().await;
|
let profiles = Config::profiles().await;
|
||||||
@@ -51,11 +58,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
is_merge_file
|
is_merge_file
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_merge_file {
|
let changes_applied = if is_merge_file {
|
||||||
return handle_merge_file(&file_path_str, &file_path, &original_content).await;
|
handle_merge_file(&file_path_str, &file_path, &original_content).await?
|
||||||
|
} else {
|
||||||
|
handle_full_validation(&file_path_str, &file_path, &original_content).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if changes_applied && let Some(trigger) = backup_trigger {
|
||||||
|
AutoBackupManager::trigger_backup(trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_full_validation(&file_path_str, &file_path, &original_content).await
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_original(
|
async fn restore_original(
|
||||||
@@ -76,7 +89,7 @@ async fn handle_merge_file(
|
|||||||
file_path_str: &str,
|
file_path_str: &str,
|
||||||
file_path: &std::path::Path,
|
file_path: &std::path::Path,
|
||||||
original_content: &str,
|
original_content: &str,
|
||||||
) -> CmdResult {
|
) -> CmdResult<bool> {
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Config,
|
Type::Config,
|
||||||
@@ -96,7 +109,7 @@ async fn handle_merge_file(
|
|||||||
} else {
|
} else {
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -108,7 +121,7 @@ async fn handle_merge_file(
|
|||||||
restore_original(file_path, original_content).await?;
|
restore_original(file_path, original_content).await?;
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||||
Ok(())
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
@@ -122,11 +135,11 @@ async fn handle_full_validation(
|
|||||||
file_path_str: &str,
|
file_path_str: &str,
|
||||||
file_path: &std::path::Path,
|
file_path: &std::path::Path,
|
||||||
original_content: &str,
|
original_content: &str,
|
||||||
) -> CmdResult {
|
) -> CmdResult<bool> {
|
||||||
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
|
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
logging!(info, Type::Config, "[cmd配置save] 验证成功");
|
logging!(info, Type::Config, "[cmd配置save] 验证成功");
|
||||||
Ok(())
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
|
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
|
||||||
@@ -160,7 +173,7 @@ async fn handle_full_validation(
|
|||||||
handle::Handle::notice_message("config_validate::error", error_msg.to_owned());
|
handle::Handle::notice_message("config_validate::error", error_msg.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
|
|||||||
@@ -158,6 +158,15 @@ pub struct IVerge {
|
|||||||
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
|
|
||||||
|
/// Enable scheduled automatic backups
|
||||||
|
pub enable_auto_backup_schedule: Option<bool>,
|
||||||
|
|
||||||
|
/// Automatic backup interval in hours
|
||||||
|
pub auto_backup_interval_hours: Option<u64>,
|
||||||
|
|
||||||
|
/// Create backups automatically when critical configs change
|
||||||
|
pub auto_backup_on_change: Option<bool>,
|
||||||
|
|
||||||
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub verge_redir_port: Option<u16>,
|
pub verge_redir_port: Option<u16>,
|
||||||
@@ -422,6 +431,9 @@ impl IVerge {
|
|||||||
auto_check_update: Some(true),
|
auto_check_update: Some(true),
|
||||||
enable_builtin_enhanced: Some(true),
|
enable_builtin_enhanced: Some(true),
|
||||||
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
|
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
|
||||||
|
enable_auto_backup_schedule: Some(false),
|
||||||
|
auto_backup_interval_hours: Some(24),
|
||||||
|
auto_backup_on_change: Some(true),
|
||||||
webdav_url: None,
|
webdav_url: None,
|
||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
@@ -517,6 +529,9 @@ impl IVerge {
|
|||||||
patch!(proxy_layout_column);
|
patch!(proxy_layout_column);
|
||||||
patch!(test_list);
|
patch!(test_list);
|
||||||
patch!(auto_log_clean);
|
patch!(auto_log_clean);
|
||||||
|
patch!(enable_auto_backup_schedule);
|
||||||
|
patch!(auto_backup_interval_hours);
|
||||||
|
patch!(auto_backup_on_change);
|
||||||
|
|
||||||
patch!(webdav_url);
|
patch!(webdav_url);
|
||||||
patch!(webdav_username);
|
patch!(webdav_username);
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
|||||||
|
|
||||||
/// Create a backup and save to local storage
|
/// Create a backup and save to local storage
|
||||||
pub async fn create_local_backup() -> Result<()> {
|
pub async fn create_local_backup() -> Result<()> {
|
||||||
|
create_local_backup_with_namer(|name| name.to_string().into())
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_local_backup_with_namer<F>(namer: F) -> Result<String>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> String,
|
||||||
|
{
|
||||||
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
@@ -133,7 +142,8 @@ pub async fn create_local_backup() -> Result<()> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let backup_dir = local_backup_dir()?;
|
let backup_dir = local_backup_dir()?;
|
||||||
let target_path = backup_dir.join(file_name.as_str());
|
let final_name = namer(file_name.as_str());
|
||||||
|
let target_path = backup_dir.join(final_name.as_str());
|
||||||
|
|
||||||
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
|
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -152,7 +162,7 @@ pub async fn create_local_backup() -> Result<()> {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(final_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{CoreManager, handle, hotkey, sysopt, tray},
|
core::{CoreManager, handle, hotkey, sysopt, tray},
|
||||||
logging_error,
|
logging_error,
|
||||||
module::lightweight,
|
module::{auto_backup::AutoBackupManager, lightweight},
|
||||||
utils::{draft::SharedBox, logging::Type},
|
utils::{draft::SharedBox, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -243,6 +243,10 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
Config::verge().await.apply();
|
Config::verge().await.apply();
|
||||||
|
logging_error!(
|
||||||
|
Type::Backup,
|
||||||
|
AutoBackupManager::global().refresh_settings().await
|
||||||
|
);
|
||||||
if !not_save_file {
|
if !not_save_file {
|
||||||
// 分离数据获取和异步调用
|
// 分离数据获取和异步调用
|
||||||
let verge_data = Config::verge().await.data_arc();
|
let verge_data = Config::verge().await.data_arc();
|
||||||
|
|||||||
332
src-tauri/src/module/auto_backup.rs
Normal file
332
src-tauri/src/module/auto_backup.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
use crate::{
|
||||||
|
config::{Config, IVerge},
|
||||||
|
feat::create_local_backup_with_namer,
|
||||||
|
logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{dirs::local_backup_dir, logging::Type},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::Local;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
fs,
|
||||||
|
sync::{Mutex, watch},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_INTERVAL_HOURS: u64 = 24;
|
||||||
|
const MIN_INTERVAL_HOURS: u64 = 1;
|
||||||
|
const MAX_INTERVAL_HOURS: u64 = 168;
|
||||||
|
const MIN_BACKUP_INTERVAL_SECS: i64 = 60;
|
||||||
|
const AUTO_BACKUP_KEEP: usize = 20;
|
||||||
|
const AUTO_MARKER: &str = "-auto-";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum AutoBackupTrigger {
|
||||||
|
Scheduled,
|
||||||
|
GlobalMerge,
|
||||||
|
GlobalScript,
|
||||||
|
ProfileChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoBackupTrigger {
|
||||||
|
const fn slug(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Scheduled => "scheduled",
|
||||||
|
Self::GlobalMerge => "merge",
|
||||||
|
Self::GlobalScript => "script",
|
||||||
|
Self::ProfileChange => "profile",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn is_schedule(self) -> bool {
|
||||||
|
matches!(self, Self::Scheduled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct AutoBackupSettings {
|
||||||
|
schedule_enabled: bool,
|
||||||
|
interval_hours: u64,
|
||||||
|
change_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoBackupSettings {
|
||||||
|
fn from_verge(verge: &IVerge) -> Self {
|
||||||
|
let interval = verge
|
||||||
|
.auto_backup_interval_hours
|
||||||
|
.unwrap_or(DEFAULT_INTERVAL_HOURS)
|
||||||
|
.clamp(MIN_INTERVAL_HOURS, MAX_INTERVAL_HOURS);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
schedule_enabled: verge.enable_auto_backup_schedule.unwrap_or(false),
|
||||||
|
interval_hours: interval,
|
||||||
|
change_enabled: verge.auto_backup_on_change.unwrap_or(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AutoBackupSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
schedule_enabled: false,
|
||||||
|
interval_hours: DEFAULT_INTERVAL_HOURS,
|
||||||
|
change_enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AutoBackupManager {
|
||||||
|
settings: Arc<RwLock<AutoBackupSettings>>,
|
||||||
|
settings_tx: watch::Sender<AutoBackupSettings>,
|
||||||
|
runner_started: AtomicBool,
|
||||||
|
exec_lock: Mutex<()>,
|
||||||
|
last_backup: AtomicI64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoBackupManager {
|
||||||
|
pub fn global() -> &'static Self {
|
||||||
|
static INSTANCE: OnceCell<AutoBackupManager> = OnceCell::new();
|
||||||
|
INSTANCE.get_or_init(|| {
|
||||||
|
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
||||||
|
Self {
|
||||||
|
settings: Arc::new(RwLock::new(AutoBackupSettings::default())),
|
||||||
|
settings_tx: tx,
|
||||||
|
runner_started: AtomicBool::new(false),
|
||||||
|
exec_lock: Mutex::new(()),
|
||||||
|
last_backup: AtomicI64::new(0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&self) -> Result<()> {
|
||||||
|
let settings = Self::load_settings().await;
|
||||||
|
{
|
||||||
|
*self.settings.write() = settings;
|
||||||
|
}
|
||||||
|
let _ = self.settings_tx.send(settings);
|
||||||
|
self.maybe_start_runner(settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_settings(&self) -> Result<()> {
|
||||||
|
let settings = Self::load_settings().await;
|
||||||
|
{
|
||||||
|
*self.settings.write() = settings;
|
||||||
|
}
|
||||||
|
let _ = self.settings_tx.send(settings);
|
||||||
|
self.maybe_start_runner(settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_backup(trigger: AutoBackupTrigger) {
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
if let Err(err) = Self::global().execute_trigger(trigger).await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Backup,
|
||||||
|
"Auto backup execution failed ({:?}): {err:#?}",
|
||||||
|
trigger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_start_runner(&self, settings: AutoBackupSettings) {
|
||||||
|
if settings.schedule_enabled {
|
||||||
|
self.ensure_runner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_runner(&self) {
|
||||||
|
if self.runner_started.swap(true, Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rx = self.settings_tx.subscribe();
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
Self::run_scheduler(&mut rx).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_scheduler(rx: &mut watch::Receiver<AutoBackupSettings>) {
|
||||||
|
let mut current = *rx.borrow();
|
||||||
|
loop {
|
||||||
|
if !current.schedule_enabled {
|
||||||
|
if rx.changed().await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = *rx.borrow();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = Duration::from_secs(current.interval_hours.saturating_mul(3600));
|
||||||
|
let sleeper = tokio::time::sleep(duration);
|
||||||
|
tokio::pin!(sleeper);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut sleeper => {
|
||||||
|
if let Err(err) = Self::global()
|
||||||
|
.execute_trigger(AutoBackupTrigger::Scheduled)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Backup,
|
||||||
|
"Scheduled auto backup failed: {err:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = rx.changed() => {
|
||||||
|
if changed.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = *rx.borrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> {
|
||||||
|
let snapshot = *self.settings.read();
|
||||||
|
|
||||||
|
if trigger.is_schedule() && !snapshot.schedule_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !trigger.is_schedule() && !snapshot.change_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.should_run_now() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _guard = self.exec_lock.lock().await;
|
||||||
|
if !self.should_run_now() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name =
|
||||||
|
create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into())
|
||||||
|
.await?;
|
||||||
|
self.last_backup
|
||||||
|
.store(Local::now().timestamp(), Ordering::Release);
|
||||||
|
|
||||||
|
if let Err(err) = cleanup_auto_backups().await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Backup,
|
||||||
|
"Failed to cleanup old auto backups: {err:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Backup,
|
||||||
|
"Auto backup created ({:?}): {}",
|
||||||
|
trigger,
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_run_now(&self) -> bool {
|
||||||
|
let last = self.last_backup.load(Ordering::Acquire);
|
||||||
|
if last == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let now = Local::now().timestamp();
|
||||||
|
now.saturating_sub(last) >= MIN_BACKUP_INTERVAL_SECS
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_settings() -> AutoBackupSettings {
|
||||||
|
let verge = Config::verge().await;
|
||||||
|
AutoBackupSettings::from_verge(&verge.latest_arc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
|
||||||
|
match file_name.rsplit_once('.') {
|
||||||
|
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
|
||||||
|
None => format!("{file_name}{AUTO_MARKER}{slug}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup_auto_backups() -> Result<()> {
|
||||||
|
if AUTO_BACKUP_KEEP == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let backup_dir = local_backup_dir()?;
|
||||||
|
if !backup_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = match fs::read_dir(&backup_dir).await {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(err) => {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Backup,
|
||||||
|
"Failed to read backup directory: {err:#?}"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut files: Vec<(PathBuf, u64)> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = match entry.file_name().into_string() {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !file_name.contains(AUTO_MARKER) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = entry
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.and_then(|meta| meta.modified())
|
||||||
|
.ok()
|
||||||
|
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||||
|
.map(|dur| dur.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
files.push((path, modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
if files.len() <= AUTO_BACKUP_KEEP {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort_by_key(|(_, ts)| *ts);
|
||||||
|
let remove_count = files.len() - AUTO_BACKUP_KEEP;
|
||||||
|
for (path, _) in files.into_iter().take(remove_count) {
|
||||||
|
if let Err(err) = fs::remove_file(&path).await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Backup,
|
||||||
|
"Failed to remove auto backup {}: {err:#?}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod auto_backup;
|
||||||
pub mod lightweight;
|
pub mod lightweight;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod sysinfo;
|
pub mod sysinfo;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
tray::Tray,
|
tray::Tray,
|
||||||
},
|
},
|
||||||
logging, logging_error,
|
logging, logging_error,
|
||||||
module::{lightweight::auto_lightweight_boot, signal},
|
module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot, signal},
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
utils::{init, logging::Type, server, window_manager::WindowManager},
|
utils::{init, logging::Type, server, window_manager::WindowManager},
|
||||||
};
|
};
|
||||||
@@ -68,6 +68,7 @@ pub fn resolve_setup_async() {
|
|||||||
init_timer(),
|
init_timer(),
|
||||||
init_hotkey(),
|
init_hotkey(),
|
||||||
init_auto_lightweight_boot(),
|
init_auto_lightweight_boot(),
|
||||||
|
init_auto_backup(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,10 @@ pub(super) async fn init_auto_lightweight_boot() {
|
|||||||
logging_error!(Type::Setup, auto_lightweight_boot().await);
|
logging_error!(Type::Setup, auto_lightweight_boot().await);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn init_auto_backup() {
|
||||||
|
logging_error!(Type::Setup, AutoBackupManager::global().init().await);
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn init_signal() {
|
pub(super) fn init_signal() {
|
||||||
logging!(info, Type::Setup, "Initializing signal handlers...");
|
logging!(info, Type::Setup, "Initializing signal handlers...");
|
||||||
signal::register();
|
signal::register();
|
||||||
|
|||||||
205
src/components/setting/mods/auto-backup-settings.tsx
Normal file
205
src/components/setting/mods/auto-backup-settings.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
InputAdornment,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import { Fragment, useMemo, useState, type ChangeEvent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Switch } from "@/components/base";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
|
const MIN_INTERVAL_HOURS = 1;
|
||||||
|
const MAX_INTERVAL_HOURS = 168;
|
||||||
|
|
||||||
|
interface AutoBackupState {
|
||||||
|
scheduleEnabled: boolean;
|
||||||
|
intervalHours: number;
|
||||||
|
changeEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AutoBackupSettings() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { verge, patchVerge } = useVerge();
|
||||||
|
const derivedValues = useMemo<AutoBackupState>(() => {
|
||||||
|
return {
|
||||||
|
scheduleEnabled: verge?.enable_auto_backup_schedule ?? false,
|
||||||
|
intervalHours: verge?.auto_backup_interval_hours ?? 24,
|
||||||
|
changeEnabled: verge?.auto_backup_on_change ?? true,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
verge?.enable_auto_backup_schedule,
|
||||||
|
verge?.auto_backup_interval_hours,
|
||||||
|
verge?.auto_backup_on_change,
|
||||||
|
]);
|
||||||
|
const [pendingValues, setPendingValues] = useState<AutoBackupState | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const values = useMemo(() => {
|
||||||
|
if (!pendingValues) {
|
||||||
|
return derivedValues;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pendingValues.scheduleEnabled === derivedValues.scheduleEnabled &&
|
||||||
|
pendingValues.intervalHours === derivedValues.intervalHours &&
|
||||||
|
pendingValues.changeEnabled === derivedValues.changeEnabled
|
||||||
|
) {
|
||||||
|
return derivedValues;
|
||||||
|
}
|
||||||
|
return pendingValues;
|
||||||
|
}, [pendingValues, derivedValues]);
|
||||||
|
const [intervalInputDraft, setIntervalInputDraft] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyPatch = useLockFn(
|
||||||
|
async (
|
||||||
|
partial: Partial<AutoBackupState>,
|
||||||
|
payload: Partial<IVergeConfig>,
|
||||||
|
) => {
|
||||||
|
const nextValues = { ...values, ...partial };
|
||||||
|
setPendingValues(nextValues);
|
||||||
|
try {
|
||||||
|
await patchVerge(payload);
|
||||||
|
} catch (error) {
|
||||||
|
showNotice.error(error);
|
||||||
|
setPendingValues(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const disabled = !verge;
|
||||||
|
|
||||||
|
const handleScheduleToggle = (
|
||||||
|
_: ChangeEvent<HTMLInputElement>,
|
||||||
|
checked: boolean,
|
||||||
|
) => {
|
||||||
|
applyPatch(
|
||||||
|
{ scheduleEnabled: checked },
|
||||||
|
{
|
||||||
|
enable_auto_backup_schedule: checked,
|
||||||
|
auto_backup_interval_hours: values.intervalHours,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeToggle = (
|
||||||
|
_: ChangeEvent<HTMLInputElement>,
|
||||||
|
checked: boolean,
|
||||||
|
) => {
|
||||||
|
applyPatch({ changeEnabled: checked }, { auto_backup_on_change: checked });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntervalInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIntervalInputDraft(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitIntervalInput = () => {
|
||||||
|
const rawValue = intervalInputDraft ?? values.intervalHours.toString();
|
||||||
|
const trimmed = rawValue.trim();
|
||||||
|
if (trimmed === "") {
|
||||||
|
setIntervalInputDraft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(trimmed);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
setIntervalInputDraft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamped = Math.min(
|
||||||
|
MAX_INTERVAL_HOURS,
|
||||||
|
Math.max(MIN_INTERVAL_HOURS, Math.round(parsed)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clamped === values.intervalHours) {
|
||||||
|
setIntervalInputDraft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPatch(
|
||||||
|
{ intervalHours: clamped },
|
||||||
|
{ auto_backup_interval_hours: clamped },
|
||||||
|
);
|
||||||
|
setIntervalInputDraft(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleDisabled = disabled || !values.scheduleEnabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ListItem divider disableGutters>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} width="100%">
|
||||||
|
<ListItemText
|
||||||
|
primary={t("settings.modals.backup.auto.scheduleLabel")}
|
||||||
|
secondary={t("settings.modals.backup.auto.scheduleHelper")}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={values.scheduleEnabled}
|
||||||
|
onChange={handleScheduleToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem divider disableGutters>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2} width="100%">
|
||||||
|
<ListItemText
|
||||||
|
primary={t("settings.modals.backup.auto.intervalLabel")}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("settings.modals.backup.auto.intervalLabel")}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
value={intervalInputDraft ?? values.intervalHours.toString()}
|
||||||
|
disabled={scheduleDisabled}
|
||||||
|
onChange={handleIntervalInputChange}
|
||||||
|
onBlur={commitIntervalInput}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
commitIntervalInput();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 160 }}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{t("shared.units.hours")}
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
htmlInput: {
|
||||||
|
min: MIN_INTERVAL_HOURS,
|
||||||
|
max: MAX_INTERVAL_HOURS,
|
||||||
|
inputMode: "numeric",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem divider disableGutters>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} width="100%">
|
||||||
|
<ListItemText
|
||||||
|
primary={t("settings.modals.backup.auto.changeLabel")}
|
||||||
|
secondary={t("settings.modals.backup.auto.changeHelper")}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={values.changeEnabled}
|
||||||
|
onChange={handleChangeToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,14 +58,6 @@ export const BackupConfigViewer = memo(
|
|||||||
webdav_username !== username ||
|
webdav_username !== username ||
|
||||||
webdav_password !== password;
|
webdav_password !== password;
|
||||||
|
|
||||||
console.log(
|
|
||||||
"webdavChanged",
|
|
||||||
webdavChanged,
|
|
||||||
webdav_url,
|
|
||||||
webdav_username,
|
|
||||||
webdav_password,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickShowPassword = () => {
|
const handleClickShowPassword = () => {
|
||||||
setShowPassword((prev) => !prev);
|
setShowPassword((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|||||||
344
src/components/setting/mods/backup-history-viewer.tsx
Normal file
344
src/components/setting/mods/backup-history-viewer.tsx
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
||||||
|
import DownloadRounded from "@mui/icons-material/DownloadRounded";
|
||||||
|
import RefreshRounded from "@mui/icons-material/RefreshRounded";
|
||||||
|
import RestoreRounded from "@mui/icons-material/RestoreRounded";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListSubheader,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { BaseDialog, BaseLoadingOverlay } from "@/components/base";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import {
|
||||||
|
deleteLocalBackup,
|
||||||
|
deleteWebdavBackup,
|
||||||
|
exportLocalBackup,
|
||||||
|
listLocalBackup,
|
||||||
|
listWebDavBackup,
|
||||||
|
restartApp,
|
||||||
|
restoreLocalBackup,
|
||||||
|
restoreWebDavBackup,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
||||||
|
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
||||||
|
|
||||||
|
type BackupSource = "local" | "webdav";
|
||||||
|
|
||||||
|
interface BackupHistoryViewerProps {
|
||||||
|
open: boolean;
|
||||||
|
source: BackupSource;
|
||||||
|
page: number;
|
||||||
|
onSourceChange: (source: BackupSource) => void;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackupRow {
|
||||||
|
filename: string;
|
||||||
|
platform: string;
|
||||||
|
backup_time: dayjs.Dayjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmAsync = async (message: string) => {
|
||||||
|
const fn = window.confirm as (msg?: string) => boolean;
|
||||||
|
return fn(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BackupHistoryViewer = ({
|
||||||
|
open,
|
||||||
|
source,
|
||||||
|
page,
|
||||||
|
onSourceChange,
|
||||||
|
onPageChange,
|
||||||
|
onClose,
|
||||||
|
}: BackupHistoryViewerProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { verge } = useVerge();
|
||||||
|
const [rows, setRows] = useState<BackupRow[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
|
const isLocal = source === "local";
|
||||||
|
const isWebDavConfigured = Boolean(
|
||||||
|
verge?.webdav_url && verge?.webdav_username && verge?.webdav_password,
|
||||||
|
);
|
||||||
|
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
|
||||||
|
const pageSize = 8;
|
||||||
|
const isBusy = loading || isRestarting;
|
||||||
|
|
||||||
|
const buildRow = useCallback((filename: string): BackupRow | null => {
|
||||||
|
const platform = filename.split("-")[0];
|
||||||
|
const match = filename.match(FILENAME_PATTERN);
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
platform,
|
||||||
|
backup_time: dayjs(match[0], DATE_FORMAT),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchRows = useCallback(async () => {
|
||||||
|
if (!open) return;
|
||||||
|
if (shouldSkipWebDav) {
|
||||||
|
setRows([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const list = isLocal ? await listLocalBackup() : await listWebDavBackup();
|
||||||
|
setRows(
|
||||||
|
list
|
||||||
|
.map((item) => buildRow(item.filename))
|
||||||
|
.filter((item): item is BackupRow => item !== null)
|
||||||
|
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setRows([]);
|
||||||
|
showNotice.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [buildRow, isLocal, open, shouldSkipWebDav]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchRows();
|
||||||
|
}, [fetchRows]);
|
||||||
|
|
||||||
|
const total = rows.length;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
||||||
|
const currentPage = Math.min(page, pageCount - 1);
|
||||||
|
const pagedRows = rows.slice(
|
||||||
|
currentPage * pageSize,
|
||||||
|
currentPage * pageSize + pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = useMemo(() => {
|
||||||
|
if (shouldSkipWebDav) {
|
||||||
|
return t("settings.modals.backup.manual.webdav");
|
||||||
|
}
|
||||||
|
if (!total) return t("settings.modals.backup.history.empty");
|
||||||
|
const recent = rows[0]?.backup_time.fromNow();
|
||||||
|
return t("settings.modals.backup.history.summary", {
|
||||||
|
count: total,
|
||||||
|
recent,
|
||||||
|
});
|
||||||
|
}, [rows, shouldSkipWebDav, t, total]);
|
||||||
|
|
||||||
|
const handleDelete = useLockFn(async (filename: string) => {
|
||||||
|
if (isRestarting) return;
|
||||||
|
if (
|
||||||
|
!(await confirmAsync(t("settings.modals.backup.messages.confirmDelete")))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (isLocal) {
|
||||||
|
await deleteLocalBackup(filename);
|
||||||
|
} else {
|
||||||
|
await deleteWebdavBackup(filename);
|
||||||
|
}
|
||||||
|
await fetchRows();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRestore = useLockFn(async (filename: string) => {
|
||||||
|
if (isRestarting) return;
|
||||||
|
if (
|
||||||
|
!(await confirmAsync(t("settings.modals.backup.messages.confirmRestore")))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (isLocal) {
|
||||||
|
await restoreLocalBackup(filename);
|
||||||
|
} else {
|
||||||
|
await restoreWebDavBackup(filename);
|
||||||
|
}
|
||||||
|
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||||
|
setIsRestarting(true);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void restartApp().catch((err: unknown) => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
showNotice.error(err);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleExport = useLockFn(async (filename: string) => {
|
||||||
|
if (isRestarting) return;
|
||||||
|
if (!isLocal) return;
|
||||||
|
const savePath = await save({ defaultPath: filename });
|
||||||
|
if (!savePath || Array.isArray(savePath)) return;
|
||||||
|
try {
|
||||||
|
await exportLocalBackup(filename, savePath);
|
||||||
|
showNotice.success("settings.modals.backup.messages.localBackupExported");
|
||||||
|
} catch (ignoreError: unknown) {
|
||||||
|
showNotice.error(
|
||||||
|
"settings.modals.backup.messages.localBackupExportFailed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (isRestarting) return;
|
||||||
|
void fetchRows();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
open={open}
|
||||||
|
title={t("settings.modals.backup.history.title")}
|
||||||
|
contentSx={{ width: 520 }}
|
||||||
|
disableOk
|
||||||
|
cancelBtn={t("shared.actions.close")}
|
||||||
|
onCancel={onClose}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: "relative", minHeight: 320 }}>
|
||||||
|
<BaseLoadingOverlay isLoading={isBusy} />
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={source}
|
||||||
|
onChange={(_, val) => {
|
||||||
|
if (isBusy) return;
|
||||||
|
onSourceChange(val as BackupSource);
|
||||||
|
onPageChange(0);
|
||||||
|
}}
|
||||||
|
textColor="primary"
|
||||||
|
indicatorColor="primary"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
value="local"
|
||||||
|
label={t("settings.modals.backup.tabs.local")}
|
||||||
|
disabled={isBusy}
|
||||||
|
sx={{ px: 2 }}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="webdav"
|
||||||
|
label={t("settings.modals.backup.tabs.webdav")}
|
||||||
|
disabled={isBusy}
|
||||||
|
sx={{ px: 2 }}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
<IconButton size="small" onClick={handleRefresh} disabled={isBusy}>
|
||||||
|
<RefreshRounded fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{summary}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List
|
||||||
|
disablePadding
|
||||||
|
subheader={
|
||||||
|
<ListSubheader disableSticky>
|
||||||
|
{t("settings.modals.backup.history.title")}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pagedRows.length === 0 ? (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={t("settings.modals.backup.history.empty") || ""}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
) : (
|
||||||
|
pagedRows.map((row) => (
|
||||||
|
<ListItem
|
||||||
|
key={`${row.platform}-${row.filename}`}
|
||||||
|
divider
|
||||||
|
secondaryAction={
|
||||||
|
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||||
|
{isLocal && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => handleExport(row.filename)}
|
||||||
|
>
|
||||||
|
<DownloadRounded fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => handleDelete(row.filename)}
|
||||||
|
>
|
||||||
|
<DeleteOutline fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => handleRestore(row.filename)}
|
||||||
|
>
|
||||||
|
<RestoreRounded fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={row.filename}
|
||||||
|
secondary={`${row.platform} · ${row.backup_time.format("YYYY-MM-DD HH:mm")}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{currentPage + 1} / {pageCount}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
disabled={isBusy || currentPage === 0}
|
||||||
|
onClick={() => onPageChange(Math.max(0, currentPage - 1))}
|
||||||
|
>
|
||||||
|
{t("shared.actions.previous")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
disabled={isBusy || currentPage >= pageCount - 1}
|
||||||
|
onClick={() =>
|
||||||
|
onPageChange(Math.min(pageCount - 1, currentPage + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("shared.actions.next")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
|
||||||
import RestoreIcon from "@mui/icons-material/Restore";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Paper,
|
|
||||||
IconButton,
|
|
||||||
Divider,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TablePagination,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
import { save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { useLockFn } from "ahooks";
|
|
||||||
import { Dayjs } from "dayjs";
|
|
||||||
import { SVGProps, memo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { restartApp } from "@/services/cmds";
|
|
||||||
import { showNotice } from "@/services/noticeService";
|
|
||||||
|
|
||||||
export type BackupFile = {
|
|
||||||
platform: string;
|
|
||||||
backup_time: Dayjs;
|
|
||||||
allow_apply: boolean;
|
|
||||||
filename: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_ROWS_PER_PAGE = 5;
|
|
||||||
|
|
||||||
type ConfirmFn = (message?: string) => boolean | Promise<boolean>;
|
|
||||||
|
|
||||||
// Normalizes synchronous and async confirm implementations.
|
|
||||||
const confirmAsync = async (message: string): Promise<boolean> => {
|
|
||||||
const confirmFn = window.confirm as unknown as ConfirmFn;
|
|
||||||
return await confirmFn.call(window, message);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BackupTableViewerProps {
|
|
||||||
datasource: BackupFile[];
|
|
||||||
page: number;
|
|
||||||
onPageChange: (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
page: number,
|
|
||||||
) => void;
|
|
||||||
total: number;
|
|
||||||
onRefresh: () => Promise<void>;
|
|
||||||
onDelete: (filename: string) => Promise<void>;
|
|
||||||
onRestore: (filename: string) => Promise<void>;
|
|
||||||
onExport?: (filename: string, destination: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BackupTableViewer = memo(
|
|
||||||
({
|
|
||||||
datasource,
|
|
||||||
page,
|
|
||||||
onPageChange,
|
|
||||||
total,
|
|
||||||
onRefresh,
|
|
||||||
onDelete,
|
|
||||||
onRestore,
|
|
||||||
onExport,
|
|
||||||
}: BackupTableViewerProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleDelete = useLockFn(async (filename: string) => {
|
|
||||||
await onDelete(filename);
|
|
||||||
await onRefresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRestore = useLockFn(async (filename: string) => {
|
|
||||||
await onRestore(filename).then(() => {
|
|
||||||
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
|
||||||
});
|
|
||||||
await restartApp();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleExport = useLockFn(async (filename: string) => {
|
|
||||||
if (!onExport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const savePath = await save({
|
|
||||||
defaultPath: filename,
|
|
||||||
});
|
|
||||||
if (!savePath || Array.isArray(savePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await onExport(filename, savePath);
|
|
||||||
showNotice.success(
|
|
||||||
"settings.modals.backup.messages.localBackupExported",
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
showNotice.error(
|
|
||||||
"settings.modals.backup.messages.localBackupExportFailed",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
{t("settings.modals.backup.table.filename")}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{t("settings.modals.backup.table.backupTime")}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
{t("settings.modals.backup.table.actions")}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{datasource.length > 0 ? (
|
|
||||||
datasource.map((file) => {
|
|
||||||
const rowKey = `${file.platform}-${file.filename}-${file.backup_time.valueOf()}`;
|
|
||||||
return (
|
|
||||||
<TableRow key={rowKey}>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{file.platform === "windows" ? (
|
|
||||||
<WindowsIcon className="h-full w-full" />
|
|
||||||
) : file.platform === "linux" ? (
|
|
||||||
<LinuxIcon className="h-full w-full" />
|
|
||||||
) : (
|
|
||||||
<MacIcon className="h-full w-full" />
|
|
||||||
)}
|
|
||||||
{file.filename}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
{file.backup_time.fromNow()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{onExport && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
color="primary"
|
|
||||||
aria-label={t(
|
|
||||||
"settings.modals.backup.actions.export",
|
|
||||||
)}
|
|
||||||
size="small"
|
|
||||||
title={t(
|
|
||||||
"settings.modals.backup.actions.exportBackup",
|
|
||||||
)}
|
|
||||||
onClick={async (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await handleExport(file.filename);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Divider
|
|
||||||
orientation="vertical"
|
|
||||||
flexItem
|
|
||||||
sx={{ mx: 1, height: 24 }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
color="secondary"
|
|
||||||
aria-label={t("shared.actions.delete")}
|
|
||||||
size="small"
|
|
||||||
title={t(
|
|
||||||
"settings.modals.backup.actions.deleteBackup",
|
|
||||||
)}
|
|
||||||
onClick={async (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const confirmed = await confirmAsync(
|
|
||||||
t(
|
|
||||||
"settings.modals.backup.messages.confirmDelete",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (confirmed) {
|
|
||||||
await handleDelete(file.filename);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Divider
|
|
||||||
orientation="vertical"
|
|
||||||
flexItem
|
|
||||||
sx={{ mx: 1, height: 24 }}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
color="primary"
|
|
||||||
aria-label={t(
|
|
||||||
"settings.modals.backup.actions.restore",
|
|
||||||
)}
|
|
||||||
size="small"
|
|
||||||
title={t(
|
|
||||||
"settings.modals.backup.actions.restoreBackup",
|
|
||||||
)}
|
|
||||||
disabled={!file.allow_apply}
|
|
||||||
onClick={async (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const confirmed = await confirmAsync(
|
|
||||||
t(
|
|
||||||
"settings.modals.backup.messages.confirmRestore",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (confirmed) {
|
|
||||||
await handleRestore(file.filename);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RestoreIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} align="center">
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: 150,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
color="textSecondary"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
{t("settings.modals.backup.table.noBackups")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<TablePagination
|
|
||||||
rowsPerPageOptions={[]}
|
|
||||||
component="div"
|
|
||||||
count={total}
|
|
||||||
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
|
||||||
page={page}
|
|
||||||
onPageChange={onPageChange}
|
|
||||||
labelRowsPerPage={t("settings.modals.backup.table.rowsPerPage")}
|
|
||||||
/>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function LinuxIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#ECEFF1"
|
|
||||||
d="m20.1 16.2l.1 2.3l-1.6 3l-2.5 4.9l-.5 4.1l1.8 5.8l4.1 2.3h6.2l5.8-4.4l2.6-6.9l-6-7.3l-1.7-4.1z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#263238"
|
|
||||||
d="M34.3 21.9c-1.6-2.3-2.9-3.7-3.6-6.6s.2-2.1-.4-4.6c-.3-1.3-.8-2.2-1.3-2.9c-.6-.7-1.3-1.1-1.7-1.2c-.9-.5-3-1.3-5.6.1c-2.7 1.4-2.4 4.4-1.9 10.5c0 .4-.1.9-.3 1.3c-.4.9-1.1 1.7-1.7 2.4c-.7 1-1.4 2-1.9 3.1c-1.2 2.3-2.3 5.2-2 6.3c.5-.1 6.8 9.5 6.8 9.7c.4-.1 2.1-.1 3.6-.1c2.1-.1 3.3-.2 5 .2c0-.3-.1-.6-.1-.9c0-.6.1-1.1.2-1.8c.1-.5.2-1 .3-1.6c-1 .9-2.8 1.9-4.5 2.2c-1.5.3-4-.2-5.2-1.7c.1 0 .3 0 .4-.1c.3-.1.6-.2.7-.4c.3-.5.1-1-.1-1.3s-1.7-1.4-2.4-2s-1.1-.9-1.5-1.3l-.8-.8c-.2-.2-.3-.4-.4-.5c-.2-.5-.3-1.1-.2-1.9c.1-1.1.5-2 1-3c.2-.4.7-1.2.7-1.2s-1.7 4.2-.8 5.5c0 0 .1-1.3.5-2.6c.3-.9.8-2.2 1.4-2.9s2.1-3.3 2.2-4.9c0-.7.1-1.4.1-1.9c-.4-.4 6.6-1.4 7-.3c.1.4 1.5 4 2.3 5.9c.4.9.9 1.7 1.2 2.7c.3 1.1.5 2.6.5 4.1c0 .3 0 .8-.1 1.3c.2 0 4.1-4.2-.5-7.7c0 0 2.8 1.3 2.9 3.9c.1 2.1-.8 3.8-1 4.1c.1 0 2.1.9 2.2.9c.4 0 1.2-.3 1.2-.3c.1-.3.4-1.1.4-1.4c.7-2.3-1-6-2.6-8.3"
|
|
||||||
/>
|
|
||||||
<g fill="#ECEFF1" transform="translate(0 -2)">
|
|
||||||
<ellipse cx="21.6" cy="15.3" rx="1.3" ry="2" />
|
|
||||||
<ellipse cx="26.1" cy="15.2" rx="1.7" ry="2.3" />
|
|
||||||
</g>
|
|
||||||
<g fill="#212121" transform="translate(0 -2)">
|
|
||||||
<ellipse
|
|
||||||
cx="21.7"
|
|
||||||
cy="15.5"
|
|
||||||
rx="1.2"
|
|
||||||
ry=".7"
|
|
||||||
transform="rotate(-97.204 21.677 15.542)"
|
|
||||||
/>
|
|
||||||
<ellipse cx="26" cy="15.6" rx="1" ry="1.3" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
fill="#FFC107"
|
|
||||||
d="M39.3 35.6c-.4-.2-1.1-.5-1.7-1.4c-.3-.5-.2-1.9-.7-2.5c-.3-.4-.7-.2-.8-.2c-.9.2-3 1.6-4.4 0c-.2-.2-.5-.5-1-.5s-.7.2-.9.6s-.2.7-.2 1.7c0 .8 0 1.7-.1 2.4c-.2 1.7-.5 2.7-.5 3.7c0 1.1.3 1.8.7 2.1c.3.3.8.5 1.9.5s1.8-.4 2.5-1.1c.5-.5.9-.7 2.3-1.7c1.1-.7 2.8-1.6 3.1-1.9c.2-.2.5-.3.5-.9c0-.5-.4-.7-.7-.8m-20.1.3c-1-1.6-1.1-1.9-1.8-2.9c-.6-1-1.9-2.9-2.7-2.9c-.6 0-.9.3-1.3.7s-.8 1.3-1.5 1.8c-.6.5-2.3.4-2.7 1s.4 1.5.4 3c0 .6-.5 1-.6 1.4c-.1.5-.2.8 0 1.2c.4.6.9.8 4.3 1.5c1.8.4 3.5 1.4 4.6 1.5s3 0 3-2.7c.1-1.6-.8-2-1.7-3.6m1.9-18.1c-.6-.4-1.1-.8-1.1-1.4s.4-.8 1-1.3c.1-.1 1.2-1.1 2.3-1.1s2.4.7 2.9.9c.9.2 1.8.4 1.7 1.1c-.1 1-.2 1.2-1.2 1.7c-.7.2-2 1.3-2.9 1.3c-.4 0-1 0-1.4-.1c-.3-.1-.8-.6-1.3-1.1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#634703"
|
|
||||||
d="M20.9 17c.2.2.5.4.8.5c.2.1.5.2.5.2h.9c.5 0 1.2-.2 1.9-.6c.7-.3.8-.5 1.3-.7c.5-.3 1-.6.8-.7s-.4 0-1.1.4c-.6.4-1.1.6-1.7.9c-.3.1-.7.3-1 .3h-.9c-.3 0-.5-.1-.8-.2c-.2-.1-.3-.2-.4-.2c-.2-.1-.6-.5-.8-.6c0 0-.2 0-.1.1zm3-2.2c.1.2.3.2.4.3s.2.1.2.1c.1-.1 0-.3-.1-.3c0-.2-.5-.2-.5-.1m-1.6.2c0 .1.2.2.2.1c.1-.1.2-.2.3-.2c.2-.1.1-.2-.2-.2c-.2.1-.2.2-.3.3"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#455A64"
|
|
||||||
d="M32 32.7v.3c.2.4.7.5 1.1.5c.6 0 1.2-.4 1.5-.8c0-.1.1-.2.2-.3c.2-.3.3-.5.4-.6c0 0-.1-.1-.1-.2c-.1-.2-.4-.4-.8-.5c-.3-.1-.8-.2-1-.2c-.9-.1-1.4.2-1.7.5c0 0 .1 0 .1.1c.2.2.3.4.3.7c.1.2 0 .3 0 .5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WindowsIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#0284c7"
|
|
||||||
d="M6.555 1.375L0 2.237v5.45h6.555zM0 13.795l6.555.933V8.313H0zm7.278-5.4l.026 6.378L16 16V8.395zM16 0L7.33 1.244v6.414H16z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MacIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 26 26"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#000"
|
|
||||||
d="M23.934 18.947c-.598 1.324-.884 1.916-1.652 3.086c-1.073 1.634-2.588 3.673-4.461 3.687c-1.666.014-2.096-1.087-4.357-1.069c-2.261.011-2.732 1.089-4.4 1.072c-1.873-.017-3.307-1.854-4.381-3.485c-3.003-4.575-3.32-9.937-1.464-12.79C4.532 7.425 6.61 6.237 8.561 6.237c1.987 0 3.236 1.092 4.879 1.092c1.594 0 2.565-1.095 4.863-1.095c1.738 0 3.576.947 4.889 2.581c-4.296 2.354-3.598 8.49.742 10.132M16.559 4.408c.836-1.073 1.47-2.587 1.24-4.131c-1.364.093-2.959.964-3.891 2.092c-.844 1.027-1.544 2.553-1.271 4.029c1.488.048 3.028-.839 3.922-1.99"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,354 +1,213 @@
|
|||||||
import { Box, Button, Divider, Paper, Tab, Tabs } from "@mui/material";
|
import { LoadingButton } from "@mui/lab";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
||||||
import type { Ref } from "react";
|
|
||||||
import {
|
import {
|
||||||
useCallback,
|
Button,
|
||||||
useEffect,
|
List,
|
||||||
useImperativeHandle,
|
ListItem,
|
||||||
useMemo,
|
ListItemText,
|
||||||
useReducer,
|
Stack,
|
||||||
useRef,
|
Typography,
|
||||||
useState,
|
} from "@mui/material";
|
||||||
} from "react";
|
import { useLockFn } from "ahooks";
|
||||||
import { createPortal } from "react-dom";
|
import type { ReactNode, Ref } from "react";
|
||||||
|
import { useImperativeHandle, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base";
|
import { BaseDialog, DialogRef } from "@/components/base";
|
||||||
import {
|
import { createLocalBackup, createWebdavBackup } from "@/services/cmds";
|
||||||
deleteLocalBackup,
|
import { showNotice } from "@/services/noticeService";
|
||||||
deleteWebdavBackup,
|
|
||||||
listLocalBackup,
|
|
||||||
listWebDavBackup,
|
|
||||||
exportLocalBackup,
|
|
||||||
restoreLocalBackup,
|
|
||||||
restoreWebDavBackup,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
|
|
||||||
import { BackupConfigViewer } from "./backup-config-viewer";
|
import { AutoBackupSettings } from "./auto-backup-settings";
|
||||||
import {
|
import { BackupHistoryViewer } from "./backup-history-viewer";
|
||||||
BackupFile,
|
import { BackupWebdavDialog } from "./backup-webdav-dialog";
|
||||||
BackupTableViewer,
|
|
||||||
DEFAULT_ROWS_PER_PAGE,
|
|
||||||
} from "./backup-table-viewer";
|
|
||||||
import { LocalBackupActions } from "./local-backup-actions";
|
|
||||||
dayjs.extend(customParseFormat);
|
|
||||||
|
|
||||||
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
|
||||||
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
|
||||||
type BackupSource = "local" | "webdav";
|
type BackupSource = "local" | "webdav";
|
||||||
type CloseButtonPosition = { top: number; left: number } | null;
|
|
||||||
|
|
||||||
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const [busyAction, setBusyAction] = useState<BackupSource | null>(null);
|
||||||
const [dialogPaper, setDialogPaper] = useReducer(
|
const [historyOpen, setHistoryOpen] = useState(false);
|
||||||
(_: HTMLElement | null, next: HTMLElement | null) => next,
|
const [historySource, setHistorySource] = useState<BackupSource>("local");
|
||||||
null as HTMLElement | null,
|
const [historyPage, setHistoryPage] = useState(0);
|
||||||
);
|
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false);
|
||||||
const [closeButtonPosition, setCloseButtonPosition] = useReducer(
|
|
||||||
(_: CloseButtonPosition, next: CloseButtonPosition) => next,
|
|
||||||
null as CloseButtonPosition,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [source, setSource] = useState<BackupSource>("local");
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => setOpen(true),
|
||||||
setOpen(true);
|
|
||||||
},
|
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Handle page change
|
const openHistory = (target: BackupSource) => {
|
||||||
const handleChangePage = useCallback(
|
setHistorySource(target);
|
||||||
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
|
setHistoryPage(0);
|
||||||
setPage(page);
|
setHistoryOpen(true);
|
||||||
},
|
};
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeSource = useCallback(
|
const handleBackup = useLockFn(async (target: BackupSource) => {
|
||||||
(_event: React.SyntheticEvent, newSource: string) => {
|
|
||||||
setSource(newSource as BackupSource);
|
|
||||||
setPage(0);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildBackupFile = useCallback((filename: string) => {
|
|
||||||
const platform = filename.split("-")[0];
|
|
||||||
const fileBackupTimeStr = filename.match(FILENAME_PATTERN);
|
|
||||||
|
|
||||||
if (fileBackupTimeStr === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
|
|
||||||
const allowApply = true;
|
|
||||||
return {
|
|
||||||
filename,
|
|
||||||
platform,
|
|
||||||
backup_time: backupTime,
|
|
||||||
allow_apply: allowApply,
|
|
||||||
} as BackupFile;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getAllBackupFiles = useCallback(async (): Promise<BackupFile[]> => {
|
|
||||||
if (source === "local") {
|
|
||||||
const files = await listLocalBackup();
|
|
||||||
return files
|
|
||||||
.map((file) => buildBackupFile(file.filename))
|
|
||||||
.filter((item): item is BackupFile => item !== null)
|
|
||||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await listWebDavBackup();
|
|
||||||
return files
|
|
||||||
.map((file) => {
|
|
||||||
return buildBackupFile(file.filename);
|
|
||||||
})
|
|
||||||
.filter((item): item is BackupFile => item !== null)
|
|
||||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
|
||||||
}, [buildBackupFile, source]);
|
|
||||||
|
|
||||||
const fetchAndSetBackupFiles = useCallback(async () => {
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setBusyAction(target);
|
||||||
const files = await getAllBackupFiles();
|
if (target === "local") {
|
||||||
setBackupFiles(files);
|
await createLocalBackup();
|
||||||
setTotal(files.length);
|
showNotice.success(
|
||||||
|
"settings.modals.backup.messages.localBackupCreated",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await createWebdavBackup();
|
||||||
|
showNotice.success("settings.modals.backup.messages.backupCreated");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setBackupFiles([]);
|
|
||||||
setTotal(0);
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
showNotice.error(
|
||||||
|
target === "local"
|
||||||
|
? "settings.modals.backup.messages.localBackupFailed"
|
||||||
|
: "settings.modals.backup.messages.backupFailed",
|
||||||
|
target === "local" ? undefined : { error },
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
}, [getAllBackupFiles]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
fetchAndSetBackupFiles();
|
|
||||||
const paper = contentRef.current?.closest(".MuiPaper-root");
|
|
||||||
setDialogPaper((paper as HTMLElement) ?? null);
|
|
||||||
} else {
|
|
||||||
setDialogPaper(null);
|
|
||||||
}
|
|
||||||
}, [open, fetchAndSetBackupFiles]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || dialogPaper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const frame = requestAnimationFrame(() => {
|
|
||||||
const paper = contentRef.current?.closest(".MuiPaper-root");
|
|
||||||
setDialogPaper((paper as HTMLElement) ?? null);
|
|
||||||
});
|
});
|
||||||
return () => cancelAnimationFrame(frame);
|
|
||||||
}, [open, dialogPaper]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dialogPaper) {
|
|
||||||
setCloseButtonPosition(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePosition = () => {
|
|
||||||
const rect = dialogPaper.getBoundingClientRect();
|
|
||||||
setCloseButtonPosition({
|
|
||||||
top: rect.bottom - 16,
|
|
||||||
left: rect.right - 24,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePosition();
|
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
|
||||||
if (typeof ResizeObserver !== "undefined") {
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
updatePosition();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(dialogPaper);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollTargets: EventTarget[] = [];
|
|
||||||
const addScrollListener = (target: EventTarget | null) => {
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target.addEventListener("scroll", updatePosition, true);
|
|
||||||
scrollTargets.push(target);
|
|
||||||
};
|
|
||||||
|
|
||||||
addScrollListener(window);
|
|
||||||
addScrollListener(dialogPaper);
|
|
||||||
const dialogContent = dialogPaper.querySelector(".MuiDialogContent-root");
|
|
||||||
addScrollListener(dialogContent);
|
|
||||||
|
|
||||||
window.addEventListener("resize", updatePosition);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver?.disconnect();
|
|
||||||
scrollTargets.forEach((target) => {
|
|
||||||
target.removeEventListener("scroll", updatePosition, true);
|
|
||||||
});
|
|
||||||
window.removeEventListener("resize", updatePosition);
|
|
||||||
};
|
|
||||||
}, [dialogPaper]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
|
||||||
async (filename: string) => {
|
|
||||||
if (source === "local") {
|
|
||||||
await deleteLocalBackup(filename);
|
|
||||||
} else {
|
|
||||||
await deleteWebdavBackup(filename);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[source],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRestore = useCallback(
|
|
||||||
async (filename: string) => {
|
|
||||||
if (source === "local") {
|
|
||||||
await restoreLocalBackup(filename);
|
|
||||||
} else {
|
|
||||||
await restoreWebDavBackup(filename);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[source],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleExport = useCallback(
|
|
||||||
async (filename: string, destination: string) => {
|
|
||||||
await exportLocalBackup(filename, destination);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataSource = useMemo<BackupFile[]>(
|
|
||||||
() =>
|
|
||||||
backupFiles.slice(
|
|
||||||
page * DEFAULT_ROWS_PER_PAGE,
|
|
||||||
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
|
|
||||||
),
|
|
||||||
[backupFiles, page],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("settings.modals.backup.title")}
|
title={t("settings.modals.backup.title")}
|
||||||
contentSx={{
|
contentSx={{ width: { xs: 360, sm: 520 } }}
|
||||||
minWidth: { xs: 320, sm: 620 },
|
disableOk
|
||||||
maxWidth: "unset",
|
cancelBtn={t("shared.actions.close")}
|
||||||
minHeight: 460,
|
onCancel={() => setOpen(false)}
|
||||||
}}
|
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
disableFooter
|
|
||||||
>
|
>
|
||||||
<Box
|
<Stack spacing={2}>
|
||||||
ref={contentRef}
|
<Stack
|
||||||
|
spacing={1}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
flexDirection: "column",
|
borderRadius: 2,
|
||||||
height: "100%",
|
p: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseLoadingOverlay isLoading={isLoading} />
|
<Typography variant="subtitle1">
|
||||||
<Paper
|
{t("settings.modals.backup.auto.title")}
|
||||||
elevation={2}
|
</Typography>
|
||||||
|
<List disablePadding sx={{ ".MuiListItem-root": { px: 0 } }}>
|
||||||
|
<AutoBackupSettings />
|
||||||
|
</List>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
spacing={1}
|
||||||
sx={{
|
sx={{
|
||||||
padding: 2,
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
display: "flex",
|
borderRadius: 2,
|
||||||
flexDirection: "column",
|
p: 2,
|
||||||
flexGrow: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Typography variant="subtitle1">
|
||||||
value={source}
|
{t("settings.modals.backup.manual.title")}
|
||||||
onChange={handleChangeSource}
|
</Typography>
|
||||||
aria-label={t("settings.modals.backup.actions.selectTarget")}
|
<List disablePadding sx={{ ".MuiListItem-root": { px: 0 } }}>
|
||||||
sx={{ mb: 2 }}
|
{(
|
||||||
>
|
[
|
||||||
<Tab value="local" label={t("settings.modals.backup.tabs.local")} />
|
{
|
||||||
<Tab
|
key: "local" as BackupSource,
|
||||||
value="webdav"
|
title: t("settings.modals.backup.tabs.local"),
|
||||||
label={t("settings.modals.backup.tabs.webdav")}
|
description: t("settings.modals.backup.manual.local"),
|
||||||
/>
|
actions: [
|
||||||
</Tabs>
|
<LoadingButton
|
||||||
{source === "local" ? (
|
key="backup"
|
||||||
<LocalBackupActions
|
variant="contained"
|
||||||
setLoading={setIsLoading}
|
size="small"
|
||||||
onBackupSuccess={fetchAndSetBackupFiles}
|
loading={busyAction === "local"}
|
||||||
onRefresh={fetchAndSetBackupFiles}
|
onClick={() => handleBackup("local")}
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<BackupConfigViewer
|
|
||||||
setLoading={setIsLoading}
|
|
||||||
onBackupSuccess={fetchAndSetBackupFiles}
|
|
||||||
onSaveSuccess={fetchAndSetBackupFiles}
|
|
||||||
onRefresh={fetchAndSetBackupFiles}
|
|
||||||
onInit={fetchAndSetBackupFiles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Divider sx={{ marginY: 2 }} />
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
minHeight: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BackupTableViewer
|
|
||||||
datasource={dataSource}
|
|
||||||
page={page}
|
|
||||||
onPageChange={handleChangePage}
|
|
||||||
total={total}
|
|
||||||
onRefresh={fetchAndSetBackupFiles}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onRestore={handleRestore}
|
|
||||||
onExport={source === "local" ? handleExport : undefined}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
{dialogPaper &&
|
|
||||||
closeButtonPosition &&
|
|
||||||
createPortal(
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "fixed",
|
|
||||||
top: closeButtonPosition.top,
|
|
||||||
left: closeButtonPosition.left,
|
|
||||||
transform: "translate(-100%, -100%)",
|
|
||||||
pointerEvents: "none",
|
|
||||||
zIndex: (theme) => theme.zIndex.modal + 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{t("settings.modals.backup.actions.backup")}
|
||||||
|
</LoadingButton>,
|
||||||
<Button
|
<Button
|
||||||
|
key="history"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => setOpen(false)}
|
size="small"
|
||||||
sx={{
|
onClick={() => openHistory("local")}
|
||||||
pointerEvents: "auto",
|
|
||||||
boxShadow: (theme) => theme.shadows[3],
|
|
||||||
backgroundColor: (theme) => theme.palette.background.paper,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("shared.actions.close")}
|
{t("settings.modals.backup.actions.viewHistory")}
|
||||||
</Button>
|
</Button>,
|
||||||
</Box>,
|
],
|
||||||
dialogPaper,
|
},
|
||||||
)}
|
{
|
||||||
|
key: "webdav" as BackupSource,
|
||||||
|
title: t("settings.modals.backup.tabs.webdav"),
|
||||||
|
description: t("settings.modals.backup.manual.webdav"),
|
||||||
|
actions: [
|
||||||
|
<LoadingButton
|
||||||
|
key="backup"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
loading={busyAction === "webdav"}
|
||||||
|
onClick={() => handleBackup("webdav")}
|
||||||
|
>
|
||||||
|
{t("settings.modals.backup.actions.backup")}
|
||||||
|
</LoadingButton>,
|
||||||
|
<Button
|
||||||
|
key="history"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => openHistory("webdav")}
|
||||||
|
>
|
||||||
|
{t("settings.modals.backup.actions.viewHistory")}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="configure"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setWebdavDialogOpen(true)}
|
||||||
|
>
|
||||||
|
{t("settings.modals.backup.manual.configureWebdav")}
|
||||||
|
</Button>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] satisfies Array<{
|
||||||
|
key: BackupSource;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
actions: ReactNode[];
|
||||||
|
}>
|
||||||
|
).map((item, idx) => (
|
||||||
|
<ListItem key={item.key} disableGutters divider={idx === 0}>
|
||||||
|
<Stack spacing={1} sx={{ width: "100%" }}>
|
||||||
|
<ListItemText
|
||||||
|
primary={item.title}
|
||||||
|
slotProps={{ secondary: { component: "span" } }}
|
||||||
|
secondary={item.description}
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{item.actions}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<BackupHistoryViewer
|
||||||
|
open={historyOpen}
|
||||||
|
source={historySource}
|
||||||
|
page={historyPage}
|
||||||
|
onSourceChange={setHistorySource}
|
||||||
|
onPageChange={setHistoryPage}
|
||||||
|
onClose={() => setHistoryOpen(false)}
|
||||||
|
/>
|
||||||
|
<BackupWebdavDialog
|
||||||
|
open={webdavDialogOpen}
|
||||||
|
onClose={() => setWebdavDialogOpen(false)}
|
||||||
|
onBackupSuccess={() => openHistory("webdav")}
|
||||||
|
setBusy={(loading) => setBusyAction(loading ? "webdav" : null)}
|
||||||
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/components/setting/mods/backup-webdav-dialog.tsx
Normal file
87
src/components/setting/mods/backup-webdav-dialog.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { BaseDialog, BaseLoadingOverlay } from "@/components/base";
|
||||||
|
import { listWebDavBackup } from "@/services/cmds";
|
||||||
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
|
import { BackupConfigViewer } from "./backup-config-viewer";
|
||||||
|
|
||||||
|
interface BackupWebdavDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onBackupSuccess?: () => void;
|
||||||
|
setBusy?: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackupWebdavDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onBackupSuccess,
|
||||||
|
setBusy,
|
||||||
|
}: BackupWebdavDialogProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLoading = useCallback(
|
||||||
|
(value: boolean) => {
|
||||||
|
setLoading(value);
|
||||||
|
setBusy?.(value);
|
||||||
|
},
|
||||||
|
[setBusy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshWebdav = useCallback(
|
||||||
|
async (options?: { silent?: boolean }) => {
|
||||||
|
handleLoading(true);
|
||||||
|
try {
|
||||||
|
await listWebDavBackup();
|
||||||
|
if (!options?.silent) {
|
||||||
|
showNotice.success(
|
||||||
|
"settings.modals.backup.messages.webdavRefreshSuccess",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotice.error(
|
||||||
|
"settings.modals.backup.messages.webdavRefreshFailed",
|
||||||
|
{ error },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
handleLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleLoading],
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshSilently = useCallback(
|
||||||
|
() => refreshWebdav({ silent: true }),
|
||||||
|
[refreshWebdav],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
open={open}
|
||||||
|
title={t("settings.modals.backup.webdav.title")}
|
||||||
|
contentSx={{ width: { xs: 360, sm: 520 } }}
|
||||||
|
disableOk
|
||||||
|
cancelBtn={t("shared.actions.close")}
|
||||||
|
onCancel={onClose}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: "relative" }}>
|
||||||
|
<BaseLoadingOverlay isLoading={loading} />
|
||||||
|
<BackupConfigViewer
|
||||||
|
setLoading={handleLoading}
|
||||||
|
onBackupSuccess={async () => {
|
||||||
|
await refreshSilently();
|
||||||
|
onBackupSuccess?.();
|
||||||
|
}}
|
||||||
|
onSaveSuccess={refreshSilently}
|
||||||
|
onRefresh={refreshWebdav}
|
||||||
|
onInit={refreshSilently}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Button, Grid, Stack, Typography } from "@mui/material";
|
|
||||||
import { useLockFn } from "ahooks";
|
|
||||||
import { memo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { createLocalBackup } from "@/services/cmds";
|
|
||||||
import { showNotice } from "@/services/noticeService";
|
|
||||||
|
|
||||||
interface LocalBackupActionsProps {
|
|
||||||
onBackupSuccess: () => Promise<void>;
|
|
||||||
onRefresh: () => Promise<void>;
|
|
||||||
setLoading: (loading: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LocalBackupActions = memo(
|
|
||||||
({ onBackupSuccess, onRefresh, setLoading }: LocalBackupActionsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleBackup = useLockFn(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await createLocalBackup();
|
|
||||||
showNotice.success(
|
|
||||||
"settings.modals.backup.messages.localBackupCreated",
|
|
||||||
);
|
|
||||||
await onBackupSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
showNotice.error("settings.modals.backup.messages.localBackupFailed");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRefresh = useLockFn(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await onRefresh();
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid size={{ xs: 12, sm: 9 }}>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{t("settings.modals.backup.fields.info")}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 3 }}>
|
|
||||||
<Stack
|
|
||||||
direction="column"
|
|
||||||
alignItems="stretch"
|
|
||||||
spacing={1.5}
|
|
||||||
sx={{ height: "100%" }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="success"
|
|
||||||
onClick={handleBackup}
|
|
||||||
type="button"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{t("settings.modals.backup.actions.backup")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
type="button"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{t("shared.actions.refresh")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "حذف النسخة الاحتياطية",
|
"deleteBackup": "حذف النسخة الاحتياطية",
|
||||||
"restore": "استعادة",
|
"restore": "استعادة",
|
||||||
"restoreBackup": "استعادة النسخة الاحتياطية"
|
"restoreBackup": "استعادة النسخة الاحتياطية",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "عنوان خادم WebDAV",
|
"webdavUrl": "عنوان خادم WebDAV",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "تمت الاستعادة بنجاح، سيعاد تشغيل التطبيق خلال ثانية واحدة",
|
"restoreSuccess": "تمت الاستعادة بنجاح، سيعاد تشغيل التطبيق خلال ثانية واحدة",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "هل تريد بالتأكيد حذف ملف النسخة الاحتياطية هذا؟",
|
"confirmDelete": "هل تريد بالتأكيد حذف ملف النسخة الاحتياطية هذا؟",
|
||||||
"confirmRestore": "هل تريد بالتأكيد استعادة ملف النسخة الاحتياطية هذا؟"
|
"confirmRestore": "هل تريد بالتأكيد استعادة ملف النسخة الاحتياطية هذا؟"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "اسم الملف",
|
"filename": "اسم الملف",
|
||||||
"backupTime": "وقت النسخ الاحتياطي",
|
"backupTime": "وقت النسخ الاحتياطي",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "إيقاف مؤقت",
|
"pause": "إيقاف مؤقت",
|
||||||
"resume": "استأنف",
|
"resume": "استأنف",
|
||||||
"closeAll": "إغلاق الكل",
|
"closeAll": "إغلاق الكل",
|
||||||
"clear": "مسح"
|
"clear": "مسح",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "التحديث عند",
|
"updateAt": "التحديث عند",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "ميلي ثانية",
|
"milliseconds": "ميلي ثانية",
|
||||||
"seconds": "ثواني",
|
"seconds": "ثواني",
|
||||||
"minutes": "دقائق",
|
"minutes": "دقائق",
|
||||||
|
"hours": "ساعات",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Sicherung löschen",
|
"deleteBackup": "Sicherung löschen",
|
||||||
"restore": "Wiederherstellen",
|
"restore": "Wiederherstellen",
|
||||||
"restoreBackup": "Sicherung wiederherstellen"
|
"restoreBackup": "Sicherung wiederherstellen",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV-Serveradresse http(s)://",
|
"webdavUrl": "WebDAV-Serveradresse http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Wiederherstellung erfolgreich. Die App wird in 1 Sekunde neu starten.",
|
"restoreSuccess": "Wiederherstellung erfolgreich. Die App wird in 1 Sekunde neu starten.",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Confirm to delete this backup file?",
|
"confirmDelete": "Confirm to delete this backup file?",
|
||||||
"confirmRestore": "Confirm to restore this backup file?"
|
"confirmRestore": "Confirm to restore this backup file?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Dateiname",
|
"filename": "Dateiname",
|
||||||
"backupTime": "Sicherungszeit",
|
"backupTime": "Sicherungszeit",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Pausieren",
|
"pause": "Pausieren",
|
||||||
"resume": "Fortsetzen",
|
"resume": "Fortsetzen",
|
||||||
"closeAll": "Alle schließen",
|
"closeAll": "Alle schließen",
|
||||||
"clear": "Löschen"
|
"clear": "Löschen",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Aktualisiert am",
|
"updateAt": "Aktualisiert am",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "Millisekunden",
|
"milliseconds": "Millisekunden",
|
||||||
"seconds": "Sekunden",
|
"seconds": "Sekunden",
|
||||||
"minutes": "Minuten",
|
"minutes": "Minuten",
|
||||||
|
"hours": "Stunden",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Delete Backup",
|
"deleteBackup": "Delete Backup",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"restoreBackup": "Restore Backup"
|
"restoreBackup": "Restore Backup",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV Server URL",
|
"webdavUrl": "WebDAV Server URL",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Restore Success, App will restart in 1s",
|
"restoreSuccess": "Restore Success, App will restart in 1s",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Confirm to delete this backup file?",
|
"confirmDelete": "Confirm to delete this backup file?",
|
||||||
"confirmRestore": "Confirm to restore this backup file?"
|
"confirmRestore": "Confirm to restore this backup file?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Filename",
|
"filename": "Filename",
|
||||||
"backupTime": "Backup Time",
|
"backupTime": "Backup Time",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"closeAll": "Close All",
|
"closeAll": "Close All",
|
||||||
"clear": "Clear"
|
"clear": "Clear",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Update At",
|
"updateAt": "Update At",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "ms",
|
"milliseconds": "ms",
|
||||||
"seconds": "seconds",
|
"seconds": "seconds",
|
||||||
"minutes": "mins",
|
"minutes": "mins",
|
||||||
|
"hours": "hrs",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Eliminar copia de seguridad",
|
"deleteBackup": "Eliminar copia de seguridad",
|
||||||
"restore": "Restaurar",
|
"restore": "Restaurar",
|
||||||
"restoreBackup": "Restaurar copia de seguridad"
|
"restoreBackup": "Restaurar copia de seguridad",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "Dirección del servidor WebDAV http(s)://",
|
"webdavUrl": "Dirección del servidor WebDAV http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Restauración exitosa. La aplicación se reiniciará en 1 segundo",
|
"restoreSuccess": "Restauración exitosa. La aplicación se reiniciará en 1 segundo",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Confirm to delete this backup file?",
|
"confirmDelete": "Confirm to delete this backup file?",
|
||||||
"confirmRestore": "Confirm to restore this backup file?"
|
"confirmRestore": "Confirm to restore this backup file?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Nombre del archivo",
|
"filename": "Nombre del archivo",
|
||||||
"backupTime": "Tiempo de copia de seguridad",
|
"backupTime": "Tiempo de copia de seguridad",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Pausar",
|
"pause": "Pausar",
|
||||||
"resume": "Reanudar",
|
"resume": "Reanudar",
|
||||||
"closeAll": "Cerrar todas",
|
"closeAll": "Cerrar todas",
|
||||||
"clear": "Limpiar"
|
"clear": "Limpiar",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Actualizado el",
|
"updateAt": "Actualizado el",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "Milisegundos",
|
"milliseconds": "Milisegundos",
|
||||||
"seconds": "Segundos",
|
"seconds": "Segundos",
|
||||||
"minutes": "Minutos",
|
"minutes": "Minutos",
|
||||||
|
"hours": "Horas",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "حذف پشتیبان",
|
"deleteBackup": "حذف پشتیبان",
|
||||||
"restore": "بازیابی",
|
"restore": "بازیابی",
|
||||||
"restoreBackup": "بازیابی پشتیبان"
|
"restoreBackup": "بازیابی پشتیبان",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "http(s):// URL سرور WebDAV",
|
"webdavUrl": "http(s):// URL سرور WebDAV",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راهاندازی مجدد میشود",
|
"restoreSuccess": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راهاندازی مجدد میشود",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "آیا از حذف این فایل پشتیبان اطمینان دارید؟",
|
"confirmDelete": "آیا از حذف این فایل پشتیبان اطمینان دارید؟",
|
||||||
"confirmRestore": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟"
|
"confirmRestore": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "نام فایل",
|
"filename": "نام فایل",
|
||||||
"backupTime": "زمان پشتیبانگیری",
|
"backupTime": "زمان پشتیبانگیری",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "توقف",
|
"pause": "توقف",
|
||||||
"resume": "از سرگیری",
|
"resume": "از سرگیری",
|
||||||
"closeAll": "بستن همه",
|
"closeAll": "بستن همه",
|
||||||
"clear": "پاک کردن"
|
"clear": "پاک کردن",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "بهروزرسانی در",
|
"updateAt": "بهروزرسانی در",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "میلیثانیه",
|
"milliseconds": "میلیثانیه",
|
||||||
"seconds": "ثانیهها",
|
"seconds": "ثانیهها",
|
||||||
"minutes": "دقیقه",
|
"minutes": "دقیقه",
|
||||||
|
"hours": "ساعت",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Hapus Cadangan",
|
"deleteBackup": "Hapus Cadangan",
|
||||||
"restore": "Pulihkan",
|
"restore": "Pulihkan",
|
||||||
"restoreBackup": "Pulihkan Cadangan"
|
"restoreBackup": "Pulihkan Cadangan",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "URL Server WebDAV",
|
"webdavUrl": "URL Server WebDAV",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
"restoreSuccess": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Konfirmasi untuk menghapus file cadangan ini?",
|
"confirmDelete": "Konfirmasi untuk menghapus file cadangan ini?",
|
||||||
"confirmRestore": "Konfirmasi untuk memulihkan file cadangan ini?"
|
"confirmRestore": "Konfirmasi untuk memulihkan file cadangan ini?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Nama Berkas",
|
"filename": "Nama Berkas",
|
||||||
"backupTime": "Waktu Cadangan",
|
"backupTime": "Waktu Cadangan",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Jeda",
|
"pause": "Jeda",
|
||||||
"resume": "Lanjut",
|
"resume": "Lanjut",
|
||||||
"closeAll": "Tutup Semua",
|
"closeAll": "Tutup Semua",
|
||||||
"clear": "Bersihkan"
|
"clear": "Bersihkan",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Diperbarui Pada",
|
"updateAt": "Diperbarui Pada",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "milidetik",
|
"milliseconds": "milidetik",
|
||||||
"seconds": "detik",
|
"seconds": "detik",
|
||||||
"minutes": "menit",
|
"minutes": "menit",
|
||||||
|
"hours": "jam",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "バックアップを削除",
|
"deleteBackup": "バックアップを削除",
|
||||||
"restore": "復元",
|
"restore": "復元",
|
||||||
"restoreBackup": "バックアップを復元"
|
"restoreBackup": "バックアップを復元",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAVサーバーのURL http(s)://",
|
"webdavUrl": "WebDAVサーバーのURL http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "復元に成功しました。アプリケーションは1秒後に再起動します。",
|
"restoreSuccess": "復元に成功しました。アプリケーションは1秒後に再起動します。",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Confirm to delete this backup file?",
|
"confirmDelete": "Confirm to delete this backup file?",
|
||||||
"confirmRestore": "Confirm to restore this backup file?"
|
"confirmRestore": "Confirm to restore this backup file?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "ファイル名",
|
"filename": "ファイル名",
|
||||||
"backupTime": "バックアップ時間",
|
"backupTime": "バックアップ時間",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "一時停止",
|
"pause": "一時停止",
|
||||||
"resume": "再開",
|
"resume": "再開",
|
||||||
"closeAll": "すべて閉じる",
|
"closeAll": "すべて閉じる",
|
||||||
"clear": "クリア"
|
"clear": "クリア",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "更新日時",
|
"updateAt": "更新日時",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "ミリ秒",
|
"milliseconds": "ミリ秒",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"minutes": "分",
|
"minutes": "分",
|
||||||
|
"hours": "時間",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "백업 내보내기",
|
"exportBackup": "백업 내보내기",
|
||||||
"deleteBackup": "백업 삭제",
|
"deleteBackup": "백업 삭제",
|
||||||
"restore": "복원",
|
"restore": "복원",
|
||||||
"restoreBackup": "백업 복원"
|
"restoreBackup": "백업 복원",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV 서버 URL",
|
"webdavUrl": "WebDAV 서버 URL",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "복원 성공, 1초 후 앱이 재시작됩니다",
|
"restoreSuccess": "복원 성공, 1초 후 앱이 재시작됩니다",
|
||||||
"localBackupExported": "로컬 백업이 내보내졌습니다",
|
"localBackupExported": "로컬 백업이 내보내졌습니다",
|
||||||
"localBackupExportFailed": "로컬 백업 내보내기 실패",
|
"localBackupExportFailed": "로컬 백업 내보내기 실패",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "이 백업 파일을 삭제하시겠습니까?",
|
"confirmDelete": "이 백업 파일을 삭제하시겠습니까?",
|
||||||
"confirmRestore": "이 백업 파일을 복원하시겠습니까?"
|
"confirmRestore": "이 백업 파일을 복원하시겠습니까?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "파일명",
|
"filename": "파일명",
|
||||||
"backupTime": "백업 시간",
|
"backupTime": "백업 시간",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "일시 정지",
|
"pause": "일시 정지",
|
||||||
"resume": "재개",
|
"resume": "재개",
|
||||||
"closeAll": "모두 닫기",
|
"closeAll": "모두 닫기",
|
||||||
"clear": "지우기"
|
"clear": "지우기",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "업데이트 시간",
|
"updateAt": "업데이트 시간",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "밀리초",
|
"milliseconds": "밀리초",
|
||||||
"seconds": "초",
|
"seconds": "초",
|
||||||
"minutes": "분",
|
"minutes": "분",
|
||||||
|
"hours": "시간",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "파일"
|
"files": "파일"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Удалить резервную копию",
|
"deleteBackup": "Удалить резервную копию",
|
||||||
"restore": "Восстановить",
|
"restore": "Восстановить",
|
||||||
"restoreBackup": "Восстановить резервную копию"
|
"restoreBackup": "Восстановить резервную копию",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "URL-адрес сервера WebDAV http(s)://",
|
"webdavUrl": "URL-адрес сервера WebDAV http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду",
|
"restoreSuccess": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Вы уверены, что хотите удалить этот файл резервной копии?",
|
"confirmDelete": "Вы уверены, что хотите удалить этот файл резервной копии?",
|
||||||
"confirmRestore": "Вы уверены, что хотите восстановить этот файл резервной копии?"
|
"confirmRestore": "Вы уверены, что хотите восстановить этот файл резервной копии?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Имя файла",
|
"filename": "Имя файла",
|
||||||
"backupTime": "Время резервного копирования",
|
"backupTime": "Время резервного копирования",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Пауза",
|
"pause": "Пауза",
|
||||||
"resume": "Возобновить",
|
"resume": "Возобновить",
|
||||||
"closeAll": "Закрыть всё",
|
"closeAll": "Закрыть всё",
|
||||||
"clear": "Очистить"
|
"clear": "Очистить",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Обновлено в",
|
"updateAt": "Обновлено в",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "миллисекунды",
|
"milliseconds": "миллисекунды",
|
||||||
"seconds": "секунды",
|
"seconds": "секунды",
|
||||||
"minutes": "минуты",
|
"minutes": "минуты",
|
||||||
|
"hours": "часы",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Yedeği Sil",
|
"deleteBackup": "Yedeği Sil",
|
||||||
"restore": "Geri Yükle",
|
"restore": "Geri Yükle",
|
||||||
"restoreBackup": "Yedeği Geri Yükle"
|
"restoreBackup": "Yedeği Geri Yükle",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV Sunucu URL'si",
|
"webdavUrl": "WebDAV Sunucu URL'si",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak",
|
"restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?",
|
"confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?",
|
||||||
"confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?"
|
"confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Dosya Adı",
|
"filename": "Dosya Adı",
|
||||||
"backupTime": "Yedekleme Zamanı",
|
"backupTime": "Yedekleme Zamanı",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Duraklat",
|
"pause": "Duraklat",
|
||||||
"resume": "Sürdür",
|
"resume": "Sürdür",
|
||||||
"closeAll": "Tümünü Kapat",
|
"closeAll": "Tümünü Kapat",
|
||||||
"clear": "Temizle"
|
"clear": "Temizle",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Güncelleme Zamanı",
|
"updateAt": "Güncelleme Zamanı",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "ms",
|
"milliseconds": "ms",
|
||||||
"seconds": "saniye",
|
"seconds": "saniye",
|
||||||
"minutes": "dakika",
|
"minutes": "dakika",
|
||||||
|
"hours": "saat",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "Export Backup",
|
"exportBackup": "Export Backup",
|
||||||
"deleteBackup": "Резерв копияне бетерү",
|
"deleteBackup": "Резерв копияне бетерү",
|
||||||
"restore": "Кайтару",
|
"restore": "Кайтару",
|
||||||
"restoreBackup": "Резерв копияне кайтару"
|
"restoreBackup": "Резерв копияне кайтару",
|
||||||
|
"viewHistory": "View history"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV сервер URL-ы (http(s)://)",
|
"webdavUrl": "WebDAV сервер URL-ы (http(s)://)",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "Уңышлы кайтарылды, кушымта 1 секундтан яңадан башланачак",
|
"restoreSuccess": "Уңышлы кайтарылды, кушымта 1 секундтан яңадан башланачак",
|
||||||
"localBackupExported": "Local backup exported successfully",
|
"localBackupExported": "Local backup exported successfully",
|
||||||
"localBackupExportFailed": "Failed to export local backup",
|
"localBackupExportFailed": "Failed to export local backup",
|
||||||
|
"webdavRefreshSuccess": "WebDAV refresh succeeded",
|
||||||
|
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
|
||||||
"confirmDelete": "Бу резерв копия файлын бетерергә телисезме?",
|
"confirmDelete": "Бу резерв копия файлын бетерергә телисезме?",
|
||||||
"confirmRestore": "Бу резерв копия файлын кире кайтарырга телисезме?"
|
"confirmRestore": "Бу резерв копия файлын кире кайтарырга телисезме?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "Automatic backup",
|
||||||
|
"scheduleLabel": "Enable scheduled backup",
|
||||||
|
"scheduleHelper": "Create local backups in the background at the configured interval.",
|
||||||
|
"intervalLabel": "Backup frequency",
|
||||||
|
"changeLabel": "Backup on critical changes",
|
||||||
|
"changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.",
|
||||||
|
"options": {
|
||||||
|
"hours": "Every {{n}} hours",
|
||||||
|
"days": "Every {{n}} days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Manual backup",
|
||||||
|
"local": "Creates a snapshot on this device, stored under the app data directory.",
|
||||||
|
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
|
||||||
|
"configureWebdav": "Configure WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Backup history",
|
||||||
|
"summary": "{{count}} backups • latest {{recent}}",
|
||||||
|
"empty": "No backups available"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV settings"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "Файл исеме",
|
"filename": "Файл исеме",
|
||||||
"backupTime": "Резерв копия вакыты",
|
"backupTime": "Резерв копия вакыты",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "Туктау",
|
"pause": "Туктау",
|
||||||
"resume": "Дәвам",
|
"resume": "Дәвам",
|
||||||
"closeAll": "Барысын да ябу",
|
"closeAll": "Барысын да ябу",
|
||||||
"clear": "Чистарту"
|
"clear": "Чистарту",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "Яңартылган вакыт",
|
"updateAt": "Яңартылган вакыт",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "Миллисекундлар",
|
"milliseconds": "Миллисекундлар",
|
||||||
"seconds": "Секундлар",
|
"seconds": "Секундлар",
|
||||||
"minutes": "Минутлар",
|
"minutes": "Минутлар",
|
||||||
|
"hours": "Сәгатьләр",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "导出备份",
|
"exportBackup": "导出备份",
|
||||||
"deleteBackup": "删除备份",
|
"deleteBackup": "删除备份",
|
||||||
"restore": "恢复",
|
"restore": "恢复",
|
||||||
"restoreBackup": "恢复备份"
|
"restoreBackup": "恢复备份",
|
||||||
|
"viewHistory": "查看记录"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV 服务器地址 http(s)://",
|
"webdavUrl": "WebDAV 服务器地址 http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "恢复成功,应用将在 1 秒后重启",
|
"restoreSuccess": "恢复成功,应用将在 1 秒后重启",
|
||||||
"localBackupExported": "本地备份导出成功",
|
"localBackupExported": "本地备份导出成功",
|
||||||
"localBackupExportFailed": "本地备份导出失败",
|
"localBackupExportFailed": "本地备份导出失败",
|
||||||
|
"webdavRefreshSuccess": "WebDAV 刷新成功",
|
||||||
|
"webdavRefreshFailed": "WebDAV 刷新失败: {{error}}",
|
||||||
"confirmDelete": "确认删除此备份文件吗?",
|
"confirmDelete": "确认删除此备份文件吗?",
|
||||||
"confirmRestore": "确认恢复此份文件吗?"
|
"confirmRestore": "确认恢复此份文件吗?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "自动备份",
|
||||||
|
"scheduleLabel": "启用定时备份",
|
||||||
|
"scheduleHelper": "按设定频率在后台创建本地备份文件。",
|
||||||
|
"intervalLabel": "备份频率",
|
||||||
|
"changeLabel": "关键变更时自动备份",
|
||||||
|
"changeHelper": "全局扩展配置/脚本或订阅增删改后会自动备份。",
|
||||||
|
"options": {
|
||||||
|
"hours": "每 {{n}} 小时",
|
||||||
|
"days": "每 {{n}} 天"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "手动备份",
|
||||||
|
"local": "在本设备的应用数据目录中创建备份。",
|
||||||
|
"webdav": "配置 WebDAV 后,可直接上传备份到服务器。",
|
||||||
|
"configureWebdav": "配置 WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "备份记录",
|
||||||
|
"summary": "共 {{count}} 份备份 · 最近 {{recent}}",
|
||||||
|
"empty": "暂无备份记录"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV 设置"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "文件名称",
|
"filename": "文件名称",
|
||||||
"backupTime": "备份时间",
|
"backupTime": "备份时间",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "暂停",
|
"pause": "暂停",
|
||||||
"resume": "继续",
|
"resume": "继续",
|
||||||
"closeAll": "关闭全部",
|
"closeAll": "关闭全部",
|
||||||
"clear": "清除"
|
"clear": "清除",
|
||||||
|
"previous": "上一页",
|
||||||
|
"next": "下一页"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "更新于",
|
"updateAt": "更新于",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "毫秒",
|
"milliseconds": "毫秒",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"minutes": "分钟",
|
"minutes": "分钟",
|
||||||
|
"hours": "小时",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "文件"
|
"files": "文件"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,8 @@
|
|||||||
"exportBackup": "匯出備份",
|
"exportBackup": "匯出備份",
|
||||||
"deleteBackup": "刪除備份",
|
"deleteBackup": "刪除備份",
|
||||||
"restore": "還原",
|
"restore": "還原",
|
||||||
"restoreBackup": "還原備份"
|
"restoreBackup": "還原備份",
|
||||||
|
"viewHistory": "檢視紀錄"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"webdavUrl": "WebDAV 伺服器位址 http(s)://",
|
"webdavUrl": "WebDAV 伺服器位址 http(s)://",
|
||||||
@@ -306,9 +307,37 @@
|
|||||||
"restoreSuccess": "還原成功,應用程式將在 1 秒後重啟",
|
"restoreSuccess": "還原成功,應用程式將在 1 秒後重啟",
|
||||||
"localBackupExported": "本機備份匯出成功",
|
"localBackupExported": "本機備份匯出成功",
|
||||||
"localBackupExportFailed": "本機備份匯出失敗",
|
"localBackupExportFailed": "本機備份匯出失敗",
|
||||||
|
"webdavRefreshSuccess": "WebDAV 更新成功",
|
||||||
|
"webdavRefreshFailed": "WebDAV 更新失敗: {{error}}",
|
||||||
"confirmDelete": "確認是否刪除此備份檔案嗎?",
|
"confirmDelete": "確認是否刪除此備份檔案嗎?",
|
||||||
"confirmRestore": "確認還原此份檔案嗎?"
|
"confirmRestore": "確認還原此份檔案嗎?"
|
||||||
},
|
},
|
||||||
|
"auto": {
|
||||||
|
"title": "自動備份",
|
||||||
|
"scheduleLabel": "啟用定時備份",
|
||||||
|
"scheduleHelper": "依設定頻率在背景建立本機備份檔案。",
|
||||||
|
"intervalLabel": "備份頻率",
|
||||||
|
"changeLabel": "關鍵變更時自動備份",
|
||||||
|
"changeHelper": "全域擴充配置/腳本或訂閱新增、刪除、修改時自動備份。",
|
||||||
|
"options": {
|
||||||
|
"hours": "每 {{n}} 小時",
|
||||||
|
"days": "每 {{n}} 天"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "手動備份",
|
||||||
|
"local": "在本機應用資料資料夾建立備份檔。",
|
||||||
|
"webdav": "設定 WebDAV 後,可直接上傳備份至伺服器。",
|
||||||
|
"configureWebdav": "設定 WebDAV"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "備份紀錄",
|
||||||
|
"summary": "共 {{count}} 份備份 · 最近 {{recent}}",
|
||||||
|
"empty": "尚無備份紀錄"
|
||||||
|
},
|
||||||
|
"webdav": {
|
||||||
|
"title": "WebDAV 設定"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"filename": "檔案名稱",
|
"filename": "檔案名稱",
|
||||||
"backupTime": "備份時間",
|
"backupTime": "備份時間",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"pause": "暫停",
|
"pause": "暫停",
|
||||||
"resume": "繼續",
|
"resume": "繼續",
|
||||||
"closeAll": "關閉全部",
|
"closeAll": "關閉全部",
|
||||||
"clear": "清除"
|
"clear": "清除",
|
||||||
|
"previous": "上一頁",
|
||||||
|
"next": "下一頁"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"updateAt": "更新於",
|
"updateAt": "更新於",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"milliseconds": "毫秒",
|
"milliseconds": "毫秒",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"minutes": "分鐘",
|
"minutes": "分鐘",
|
||||||
|
"hours": "小時",
|
||||||
"kilobytes": "KB",
|
"kilobytes": "KB",
|
||||||
"files": "檔案"
|
"files": "檔案"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ export const translationKeys = [
|
|||||||
"settings.modals.backup.actions.deleteBackup",
|
"settings.modals.backup.actions.deleteBackup",
|
||||||
"settings.modals.backup.actions.restore",
|
"settings.modals.backup.actions.restore",
|
||||||
"settings.modals.backup.actions.restoreBackup",
|
"settings.modals.backup.actions.restoreBackup",
|
||||||
|
"settings.modals.backup.actions.viewHistory",
|
||||||
"settings.modals.backup.fields.webdavUrl",
|
"settings.modals.backup.fields.webdavUrl",
|
||||||
"settings.modals.backup.fields.username",
|
"settings.modals.backup.fields.username",
|
||||||
"settings.modals.backup.fields.info",
|
"settings.modals.backup.fields.info",
|
||||||
@@ -478,8 +479,26 @@ export const translationKeys = [
|
|||||||
"settings.modals.backup.messages.restoreSuccess",
|
"settings.modals.backup.messages.restoreSuccess",
|
||||||
"settings.modals.backup.messages.localBackupExported",
|
"settings.modals.backup.messages.localBackupExported",
|
||||||
"settings.modals.backup.messages.localBackupExportFailed",
|
"settings.modals.backup.messages.localBackupExportFailed",
|
||||||
|
"settings.modals.backup.messages.webdavRefreshSuccess",
|
||||||
|
"settings.modals.backup.messages.webdavRefreshFailed",
|
||||||
"settings.modals.backup.messages.confirmDelete",
|
"settings.modals.backup.messages.confirmDelete",
|
||||||
"settings.modals.backup.messages.confirmRestore",
|
"settings.modals.backup.messages.confirmRestore",
|
||||||
|
"settings.modals.backup.auto.title",
|
||||||
|
"settings.modals.backup.auto.scheduleLabel",
|
||||||
|
"settings.modals.backup.auto.scheduleHelper",
|
||||||
|
"settings.modals.backup.auto.intervalLabel",
|
||||||
|
"settings.modals.backup.auto.changeLabel",
|
||||||
|
"settings.modals.backup.auto.changeHelper",
|
||||||
|
"settings.modals.backup.auto.options.hours",
|
||||||
|
"settings.modals.backup.auto.options.days",
|
||||||
|
"settings.modals.backup.manual.title",
|
||||||
|
"settings.modals.backup.manual.local",
|
||||||
|
"settings.modals.backup.manual.webdav",
|
||||||
|
"settings.modals.backup.manual.configureWebdav",
|
||||||
|
"settings.modals.backup.history.title",
|
||||||
|
"settings.modals.backup.history.summary",
|
||||||
|
"settings.modals.backup.history.empty",
|
||||||
|
"settings.modals.backup.webdav.title",
|
||||||
"settings.modals.backup.table.filename",
|
"settings.modals.backup.table.filename",
|
||||||
"settings.modals.backup.table.backupTime",
|
"settings.modals.backup.table.backupTime",
|
||||||
"settings.modals.backup.table.actions",
|
"settings.modals.backup.table.actions",
|
||||||
@@ -639,6 +658,8 @@ export const translationKeys = [
|
|||||||
"shared.actions.resume",
|
"shared.actions.resume",
|
||||||
"shared.actions.closeAll",
|
"shared.actions.closeAll",
|
||||||
"shared.actions.clear",
|
"shared.actions.clear",
|
||||||
|
"shared.actions.previous",
|
||||||
|
"shared.actions.next",
|
||||||
"shared.labels.updateAt",
|
"shared.labels.updateAt",
|
||||||
"shared.labels.timeout",
|
"shared.labels.timeout",
|
||||||
"shared.labels.icon",
|
"shared.labels.icon",
|
||||||
@@ -659,6 +680,7 @@ export const translationKeys = [
|
|||||||
"shared.units.milliseconds",
|
"shared.units.milliseconds",
|
||||||
"shared.units.seconds",
|
"shared.units.seconds",
|
||||||
"shared.units.minutes",
|
"shared.units.minutes",
|
||||||
|
"shared.units.hours",
|
||||||
"shared.units.kilobytes",
|
"shared.units.kilobytes",
|
||||||
"shared.units.files",
|
"shared.units.files",
|
||||||
"shared.placeholders.filter",
|
"shared.placeholders.filter",
|
||||||
|
|||||||
@@ -690,12 +690,36 @@ export interface TranslationResources {
|
|||||||
restore: string;
|
restore: string;
|
||||||
restoreBackup: string;
|
restoreBackup: string;
|
||||||
selectTarget: string;
|
selectTarget: string;
|
||||||
|
viewHistory: string;
|
||||||
|
};
|
||||||
|
auto: {
|
||||||
|
changeHelper: string;
|
||||||
|
changeLabel: string;
|
||||||
|
intervalLabel: string;
|
||||||
|
options: {
|
||||||
|
days: string;
|
||||||
|
hours: string;
|
||||||
|
};
|
||||||
|
scheduleHelper: string;
|
||||||
|
scheduleLabel: string;
|
||||||
|
title: string;
|
||||||
};
|
};
|
||||||
fields: {
|
fields: {
|
||||||
info: string;
|
info: string;
|
||||||
username: string;
|
username: string;
|
||||||
webdavUrl: string;
|
webdavUrl: string;
|
||||||
};
|
};
|
||||||
|
history: {
|
||||||
|
empty: string;
|
||||||
|
summary: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
manual: {
|
||||||
|
configureWebdav: string;
|
||||||
|
local: string;
|
||||||
|
title: string;
|
||||||
|
webdav: string;
|
||||||
|
};
|
||||||
messages: {
|
messages: {
|
||||||
backupCreated: string;
|
backupCreated: string;
|
||||||
backupFailed: string;
|
backupFailed: string;
|
||||||
@@ -711,6 +735,8 @@ export interface TranslationResources {
|
|||||||
usernameRequired: string;
|
usernameRequired: string;
|
||||||
webdavConfigSaved: string;
|
webdavConfigSaved: string;
|
||||||
webdavConfigSaveFailed: string;
|
webdavConfigSaveFailed: string;
|
||||||
|
webdavRefreshFailed: string;
|
||||||
|
webdavRefreshSuccess: string;
|
||||||
webdavUrlRequired: string;
|
webdavUrlRequired: string;
|
||||||
};
|
};
|
||||||
table: {
|
table: {
|
||||||
@@ -725,6 +751,9 @@ export interface TranslationResources {
|
|||||||
webdav: string;
|
webdav: string;
|
||||||
};
|
};
|
||||||
title: string;
|
title: string;
|
||||||
|
webdav: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
clashCore: {
|
clashCore: {
|
||||||
variants: {
|
variants: {
|
||||||
@@ -1139,7 +1168,9 @@ export interface TranslationResources {
|
|||||||
hideDetails: string;
|
hideDetails: string;
|
||||||
listView: string;
|
listView: string;
|
||||||
new: string;
|
new: string;
|
||||||
|
next: string;
|
||||||
pause: string;
|
pause: string;
|
||||||
|
previous: string;
|
||||||
refresh: string;
|
refresh: string;
|
||||||
refreshPage: string;
|
refreshPage: string;
|
||||||
resetToDefault: string;
|
resetToDefault: string;
|
||||||
@@ -1242,6 +1273,7 @@ export interface TranslationResources {
|
|||||||
};
|
};
|
||||||
units: {
|
units: {
|
||||||
files: string;
|
files: string;
|
||||||
|
hours: string;
|
||||||
kilobytes: string;
|
kilobytes: string;
|
||||||
milliseconds: string;
|
milliseconds: string;
|
||||||
minutes: string;
|
minutes: string;
|
||||||
|
|||||||
3
src/types/types.d.ts
vendored
3
src/types/types.d.ts
vendored
@@ -854,6 +854,9 @@ interface IVergeConfig {
|
|||||||
enable_auto_delay_detection?: boolean;
|
enable_auto_delay_detection?: boolean;
|
||||||
enable_builtin_enhanced?: boolean;
|
enable_builtin_enhanced?: boolean;
|
||||||
auto_log_clean?: 0 | 1 | 2 | 3 | 4;
|
auto_log_clean?: 0 | 1 | 2 | 3 | 4;
|
||||||
|
enable_auto_backup_schedule?: boolean;
|
||||||
|
auto_backup_interval_hours?: number;
|
||||||
|
auto_backup_on_change?: boolean;
|
||||||
proxy_layout_column?: number;
|
proxy_layout_column?: number;
|
||||||
test_list?: IVergeTestItem[];
|
test_list?: IVergeTestItem[];
|
||||||
webdav_url?: string;
|
webdav_url?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user