mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2024-11-16 03:32:36 +08:00
Compare commits
24 Commits
9892470d08
...
976f70b8c2
Author | SHA1 | Date | |
---|---|---|---|
|
976f70b8c2 | ||
|
e87e06dd37 | ||
|
b9bb79b4dc | ||
|
d0f907f349 | ||
|
867c83b84c | ||
|
632d389411 | ||
|
b4df1b6e42 | ||
|
176620e3bb | ||
|
c37f22dc65 | ||
|
9d9cf27460 | ||
|
6e8def3ef7 | ||
|
ec0d872f11 | ||
|
9e4a8708e7 | ||
|
a681fdeee1 | ||
|
8763a76475 | ||
|
4306fba997 | ||
|
3759239dac | ||
|
4ec0b1d6e4 | ||
|
6271726f07 | ||
|
e5740579f4 | ||
|
ca97e3a3e6 | ||
|
d3f6822080 | ||
|
9d9a6dfddb | ||
|
c3e24d7b96 |
14
README.md
14
README.md
|
@ -32,7 +32,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:https://verge.dginv.click/#/register?code=oaxsAGo6
|
||||
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||
- 海外团队,无跑路风险,高达 50% 返佣
|
||||
|
@ -43,11 +43,13 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||
|
||||
## Features
|
||||
|
||||
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
|
||||
- Improved UI and supports custom theme color.
|
||||
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
|
||||
- System proxy setting and guard.
|
||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
|
||||
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
|
||||
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
|
||||
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
|
||||
- 可视化节点和规则编辑
|
||||
- WebDav 配置备份和同步
|
||||
|
||||
### FAQ
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
### Features
|
||||
|
||||
- Meta(mihomo)内核升级 1.18.9
|
||||
- Meta(mihomo)内核升级 1.18.10
|
||||
- Win 下的系统代理替换为 Shadowsocks/CFW/v2rayN 等成熟的 sysproxy.exe 方案,解决拨号/VPN 环境下无法设置系统代理的问题
|
||||
- 服务模式改进为启动软件时自动安装,TUN 模式可自由开启不再限制于服务模式
|
||||
- Mac 下可用 URL Scheme 导入订阅
|
||||
|
@ -25,6 +25,9 @@
|
|||
- 加入图标 svg 格式检测
|
||||
- 增加更多 app 调试日志
|
||||
- 添加 MacOS 下白色桌面的 tray 黑色配色(但会代理系统代理、tun 模式图标失效的问题)
|
||||
- 增加 Webdav 备份功能
|
||||
- 添加统一延迟的设置开关
|
||||
- 添加 Windows 下自动检测并下载 vc runtime 的功能
|
||||
|
||||
### Performance
|
||||
|
||||
|
|
90
package.json
90
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.0.0-rc.5",
|
||||
"version": "2.0.0-rc.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||
|
@ -21,77 +21,77 @@
|
|||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.16.0",
|
||||
"@mui/icons-material": "^6.1.6",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.16.0",
|
||||
"@mui/x-data-grid": "^7.9.0",
|
||||
"@tauri-apps/api": "2.0.0-rc.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-notification": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-process": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0-rc",
|
||||
"@mui/material": "^6.1.6",
|
||||
"@mui/x-data-grid": "^7.22.2",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
||||
"@tauri-apps/plugin-notification": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.0",
|
||||
"axios": "^1.7.2",
|
||||
"ahooks": "^3.8.1",
|
||||
"axios": "^1.7.7",
|
||||
"cli-color": "^2.0.4",
|
||||
"dayjs": "1.11.5",
|
||||
"foxact": "^0.2.35",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.41",
|
||||
"glob": "^11.0.0",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next": "^23.16.5",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.49.0",
|
||||
"monaco-yaml": "^5.2.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"peggy": "^4.0.3",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"monaco-yaml": "^5.2.3",
|
||||
"nanoid": "^5.0.8",
|
||||
"peggy": "^4.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-monaco-editor": "^0.55.0",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-monaco-editor": "^0.56.2",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.7.11",
|
||||
"react-virtuoso": "^4.12.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.2.5",
|
||||
"tar": "^6.2.1",
|
||||
"types-pac": "^1.0.2"
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.1.1",
|
||||
"@tauri-apps/cli": "2.0.4",
|
||||
"@tauri-apps/cli": "2.1.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@vitejs/plugin-legacy": "^5.4.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"adm-zip": "^0.5.14",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"husky": "^7.0.4",
|
||||
"meta-json-schema": "^1.18.9",
|
||||
"meta-json-schema": "^1.18.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"sass": "^1.77.6",
|
||||
"terser": "^5.31.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3",
|
||||
"sass": "^1.80.6",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
|
|
4464
pnpm-lock.yaml
4464
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import zlib from "zlib";
|
||||
import tar from "tar";
|
||||
import { extract } from "tar";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
|
@ -232,7 +232,7 @@ async function resolveSidecar(binInfo) {
|
|||
} else if (zipFile.endsWith(".tgz")) {
|
||||
// tgz
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
await tar.extract({
|
||||
await extract({
|
||||
cwd: tempDir,
|
||||
file: tempZip,
|
||||
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||
|
|
1136
src-tauri/Cargo.lock
generated
1136
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -10,7 +10,7 @@ edition = "2021"
|
|||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
|
@ -22,7 +22,7 @@ dunce = "1.0"
|
|||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.31.4"
|
||||
sysinfo = "0.32.0"
|
||||
boa_engine = "0.19.1"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
|
@ -37,7 +37,7 @@ tokio = { version = "1", features = ["full"] }
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
|
||||
tauri = { version = "2.0.0-rc", features = [
|
||||
tauri = { version = "2.1.1", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
|
@ -54,6 +54,8 @@ tauri-plugin-clipboard-manager = "2.0.1"
|
|||
tauri-plugin-deep-link = "2.0.1"
|
||||
tauri-plugin-devtools = "2.0.0-rc"
|
||||
url = "2.5.2"
|
||||
zip = "2.2.0"
|
||||
reqwest_dav = "0.1.14"
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
|
|
|
@ -11,6 +11,7 @@ use serde_yaml::Mapping;
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
|
@ -184,7 +185,7 @@ pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
|||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub async fn restart_sidecar() -> CmdResult {
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
|
@ -375,6 +376,50 @@ pub async fn exit_app() {
|
|||
feat::quit(Some(0));
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub mod service {
|
||||
use super::*;
|
||||
use crate::core::service;
|
||||
|
|
|
@ -147,6 +147,10 @@ pub struct IVerge {
|
|||
pub verge_port: Option<u16>,
|
||||
|
||||
pub verge_http_enabled: Option<bool>,
|
||||
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -304,6 +308,10 @@ impl IVerge {
|
|||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(auto_log_clean);
|
||||
|
||||
patch!(webdav_url);
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
|
154
src-tauri/src/core/backup.rs
Normal file
154
src-tauri/src/core/backup.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::Error;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||
use std::env::{consts::OS, temp_dir};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
pub struct WebDavClient {
|
||||
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
client: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
|
||||
if self.client.lock().is_none() {
|
||||
let verge = Config::verge().latest().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
{
|
||||
let msg =
|
||||
"Unable to create web dav client, please make sure the webdav config is correct"
|
||||
.to_string();
|
||||
log::error!(target: "app","{}",msg);
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
|
||||
let url = verge.webdav_url.unwrap_or_default();
|
||||
let username = verge.webdav_username.unwrap_or_default();
|
||||
let password = verge.webdav_password.unwrap_or_default();
|
||||
let url = url.trim_end_matches('/');
|
||||
let client = reqwest_dav::ClientBuilder::new()
|
||||
.set_agent(
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.set_host(url.to_owned())
|
||||
.set_auth(reqwest_dav::Auth::Basic(
|
||||
username.to_owned(),
|
||||
password.to_owned(),
|
||||
))
|
||||
.build()?;
|
||||
|
||||
if (client
|
||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||
.await)
|
||||
.is_err()
|
||||
{
|
||||
client.mkcol(dirs::BACKUP_DIR).await?;
|
||||
}
|
||||
|
||||
*self.client.lock() = Some(client.clone());
|
||||
}
|
||||
Ok(self.client.lock().clone().unwrap())
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
if !self.client.lock().is_none() {
|
||||
self.client.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||
let client = self.get_client().await?;
|
||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||
client
|
||||
.put(webdav_path.as_ref(), fs::read(file_path)?)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
||||
let client = self.get_client().await?;
|
||||
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
|
||||
let response = client.get(path.as_str()).await?;
|
||||
let content = response.bytes().await?;
|
||||
fs::write(&storage_path, &content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
|
||||
let client = self.get_client().await?;
|
||||
let files = client
|
||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(1))
|
||||
.await?;
|
||||
let mut final_files = Vec::new();
|
||||
for file in files {
|
||||
if let ListEntity::File(file) = file {
|
||||
final_files.push(file);
|
||||
}
|
||||
}
|
||||
Ok(final_files)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
|
||||
let client = self.get_client().await?;
|
||||
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||
client.delete(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||
let zip_path = temp_dir().join(&zip_file_name);
|
||||
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
zip.add_directory("profiles/", SimpleFileOptions::default())?;
|
||||
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
|
||||
zip.start_file(backup_path, options)?;
|
||||
zip.write_all(fs::read(path).unwrap().as_slice())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
zip.start_file(dirs::CLASH_CONFIG, options)?;
|
||||
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
||||
|
||||
let mut verge_config: serde_json::Value =
|
||||
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
|
||||
if let Some(obj) = verge_config.as_object_mut() {
|
||||
obj.remove("webdav_username");
|
||||
obj.remove("webdav_password");
|
||||
obj.remove("webdav_url");
|
||||
}
|
||||
zip.start_file(dirs::VERGE_CONFIG, options)?;
|
||||
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
|
||||
|
||||
zip.start_file(dirs::PROFILE_YAML, options)?;
|
||||
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
||||
zip.finish()?;
|
||||
Ok((zip_file_name, zip_path))
|
||||
}
|
|
@ -87,6 +87,7 @@ impl CoreManager {
|
|||
service::stop_core_by_service().await?;
|
||||
}
|
||||
*running = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod backup;
|
||||
pub mod clash_api;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use crate::{
|
||||
cmds,
|
||||
config::Config,
|
||||
core::CoreManager,
|
||||
feat, log_err, t,
|
||||
feat, t,
|
||||
utils::{
|
||||
dirs,
|
||||
resolve::{self, VERSION},
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{
|
||||
menu::CheckMenuItem,
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
|
||||
|
@ -17,7 +17,6 @@ use tauri::{
|
|||
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
Wry,
|
||||
};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use super::handle;
|
||||
pub struct Tray {}
|
||||
|
@ -408,7 +407,7 @@ fn create_tray_menu(
|
|||
Ok(menu)
|
||||
}
|
||||
|
||||
fn on_menu_event(app_handle: &AppHandle, event: MenuEvent) {
|
||||
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
|
@ -423,15 +422,7 @@ fn on_menu_event(app_handle: &AppHandle, event: MenuEvent) {
|
|||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
});
|
||||
resolve::resolve_reset();
|
||||
//睡1秒再重启
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
tauri::process::restart(&app_handle.env());
|
||||
}
|
||||
"restart_app" => feat::restart_app(),
|
||||
"quit" => {
|
||||
println!("quit");
|
||||
feat::quit(Some(0));
|
||||
|
|
|
@ -7,10 +7,15 @@
|
|||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::dirs::app_home_dir;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
// 打开面板
|
||||
pub fn open_or_close_dashboard() {
|
||||
|
@ -39,6 +44,19 @@ pub fn restart_clash_core() {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn restart_app() {
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
});
|
||||
resolve::resolve_reset();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
let _ = app_handle.save_window_state(StateFlags::default());
|
||||
tauri::process::restart(&app_handle.env());
|
||||
});
|
||||
}
|
||||
|
||||
// 切换模式 rule/global/direct/script mode
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
|
@ -401,3 +419,60 @@ pub async fn test_delay(url: String) -> Result<u32> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Err(err) = backup::WebDavClient::global()
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
backup::WebDavClient::global()
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
|
||||
// 最后删除临时文件
|
||||
fs::remove_file(backup_storage_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -84,7 +84,8 @@ pub fn run() {
|
|||
cmds::get_portable_flag,
|
||||
cmds::get_network_interfaces,
|
||||
// cmds::kill_sidecar,
|
||||
cmds::restart_sidecar,
|
||||
cmds::restart_core,
|
||||
cmds::restart_app,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::get_clash_logs,
|
||||
|
@ -123,7 +124,13 @@ pub fn run() {
|
|||
// service mode
|
||||
cmds::service::check_service,
|
||||
// clash api
|
||||
cmds::clash_api_get_proxy_delay
|
||||
cmds::clash_api_get_proxy_delay,
|
||||
// backup
|
||||
cmds::create_webdav_backup,
|
||||
cmds::save_webdav_config,
|
||||
cmds::list_webdav_backup,
|
||||
cmds::delete_webdav_backup,
|
||||
cmds::restore_webdav_backup,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
@ -6,14 +6,19 @@ use tauri::Manager;
|
|||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
|
||||
|
||||
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
|
||||
static CLASH_CONFIG: &str = "config.yaml";
|
||||
static VERGE_CONFIG: &str = "verge.yaml";
|
||||
static PROFILE_YAML: &str = "profiles.yaml";
|
||||
pub static CLASH_CONFIG: &str = "config.yaml";
|
||||
pub static VERGE_CONFIG: &str = "verge.yaml";
|
||||
pub static PROFILE_YAML: &str = "profiles.yaml";
|
||||
|
||||
/// init portable flag
|
||||
pub fn init_portable_flag() -> Result<()> {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge",
|
||||
"version": "2.0.0-rc.5",
|
||||
"version": "2.0.0-rc.6",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
|
|
|
@ -10,7 +10,7 @@ type Props = {
|
|||
};
|
||||
|
||||
export const BaseFieldset: React.FC<Props> = (props: Props) => {
|
||||
const Fieldset = styled(Box)(() => ({
|
||||
const Fieldset = styled(Box)<{ component?: string }>(() => ({
|
||||
position: "relative",
|
||||
border: "1px solid #bbb",
|
||||
borderRadius: "5px",
|
||||
|
|
33
src/components/base/base-loading-overlay.tsx
Normal file
33
src/components/base/base-loading-overlay.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
|
||||
export interface BaseLoadingOverlayProps {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
||||
isLoading,
|
||||
}) => {
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseLoadingOverlay;
|
|
@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
|
|||
export { BaseErrorBoundary } from "./base-error-boundary";
|
||||
export { Notice } from "./base-notice";
|
||||
export { Switch } from "./base-switch";
|
||||
export { BaseLoadingOverlay } from "./base-loading-overlay";
|
||||
|
|
|
@ -190,7 +190,7 @@ export const ProviderButton = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
|
@ -202,7 +202,8 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
|||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
|
||||
({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
|
@ -212,7 +213,8 @@ const StyledTypeBox = styled(Box)(({ theme }) => ({
|
|||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const boxStyle = {
|
||||
height: 26,
|
||||
|
|
|
@ -160,6 +160,16 @@ export const ProxyItemMini = (props: Props) => {
|
|||
TFO
|
||||
</TypeBox>
|
||||
)}
|
||||
{proxy.mptcp && (
|
||||
<TypeBox color="text.secondary" component="span">
|
||||
MPTCP
|
||||
</TypeBox>
|
||||
)}
|
||||
{proxy.smux && (
|
||||
<TypeBox color="text.secondary" component="span">
|
||||
SMUX
|
||||
</TypeBox>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -239,7 +249,9 @@ const Widget = styled(Box)(({ theme: { typography } }) => ({
|
|||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
const TypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme: { palette, typography } }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: "text.secondary",
|
||||
|
|
|
@ -31,7 +31,7 @@ const Widget = styled(Box)(() => ({
|
|||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled("span")(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.36),
|
||||
|
@ -121,14 +121,14 @@ export const ProxyItem = (props: Props) => {
|
|||
{showType && proxy.now && ` - ${proxy.now}`}
|
||||
</Box>
|
||||
{showType && !!proxy.provider && (
|
||||
<TypeBox component="span">{proxy.provider}</TypeBox>
|
||||
<TypeBox>{proxy.provider}</TypeBox>
|
||||
)}
|
||||
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
{showType && proxy.xudp && (
|
||||
<TypeBox component="span">XUDP</TypeBox>
|
||||
)}
|
||||
{showType && proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
|
||||
{showType && <TypeBox>{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox>UDP</TypeBox>}
|
||||
{showType && proxy.xudp && <TypeBox>XUDP</TypeBox>}
|
||||
{showType && proxy.tfo && <TypeBox>TFO</TypeBox>}
|
||||
{showType && proxy.mptcp && <TypeBox>MPTCP</TypeBox>}
|
||||
{showType && proxy.smux && <TypeBox>SMUX</TypeBox>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -162,7 +162,9 @@ export const ProviderButton = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
|
@ -174,7 +176,9 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
|||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
const StyledTypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
|
|
235
src/components/setting/mods/backup-config-viewer.tsx
Normal file
235
src/components/setting/mods/backup-config-viewer.tsx
Normal file
|
@ -0,0 +1,235 @@
|
|||
import { useState, useRef, memo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Notice } from "@/components/base";
|
||||
import { isValidUrl } from "@/utils/helper";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Grid2,
|
||||
Box,
|
||||
Stack,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
|
||||
|
||||
export interface BackupConfigViewerProps {
|
||||
onBackupSuccess: () => Promise<void>;
|
||||
onSaveSuccess: () => Promise<void>;
|
||||
onRefresh: () => Promise<void>;
|
||||
onInit: () => Promise<void>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const BackupConfigViewer = memo(
|
||||
({
|
||||
onBackupSuccess,
|
||||
onSaveSuccess,
|
||||
onRefresh,
|
||||
onInit,
|
||||
setLoading,
|
||||
}: BackupConfigViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const { webdav_url, webdav_username, webdav_password } = verge || {};
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
||||
defaultValues: {
|
||||
url: webdav_url,
|
||||
username: webdav_username,
|
||||
password: webdav_password,
|
||||
},
|
||||
});
|
||||
const url = watch("url");
|
||||
const username = watch("username");
|
||||
const password = watch("password");
|
||||
|
||||
const webdavChanged =
|
||||
webdav_url !== url ||
|
||||
webdav_username !== username ||
|
||||
webdav_password !== password;
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (webdav_url && webdav_username && webdav_password) {
|
||||
onInit();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkForm = () => {
|
||||
const username = usernameRef.current?.value;
|
||||
const password = passwordRef.current?.value;
|
||||
const url = urlRef.current?.value;
|
||||
|
||||
if (!url) {
|
||||
urlRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("WebDAV URL Required"));
|
||||
} else if (!isValidUrl(url)) {
|
||||
urlRef.current?.focus();
|
||||
Notice.error(t("Invalid WebDAV URL"));
|
||||
throw new Error(t("Invalid WebDAV URL"));
|
||||
}
|
||||
if (!username) {
|
||||
usernameRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("Username Required"));
|
||||
}
|
||||
if (!password) {
|
||||
passwordRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("Password Required"));
|
||||
}
|
||||
};
|
||||
|
||||
const save = useLockFn(async (data: IWebDavConfig) => {
|
||||
checkForm();
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveWebdavConfig(data.url, data.username, data.password).then(
|
||||
() => {
|
||||
Notice.success(t("WebDAV Config Saved"));
|
||||
onSaveSuccess();
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
Notice.error(t("WebDAV Config Save Failed", { error }), 3000);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const handleBackup = useLockFn(async () => {
|
||||
checkForm();
|
||||
try {
|
||||
setLoading(true);
|
||||
await createWebdavBackup().then(async () => {
|
||||
await onBackupSuccess();
|
||||
Notice.success(t("Backup Created"));
|
||||
});
|
||||
} catch (error) {
|
||||
Notice.error(t("Backup Failed", { error }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 12, sm: 9 }}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t("WebDAV Server URL")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("url")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={urlRef}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Username")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={usernameRef}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Password")}
|
||||
type={showPassword ? "text" : "password"}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={passwordRef}
|
||||
{...register("password")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 3 }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
alignItems="stretch"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{webdavChanged ||
|
||||
webdav_url === null ||
|
||||
webdav_username === null ||
|
||||
webdav_password === null ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color={"primary"}
|
||||
sx={{ height: "100%" }}
|
||||
type="button"
|
||||
onClick={handleSubmit(save)}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleBackup}
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Backup")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onRefresh}
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Refresh")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
);
|
266
src/components/setting/mods/backup-table-viewer.tsx
Normal file
266
src/components/setting/mods/backup-table-viewer.tsx
Normal file
|
@ -0,0 +1,266 @@
|
|||
import { SVGProps, memo } from "react";
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
IconButton,
|
||||
Divider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
} from "@mui/material";
|
||||
import { Notice } from "@/components/base";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dayjs } from "dayjs";
|
||||
import {
|
||||
deleteWebdavBackup,
|
||||
restoreWebDavBackup,
|
||||
restartApp,
|
||||
} from "@/services/cmds";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import RestoreIcon from "@mui/icons-material/Restore";
|
||||
|
||||
export type BackupFile = IWebDavFile & {
|
||||
platform: string;
|
||||
backup_time: Dayjs;
|
||||
allow_apply: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
|
||||
export interface BackupTableViewerProps {
|
||||
datasource: BackupFile[];
|
||||
page: number;
|
||||
onPageChange: (
|
||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||
page: number
|
||||
) => void;
|
||||
total: number;
|
||||
onRefresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const BackupTableViewer = memo(
|
||||
({
|
||||
datasource,
|
||||
page,
|
||||
onPageChange,
|
||||
total,
|
||||
onRefresh,
|
||||
}: BackupTableViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useLockFn(async (filename: string) => {
|
||||
await deleteWebdavBackup(filename);
|
||||
await onRefresh();
|
||||
});
|
||||
|
||||
const handleRestore = useLockFn(async (filename: string) => {
|
||||
await restoreWebDavBackup(filename).then(() => {
|
||||
Notice.success(t("Restore Success, App will restart in 1s"));
|
||||
});
|
||||
await restartApp();
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("Filename")}</TableCell>
|
||||
<TableCell>{t("Backup Time")}</TableCell>
|
||||
<TableCell align="right">{t("Actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{datasource.length > 0 ? (
|
||||
datasource?.map((file, index) => (
|
||||
<TableRow key={index}>
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
aria-label={t("Delete")}
|
||||
size="small"
|
||||
title={t("Delete Backup")}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await window.confirm(
|
||||
t("Confirm to delete this backup file?")
|
||||
);
|
||||
if (confirmed) {
|
||||
await handleDelete(file.filename);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
sx={{ mx: 1, height: 24 }}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label={t("Restore")}
|
||||
size="small"
|
||||
title={t("Restore Backup")}
|
||||
disabled={!file.allow_apply}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await window.confirm(
|
||||
t("Confirm to restore this backup file?")
|
||||
);
|
||||
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("No Backups")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[]}
|
||||
component="div"
|
||||
count={total}
|
||||
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
||||
page={page}
|
||||
onPageChange={onPageChange}
|
||||
labelRowsPerPage={t("Rows per page")}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
142
src/components/setting/mods/backup-viewer.tsx
Normal file
142
src/components/setting/mods/backup-viewer.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { BaseLoadingOverlay } from "@/components/base";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import {
|
||||
BackupTableViewer,
|
||||
BackupFile,
|
||||
DEFAULT_ROWS_PER_PAGE,
|
||||
} from "./backup-table-viewer";
|
||||
import { BackupConfigViewer } from "./backup-config-viewer";
|
||||
import { Box, Paper, Divider } from "@mui/material";
|
||||
import { listWebDavBackup } from "@/services/cmds";
|
||||
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}/;
|
||||
|
||||
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||
const [dataSource, setDataSource] = useState<BackupFile[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
// Handle page change
|
||||
const handleChangePage = useCallback(
|
||||
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
|
||||
setPage(page);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchAndSetBackupFiles = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const files = await getAllBackupFiles();
|
||||
setBackupFiles(files);
|
||||
setTotal(files.length);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Notice.error(t("Failed to fetch backup files"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllBackupFiles = async () => {
|
||||
const files = await listWebDavBackup();
|
||||
return files
|
||||
.map((file) => {
|
||||
const platform = file.filename.split("-")[0];
|
||||
const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
|
||||
|
||||
if (fileBackupTimeStr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
|
||||
const allowApply = OS === platform;
|
||||
return {
|
||||
...file,
|
||||
platform,
|
||||
backup_time: backupTime,
|
||||
allow_apply: allowApply,
|
||||
} as BackupFile;
|
||||
})
|
||||
.filter((item) => item !== null)
|
||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(
|
||||
backupFiles.slice(
|
||||
page * DEFAULT_ROWS_PER_PAGE,
|
||||
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE
|
||||
)
|
||||
);
|
||||
}, [page, backupFiles]);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Backup Setting")}
|
||||
contentSx={{ width: 600, maxHeight: 800 }}
|
||||
okBtn={t("")}
|
||||
cancelBtn={t("Close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
disableOk
|
||||
>
|
||||
<Box sx={{ maxWidth: 800 }}>
|
||||
<BaseLoadingOverlay isLoading={isLoading} />
|
||||
<Paper elevation={2} sx={{ padding: 2 }}>
|
||||
<BackupConfigViewer
|
||||
setLoading={setIsLoading}
|
||||
onBackupSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onSaveSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onRefresh={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onInit={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ marginY: 2 }} />
|
||||
<BackupTableViewer
|
||||
datasource={dataSource}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
total={total}
|
||||
onRefresh={fetchAndSetBackupFiles}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -17,7 +17,7 @@ import {
|
|||
ListItemButton,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { changeClashCore, restartSidecar } from "@/services/cmds";
|
||||
import { changeClashCore, restartCore } from "@/services/cmds";
|
||||
import { closeAllConnections, upgradeCore } from "@/services/api";
|
||||
|
||||
const VALID_CORE = [
|
||||
|
@ -59,7 +59,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
|
||||
const onRestart = useLockFn(async () => {
|
||||
try {
|
||||
await restartSidecar();
|
||||
await restartCore();
|
||||
Notice.success(t(`Clash Core Restarted`), 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
|
||||
import { TextField, Select, MenuItem, Typography } from "@mui/material";
|
||||
import {
|
||||
SettingsRounded,
|
||||
ShuffleRounded,
|
||||
LanRounded,
|
||||
InfoRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { DialogRef, Notice, Switch } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
|
@ -122,11 +113,10 @@ const SettingClash = ({ onError }: Props) => {
|
|||
<SettingItem
|
||||
label={t("Unified Delay")}
|
||||
extra={
|
||||
<Tooltip title={t("Unified Delay Info")} placement="top">
|
||||
<IconButton color="inherit" size="small">
|
||||
<InfoRounded fontSize="inherit" sx={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<TooltipIcon
|
||||
title={t("Unified Delay Info")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
|
@ -141,7 +131,12 @@ const SettingClash = ({ onError }: Props) => {
|
|||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Log Level")}>
|
||||
<SettingItem
|
||||
label={t("Log Level")}
|
||||
extra={
|
||||
<TooltipIcon title={t("Log Level Info")} sx={{ opacity: "0.7" }} />
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
// clash premium 2022.08.26 值为warn
|
||||
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
|
||||
|
|
|
@ -114,7 +114,9 @@ const SettingSystem = ({ onError }: Props) => {
|
|||
|
||||
<SettingItem
|
||||
label={t("Silent Start")}
|
||||
extra={<TooltipIcon title={t("Silent Start Info")} />}
|
||||
extra={
|
||||
<TooltipIcon title={t("Silent Start Info")} sx={{ opacity: "0.7" }} />
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
value={enable_silent_start ?? false}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer";
|
|||
import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { routers } from "@/pages/_routers";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
|
@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
const themeRef = useRef<DialogRef>(null);
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
|
@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
<MiscViewer ref={miscRef} />
|
||||
<LayoutViewer ref={layoutRef} />
|
||||
<UpdateViewer ref={updateRef} />
|
||||
<BackupViewer ref={backupRef} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
|
@ -238,6 +241,17 @@ const SettingVerge = ({ onError }: Props) => {
|
|||
label={t("Hotkey Setting")}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => backupRef.current?.open()}
|
||||
label={t("Backup Setting")}
|
||||
extra={
|
||||
<TooltipIcon
|
||||
title={t("Backup Setting Info")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => configRef.current?.open()}
|
||||
label={t("Runtime Config")}
|
||||
|
|
|
@ -332,6 +332,7 @@
|
|||
"clash_mode_direct": "Direct Mode",
|
||||
"toggle_system_proxy": "Enable/Disable System Proxy",
|
||||
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
||||
"Backup Setting": "Backup Setting",
|
||||
"Runtime Config": "Runtime Config",
|
||||
"Open Conf Dir": "Open Conf Dir",
|
||||
"Open Core Dir": "Open Core Dir",
|
||||
|
@ -370,5 +371,29 @@
|
|||
"Switched to _clash Core": "Switched to {{core}} Core",
|
||||
"GeoData Updated": "GeoData Updated",
|
||||
"Currently on the Latest Version": "Currently on the Latest Version",
|
||||
"Import Subscription Successful": "Import subscription successful"
|
||||
"Import Subscription Successful": "Import subscription successful",
|
||||
"WebDAV Server URL": "WebDAV Server URL",
|
||||
"Username": "Username",
|
||||
"Password": "Password",
|
||||
"Backup": "Backup",
|
||||
"Filename": "Filename",
|
||||
"Actions": "Actions",
|
||||
"Restore": "Restore",
|
||||
"No Backups": "No backups available",
|
||||
"WebDAV URL Required": "WebDAV URL cannot be empty",
|
||||
"Invalid WebDAV URL": "Invalid WebDAV URL format",
|
||||
"Username Required": "Username cannot be empty",
|
||||
"Password Required": "Password cannot be empty",
|
||||
"Failed to Fetch Backups": "Failed to fetch backup files",
|
||||
"WebDAV Config Saved": "WebDAV configuration saved successfully",
|
||||
"WebDAV Config Save Failed": "Failed to save WebDAV configuration: {{error}}",
|
||||
"Backup Created": "Backup created successfully",
|
||||
"Backup Failed": "Backup failed: {{error}}",
|
||||
"Delete Backup": "Delete Backup",
|
||||
"Restore Backup": "Restore Backup",
|
||||
"Backup Time": "Backup Time",
|
||||
"Confirm to delete this backup file?": "Confirm to delete this backup file?",
|
||||
"Confirm to restore this backup file?": "Confirm to restore this backup file?",
|
||||
"Restore Success, App will restart in 1s": "Restore Success, App will restart in 1s",
|
||||
"Failed to fetch backup files": "Failed to fetch backup files"
|
||||
}
|
||||
|
|
|
@ -65,6 +65,8 @@
|
|||
"Append Rule": "اضافه کردن قانون به انتها",
|
||||
"Prepend Group": "اضافه کردن گروه به ابتدا",
|
||||
"Append Group": "اضافه کردن گروه به انتها",
|
||||
"Prepend Proxy": "پیشافزودن پراکسی",
|
||||
"Append Proxy": "پسافزودن پراکسی",
|
||||
"Rule Condition Required": "شرط قانون الزامی است",
|
||||
"Invalid Rule": "قانون نامعتبر",
|
||||
"Advanced": "پیشرفته",
|
||||
|
@ -111,7 +113,7 @@
|
|||
"select": "انتخاب پروکسی به صورت دستی",
|
||||
"url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL",
|
||||
"fallback": "تعویض به پروکسی دیگر در صورت بروز خطا",
|
||||
"load-balance": "توزیع پروکسی بر اساس توازن بار",
|
||||
"load-balance": "توزیع <EFBFBD><EFBFBD>روکسی بر اساس توازن بار",
|
||||
"relay": "عبور از زنجیره پروکسی تعریف شده",
|
||||
"Group Name": "نام گروه",
|
||||
"Use Proxies": "استفاده از پروکسیها",
|
||||
|
@ -235,6 +237,9 @@
|
|||
"Auto Launch": "راهاندازی خودکار",
|
||||
"Silent Start": "شروع بیصدا",
|
||||
"Silent Start Info": "برنامه را در حالت پسزمینه بدون نمایش پانل اجرا کنید",
|
||||
"TG Channel": "کانال تلگرام",
|
||||
"Manual": "راهنما",
|
||||
"Github Repo": "مخزن GitHub",
|
||||
"Clash Setting": "تنظیمات Clash",
|
||||
"Allow Lan": "اجازه LAN",
|
||||
"Network Interface": "رابط شبکه",
|
||||
|
@ -244,6 +249,7 @@
|
|||
"Unified Delay": "معادلDELAY",
|
||||
"Unified Delay Info": "معادلDELAY را فعال کنید تا ترافیک شبکه به سرعت رسید",
|
||||
"Log Level": "سطح لاگ",
|
||||
"Log Level Info": "این فقط روی فایلهای لاگ هسته تحت فایل سرویس در فهرست ورود اثر میگذارد.",
|
||||
"Port Config": "پیکربندی پورت",
|
||||
"Random Port": "پورت تصادفی",
|
||||
"Mixed Port": "پورت پروکسی ترکیبی",
|
||||
|
@ -269,9 +275,6 @@
|
|||
"Open UWP tool": "باز کردن ابزار UWP",
|
||||
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامههای UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شدهاند و این ابزار میتواند برای دور زدن این محدودیت استفاده شود",
|
||||
"Update GeoData": "بهروزرسانی GeoData",
|
||||
"TG Channel": "کانال تلگرام",
|
||||
"Manual": "راهنما",
|
||||
"Github Repo": "مخزن GitHub",
|
||||
"Verge Setting": "تنظیمات Verge",
|
||||
"Language": "زبان",
|
||||
"Theme Mode": "حالت تم",
|
||||
|
@ -330,6 +333,8 @@
|
|||
"clash_mode_direct": "حالت مستقیم",
|
||||
"toggle_system_proxy": "فعال/غیرفعال کردن پراکسی سیستم",
|
||||
"toggle_tun_mode": "فعال/غیرفعال کردن حالت Tun",
|
||||
"Backup Setting": "تنظیمات پشتیبان گیری",
|
||||
"Backup Setting Info": "از فایل های پیکربندی پشتیبان WebDAV پشتیبانی می کند",
|
||||
"Runtime Config": "پیکربندی زمان اجرا",
|
||||
"Open Conf Dir": "باز کردن پوشه برنامه",
|
||||
"Open Core Dir": "باز کردن پوشه هسته",
|
||||
|
@ -368,5 +373,29 @@
|
|||
"Switched to _clash Core": "تغییر به هسته {{core}}",
|
||||
"GeoData Updated": "GeoData بهروزرسانی شد",
|
||||
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
|
||||
"Import Subscription Successfully": "عضویت با موفقیت وارد شد"
|
||||
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
|
||||
"WebDAV Server URL": "http(s):// URL سرور WebDAV",
|
||||
"Username": "نام کاربری",
|
||||
"Password": "رمز عبور",
|
||||
"Backup": "پشتیبانگیری",
|
||||
"Filename": "نام فایل",
|
||||
"Actions": "عملیات",
|
||||
"Restore": "بازیابی",
|
||||
"No Backups": "هیچ پشتیبانی موجود نیست",
|
||||
"WebDAV URL Required": "آدرس WebDAV نمیتواند خالی باشد",
|
||||
"Invalid WebDAV URL": "فرمت آدرس WebDAV نامعتبر است",
|
||||
"Username Required": "نام کاربری نمیتواند خالی باشد",
|
||||
"Password Required": "رمز عبور نمیتواند خالی باشد",
|
||||
"Failed to Fetch Backups": "دریافت فایلهای پشتیبان ناموفق بود",
|
||||
"WebDAV Config Saved": "پیکربندی WebDAV با موفقیت ذخیره شد",
|
||||
"WebDAV Config Save Failed": "خطا در ذخیره تنظیمات WebDAV: {{error}}",
|
||||
"Backup Created": "پشتیبانگیری با موفقیت ایجاد شد",
|
||||
"Backup Failed": "خطا در پشتیبانگیری: {{error}}",
|
||||
"Delete Backup": "حذف پشتیبان",
|
||||
"Restore Backup": "بازیابی پشتیبان",
|
||||
"Backup Time": "زمان پشتیبانگیری",
|
||||
"Confirm to delete this backup file?": "آیا از حذف این فایل پشتیبان اطمینان دارید؟",
|
||||
"Confirm to restore this backup file?": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟",
|
||||
"Restore Success, App will restart in 1s": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راهاندازی مجدد میشود",
|
||||
"Failed to fetch backup files": "دریافت فایلهای پشتیبان ناموفق بود"
|
||||
}
|
||||
|
|
|
@ -65,6 +65,8 @@
|
|||
"Append Rule": "Добавить правило в конец",
|
||||
"Prepend Group": "Добавить группу в начало",
|
||||
"Append Group": "Добавить группу в конец",
|
||||
"Prepend Proxy": "Добавить прокси в начало",
|
||||
"Append Proxy": "Добавить прокси в конец",
|
||||
"Rule Condition Required": "Требуется условие правила",
|
||||
"Invalid Rule": "Недействительное правило",
|
||||
"Advanced": "Дополнительно",
|
||||
|
@ -247,6 +249,7 @@
|
|||
"Unified Delay": "Общий задержка",
|
||||
"Unified Delay Info": "Когда унифицированная задержка включена, будут выполнены два теста задержки, чтобы устранить различия в задержке между разными типами узлов, вызванные подтверждением соединения и т. д",
|
||||
"Log Level": "Уровень логов",
|
||||
"Log Level Info": "Это действует только на файлы журнала ядра в служебном файле в каталоге журналов.",
|
||||
"Port Config": "Настройка порта",
|
||||
"Random Port": "Случайный порт",
|
||||
"Mixed Port": "Смешанный прокси-порт",
|
||||
|
@ -330,6 +333,8 @@
|
|||
"clash_mode_direct": "Прямой режим",
|
||||
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
||||
"toggle_tun_mode": "Включить/Отключить режим туннеля",
|
||||
"Backup Setting": "Настройки резервного копирования",
|
||||
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
|
||||
"Runtime Config": "Используемый конфиг",
|
||||
"Open Conf Dir": "Открыть папку приложения",
|
||||
"Open Core Dir": "Открыть папку ядра",
|
||||
|
@ -368,5 +373,29 @@
|
|||
"Switched to _clash Core": "Переключено на ядра {{core}}",
|
||||
"GeoData Updated": "GeoData Обновлена",
|
||||
"Currently on the Latest Version": "В настоящее время используется последняя версия",
|
||||
"Import subscription successful": "Импорт подписки успешно"
|
||||
"Import subscription successful": "Импорт подписки успешно",
|
||||
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
|
||||
"Username": "Имя пользователя",
|
||||
"Password": "Пароль",
|
||||
"Backup": "Резервное копирование",
|
||||
"Filename": "Имя файла",
|
||||
"Actions": "Действия",
|
||||
"Restore": "Восстановить",
|
||||
"No Backups": "Нет доступных резервных копий",
|
||||
"WebDAV URL Required": "URL-адрес WebDAV не может быть пустым",
|
||||
"Invalid WebDAV URL": "Неверный формат URL-адреса WebDAV",
|
||||
"Username Required": "Имя пользователя не может быть пустым",
|
||||
"Password Required": "Пароль не может быть пустым",
|
||||
"Failed to Fetch Backups": "Не удалось получить файлы резервных копий",
|
||||
"WebDAV Config Saved": "Конфигурация WebDAV успешно сохранена",
|
||||
"WebDAV Config Save Failed": "Не удалось сохранить конфигурацию WebDAV: {{error}}",
|
||||
"Backup Created": "Резервная копия успешно создана",
|
||||
"Backup Failed": "Ошибка резервного копирования: {{error}}",
|
||||
"Delete Backup": "Удалить резервную копию",
|
||||
"Restore Backup": "Восстановить резервную копию",
|
||||
"Backup Time": "Время резервного копирования",
|
||||
"Confirm to delete this backup file?": "Вы уверены, что хотите удалить этот файл резервной копии?",
|
||||
"Confirm to restore this backup file?": "Вы уверены, что хотите восстановить этот файл резервной копии?",
|
||||
"Restore Success, App will restart in 1s": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду",
|
||||
"Failed to fetch backup files": "Не удалось получить файлы резервных копий"
|
||||
}
|
||||
|
|
|
@ -246,9 +246,10 @@
|
|||
"Ip Address": "IP 地址",
|
||||
"Mac Address": "MAC 地址",
|
||||
"IPv6": "IPv6",
|
||||
"Unified Delay": "统一延时",
|
||||
"Unified Delay": "统一延迟",
|
||||
"Unified Delay Info": "开启统一延迟时,会进行两次延迟测试,以消除连接握手等带来的不同类型节点的延迟差异",
|
||||
"Log Level": "日志等级",
|
||||
"Log Level Info": "仅对日志目录Service文件夹下的内核日志文件生效",
|
||||
"Port Config": "端口设置",
|
||||
"Random Port": "随机端口",
|
||||
"Mixed Port": "混合代理端口",
|
||||
|
@ -332,6 +333,8 @@
|
|||
"clash_mode_direct": "直连模式",
|
||||
"toggle_system_proxy": "打开/关闭系统代理",
|
||||
"toggle_tun_mode": "打开/关闭 Tun 模式",
|
||||
"Backup Setting": "备份设置",
|
||||
"Backup Setting Info": "支持WebDAV备份配置文件",
|
||||
"Runtime Config": "当前配置",
|
||||
"Open Conf Dir": "配置目录",
|
||||
"Open Core Dir": "内核目录",
|
||||
|
@ -370,5 +373,29 @@
|
|||
"Switched to _clash Core": "已切换至 {{core}} 内核",
|
||||
"GeoData Updated": "已更新 GeoData",
|
||||
"Currently on the Latest Version": "当前已是最新版本",
|
||||
"Import Subscription Successful": "导入订阅成功"
|
||||
"Import Subscription Successful": "导入订阅成功",
|
||||
"WebDAV Server URL": "WebDAV服务器地址 http(s)://",
|
||||
"Username": "用户名",
|
||||
"Password": "密码",
|
||||
"Backup": "备份",
|
||||
"Filename": "文件名称",
|
||||
"Actions": "操作",
|
||||
"Restore": "恢复",
|
||||
"No Backups": "暂无备份",
|
||||
"WebDAV URL Required": "WebDAV 服务器地址不能为空",
|
||||
"Invalid WebDAV URL": "无效的 WebDAV 服务器地址格式",
|
||||
"Username Required": "用户名不能为空",
|
||||
"Password Required": "密码不能为空",
|
||||
"Failed to Fetch Backups": "获取备份文件失败",
|
||||
"WebDAV Config Saved": "WebDAV 配置保存成功",
|
||||
"WebDAV Config Save Failed": "保存 WebDAV 配置失败: {{error}}",
|
||||
"Backup Created": "备份创建成功",
|
||||
"Backup Failed": "备份失败: {{error}}",
|
||||
"Delete Backup": "删除备份",
|
||||
"Restore Backup": "恢复备份",
|
||||
"Backup Time": "备份时间",
|
||||
"Confirm to delete this backup file?": "确认删除此备份文件吗?",
|
||||
"Confirm to restore this backup file?": "确认恢复此 份文件吗?",
|
||||
"Restore Success, App will restart in 1s": "恢复成功,应用将在1秒后重启",
|
||||
"Failed to fetch backup files": "获取备份文件失败"
|
||||
}
|
||||
|
|
|
@ -122,6 +122,8 @@ export const getProxies = async () => {
|
|||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -141,8 +141,12 @@ export async function changeClashCore(clashCore: string) {
|
|||
return invoke<any>("change_clash_core", { clashCore });
|
||||
}
|
||||
|
||||
export async function restartSidecar() {
|
||||
return invoke<void>("restart_sidecar");
|
||||
export async function restartCore() {
|
||||
return invoke<void>("restart_core");
|
||||
}
|
||||
|
||||
export async function restartApp() {
|
||||
return invoke<void>("restart_app");
|
||||
}
|
||||
|
||||
export async function getAppDir() {
|
||||
|
@ -236,3 +240,35 @@ export async function getNetworkInterfaces() {
|
|||
export async function getNetworkInterfacesInfo() {
|
||||
return invoke<INetworkInterface[]>("get_network_interfaces_info");
|
||||
}
|
||||
|
||||
export async function createWebdavBackup() {
|
||||
return invoke<void>("create_webdav_backup");
|
||||
}
|
||||
|
||||
export async function deleteWebdavBackup(filename: string) {
|
||||
return invoke<void>("delete_webdav_backup", { filename });
|
||||
}
|
||||
|
||||
export async function restoreWebDavBackup(filename: string) {
|
||||
return invoke<void>("restore_webdav_backup", { filename });
|
||||
}
|
||||
|
||||
export async function saveWebdavConfig(
|
||||
url: string,
|
||||
username: string,
|
||||
password: String
|
||||
) {
|
||||
return invoke<void>("save_webdav_config", {
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listWebDavBackup() {
|
||||
let list: IWebDavFile[] = await invoke<IWebDavFile[]>("list_webdav_backup");
|
||||
list.map((item) => {
|
||||
item.filename = item.href.split("/").pop() as string;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
|
23
src/services/types.d.ts
vendored
23
src/services/types.d.ts
vendored
|
@ -56,6 +56,8 @@ interface IProxyItem {
|
|||
udp: boolean;
|
||||
xudp: boolean;
|
||||
tfo: boolean;
|
||||
mptcp: boolean;
|
||||
smux: boolean;
|
||||
history: {
|
||||
time: string;
|
||||
delay: number;
|
||||
|
@ -468,6 +470,7 @@ interface IProxyVlessConfig extends IProxyBaseConfig {
|
|||
fingerprint?: string;
|
||||
servername?: string;
|
||||
"client-fingerprint"?: ClientFingerprint;
|
||||
smux?: boolean;
|
||||
}
|
||||
// vmess
|
||||
interface IProxyVmessConfig extends IProxyBaseConfig {
|
||||
|
@ -496,6 +499,7 @@ interface IProxyVmessConfig extends IProxyBaseConfig {
|
|||
"global-padding"?: boolean;
|
||||
"authenticated-length"?: boolean;
|
||||
"client-fingerprint"?: ClientFingerprint;
|
||||
smux?: boolean;
|
||||
}
|
||||
interface WireGuardPeerOptions {
|
||||
server?: string;
|
||||
|
@ -604,6 +608,7 @@ interface IProxyShadowsocksConfig extends IProxyBaseConfig {
|
|||
"udp-over-tcp"?: boolean;
|
||||
"udp-over-tcp-version"?: number;
|
||||
"client-fingerprint"?: ClientFingerprint;
|
||||
smux?: boolean;
|
||||
}
|
||||
// shadowsocksR
|
||||
interface IProxyshadowsocksRConfig extends IProxyBaseConfig {
|
||||
|
@ -744,4 +749,22 @@ interface IVergeConfig {
|
|||
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||
proxy_layout_column?: number;
|
||||
test_list?: IVergeTestItem[];
|
||||
webdav_url?: string;
|
||||
webdav_username?: string;
|
||||
webdav_password?: string;
|
||||
}
|
||||
|
||||
interface IWebDavFile {
|
||||
filename: string;
|
||||
href: string;
|
||||
last_modified: string;
|
||||
content_length: number;
|
||||
content_type: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface IWebDavConfig {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
|
9
src/utils/helper.ts
Normal file
9
src/utils/helper.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const isValidUrl = (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -23,8 +23,10 @@ export const createSockette = (
|
|||
remainRetryCount -= 1;
|
||||
|
||||
if (remainRetryCount >= 0) {
|
||||
if (this instanceof Sockette) {
|
||||
this.close();
|
||||
this.reconnect();
|
||||
}
|
||||
} else {
|
||||
opt.onerror?.call(this, ev);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user