Compare commits
21 Commits
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
|
||||
|
||||
### Setup for Windows Users
|
||||
|
||||
If you're a Windows user, you may need to perform some additional steps:
|
||||
|
||||
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||
- The gnu `patch` tool should be installed
|
||||
|
||||
### Install Node.js Packages
|
||||
|
||||
After installing Rust and Node.js, install the necessary Node.js packages:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### Download the Clash Binary
|
||||
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
- Automatically download it via the provided script:
|
||||
```shell
|
||||
pnpm run check
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
```
|
||||
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
### Run the Development Server
|
||||
|
||||
To run the development server, use the following command:
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
# If an app instance already exists, use a different command
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
If you want to build the project, use:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Contributing Your Changes
|
||||
|
||||
Once you have made your changes:
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Commit your changes with clear and concise commit messages.
|
||||
4. Push your branch to your fork and submit a pull request to our repository.
|
||||
|
||||
We appreciate your contributions and look forward to your active participation in our project!
|
||||
46
README.md
46
README.md
@@ -41,18 +41,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
|
||||
Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/Clash.Verge_1.4.8_x64-setup.exe)
|
||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/Clash.Verge_1.4.8_x86-setup.exe)
|
||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/Clash.Verge_1.4.8_arm64-setup.exe)
|
||||
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x64-setup.exe)
|
||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x86-setup.exe)
|
||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_arm64-setup.exe)
|
||||
|
||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/Clash.Verge_1.4.8_x64.dmg)
|
||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/Clash.Verge_1.4.8_aarch64.dmg)
|
||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x64.dmg)
|
||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_aarch64.dmg)
|
||||
|
||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/clash-verge_1.4.8_amd64.AppImage)
|
||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/clash-verge_1.4.8_amd64.deb)
|
||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/clash-verge_1.4.8_i386.AppImage)
|
||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/clash-verge_1.4.8_i386.deb)
|
||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.8/clash-verge_1.4.8_arm64.deb)
|
||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_amd64.AppImage)
|
||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_amd64.deb)
|
||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_i386.AppImage)
|
||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_i386.deb)
|
||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_arm64.deb)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
@@ -66,34 +66,14 @@ open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/C
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
||||
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
Then download the clash binary... Or you can download it from [clash meta release](https://github.com/MetaCubeX/Clash.Meta/releases) and rename it according to [tauri config](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
```shell
|
||||
# force update to latest version
|
||||
# pnpm run check --force
|
||||
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Todos
|
||||
|
||||
31
UPDATELOG.md
31
UPDATELOG.md
@@ -1,3 +1,34 @@
|
||||
## v1.4.10
|
||||
|
||||
### Features
|
||||
|
||||
- 设置中添加退出按钮
|
||||
- 支持自定义软件启动页
|
||||
- 在 Proxy Provider 页面展示订阅信息
|
||||
- 优化 Provider 支持
|
||||
|
||||
### Bugs fixed:
|
||||
|
||||
- 更改端口时立即重设系统代理
|
||||
- 网站测试超时错误
|
||||
|
||||
---
|
||||
|
||||
## v1.4.9
|
||||
|
||||
### Features
|
||||
|
||||
- 支持启动时运行脚本
|
||||
- 支持代理组显示图标
|
||||
- 新增测试页面
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 连接页面时间排序错误
|
||||
- 连接页面表格宽度优化
|
||||
|
||||
---
|
||||
|
||||
## v1.4.8
|
||||
|
||||
### Features
|
||||
|
||||
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.4.8",
|
||||
"version": "1.4.10",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
@@ -19,26 +19,27 @@
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/icons-material": "^5.15.5",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.14.19",
|
||||
"@mui/x-data-grid": "^6.18.2",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@mui/material": "^5.15.5",
|
||||
"@mui/x-data-grid": "^6.18.7",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"ahooks": "^3.7.8",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.6.5",
|
||||
"dayjs": "1.11.5",
|
||||
"i18next": "^23.7.7",
|
||||
"i18next": "^23.7.16",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-router-dom": "^6.21.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.6.2",
|
||||
"recoil": "^0.7.7",
|
||||
@@ -48,15 +49,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.1.1",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"adm-zip": "^0.5.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -64,10 +65,10 @@
|
||||
"husky": "^7.0.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
|
||||
1239
pnpm-lock.yaml
generated
1239
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ const PLATFORM_MAP = {
|
||||
"i686-unknown-linux-gnu": "linux",
|
||||
"aarch64-unknown-linux-gnu": "linux",
|
||||
"armv7-unknown-linux-gnueabihf": "linux",
|
||||
"loongarch64-unknown-linux-gnu": "linux",
|
||||
};
|
||||
const ARCH_MAP = {
|
||||
"x86_64-pc-windows-msvc": "x64",
|
||||
@@ -32,6 +33,7 @@ const ARCH_MAP = {
|
||||
"i686-unknown-linux-gnu": "ia32",
|
||||
"aarch64-unknown-linux-gnu": "arm64",
|
||||
"armv7-unknown-linux-gnueabihf": "arm",
|
||||
"loongarch64-unknown-linux-gnu": "loong64",
|
||||
};
|
||||
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
@@ -63,6 +65,7 @@ const META_ALPHA_MAP = {
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest alpha release version from the version.txt file
|
||||
@@ -108,6 +111,7 @@ const META_MAP = {
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
|
||||
790
src-tauri/Cargo.lock
generated
790
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "1.4.8"
|
||||
version = "1.4.10"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0"
|
||||
@@ -39,7 +39,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
sysproxy = { git="https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
|
||||
tauri = { version = "1.5", features = [ "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
tauri = { version = "1.5", features = [ "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.0.0" # 高版本会返回错误 Status
|
||||
|
||||
@@ -261,6 +261,11 @@ pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
||||
@@ -261,7 +261,9 @@ impl PrfItem {
|
||||
},
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
None => Some(
|
||||
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
|
||||
),
|
||||
};
|
||||
let option = match update_interval {
|
||||
Some(val) => Some(PrfOption {
|
||||
|
||||
@@ -25,6 +25,11 @@ pub struct IVerge {
|
||||
/// copy env type
|
||||
pub env_type: Option<String>,
|
||||
|
||||
/// start page
|
||||
pub start_page: Option<String>,
|
||||
/// startup script path
|
||||
pub startup_script: Option<String>,
|
||||
|
||||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
@@ -88,6 +93,9 @@ pub struct IVerge {
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// 测试网站列表
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
|
||||
/// 日志清理
|
||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||
pub auto_log_clean: Option<i32>,
|
||||
@@ -103,6 +111,14 @@ pub struct IVerge {
|
||||
pub verge_mixed_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IVergeTestItem {
|
||||
pub uid: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IVergeTheme {
|
||||
pub primary_color: Option<String>,
|
||||
@@ -139,6 +155,7 @@ impl IVerge {
|
||||
env_type: Some("bash".into()),
|
||||
#[cfg(target_os = "windows")]
|
||||
env_type: Some("powershell".into()),
|
||||
start_page: Some("/".into()),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
@@ -178,6 +195,8 @@ impl IVerge {
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
patch!(env_type);
|
||||
patch!(start_page);
|
||||
patch!(startup_script);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
|
||||
@@ -202,6 +221,7 @@ impl IVerge {
|
||||
patch!(default_latency_test);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(enable_clash_fields);
|
||||
patch!(auto_log_clean);
|
||||
patch!(window_size_position);
|
||||
|
||||
@@ -123,6 +123,12 @@ impl Sysopt {
|
||||
sysproxy.enable = enable;
|
||||
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
sysproxy.port = port;
|
||||
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
sysproxy.set_system_proxy_with_registry()?;
|
||||
|
||||
@@ -225,6 +225,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let port = patch.verge_mixed_port;
|
||||
|
||||
match {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -249,7 +250,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
if auto_launch.is_some() {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
|
||||
sysopt::Sysopt::global().update_sysproxy()?;
|
||||
sysopt::Sysopt::global().guard_proxy();
|
||||
}
|
||||
@@ -368,3 +369,42 @@ pub fn copy_clash_env(app_handle: &AppHandle) {
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn test_delay(url: String) -> Result<u32> {
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await?;
|
||||
if response.status().is_success() {
|
||||
let delay = start.elapsed().as_millis() as u32;
|
||||
Ok(delay)
|
||||
} else {
|
||||
Ok(10000u32)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ fn main() -> std::io::Result<()> {
|
||||
// verge
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
cmds::test_delay,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
cmds::get_profiles,
|
||||
|
||||
@@ -80,6 +80,19 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// get the last part of the url, if not found, return empty string
|
||||
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
|
||||
let segments: Vec<&str> = path.split('/').collect();
|
||||
let last_segment = segments.last()?;
|
||||
|
||||
Some(
|
||||
percent_encoding::percent_decode_str(last_segment)
|
||||
.decode_utf8_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// open file
|
||||
/// use vscode by default
|
||||
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
|
||||
@@ -8,7 +8,9 @@ use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tauri::api::process::Command;
|
||||
|
||||
/// initialize this instance's log file
|
||||
fn init_log() -> Result<()> {
|
||||
@@ -340,3 +342,44 @@ pub fn init_scheme() -> Result<()> {
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn startup_script() -> Result<()> {
|
||||
let path = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
verge.startup_script.clone().unwrap_or("".to_string())
|
||||
};
|
||||
|
||||
if !path.is_empty() {
|
||||
let mut shell = "";
|
||||
if path.ends_with(".sh") {
|
||||
shell = "bash";
|
||||
}
|
||||
if path.ends_with(".ps1") {
|
||||
shell = "powershell";
|
||||
}
|
||||
if path.ends_with(".bat") {
|
||||
shell = "cmd";
|
||||
}
|
||||
if shell.is_empty() {
|
||||
return Err(anyhow::anyhow!("unsupported script: {path}"));
|
||||
}
|
||||
let current_dir = PathBuf::from(path.clone());
|
||||
if !current_dir.exists() {
|
||||
return Err(anyhow::anyhow!("script not found: {path}"));
|
||||
}
|
||||
let current_dir = current_dir.parent();
|
||||
match current_dir {
|
||||
Some(dir) => {
|
||||
let _ = Command::new(shell)
|
||||
.current_dir(dir.to_path_buf())
|
||||
.args(&[path])
|
||||
.output()?;
|
||||
}
|
||||
None => {
|
||||
let _ = Command::new(shell).args(&[path]).output()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub fn resolve_setup(app: &mut App) {
|
||||
#[cfg(target_os = "windows")]
|
||||
log_err!(init::init_service());
|
||||
log_err!(init::init_scheme());
|
||||
|
||||
log_err!(init::startup_script());
|
||||
// 处理随机端口
|
||||
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.4.8"
|
||||
"version": "1.4.10"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -54,11 +54,15 @@
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src http: https: data: 'self';"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
} from "@mui/x-data-grid";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { sortWithUnit, sortStringTime } from "@/utils/custom-comparator";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
@@ -25,7 +28,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: sortWithUnit,
|
||||
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||
parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
@@ -33,7 +37,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: sortWithUnit,
|
||||
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||
parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
@@ -41,7 +46,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: sortWithUnit,
|
||||
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
@@ -49,11 +55,12 @@ export const ConnectionTable = (props: Props) => {
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: sortWithUnit,
|
||||
valueFormatter: (params: GridValueFormatterParams<number>) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||
{ field: "process", headerName: "Process", flex: 240, minWidth: 120 },
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
@@ -61,7 +68,11 @@ export const ConnectionTable = (props: Props) => {
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: sortStringTime,
|
||||
sortComparator: (v1, v2) => {
|
||||
return new Date(v2).getTime() - new Date(v1).getTime();
|
||||
},
|
||||
valueFormatter: (params: GridValueFormatterParams<string>) =>
|
||||
dayjs(params.value).fromNow(),
|
||||
},
|
||||
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||
{
|
||||
@@ -83,14 +94,14 @@ export const ConnectionTable = (props: Props) => {
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: parseTraffic(each.download).join(" "),
|
||||
upload: parseTraffic(each.upload).join(" "),
|
||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
chains,
|
||||
rule,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: dayjs(each.start).fromNow(),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
|
||||
@@ -7,25 +7,32 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
styled,
|
||||
Box,
|
||||
alpha,
|
||||
Typography,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getProviders, providerUpdate } from "@/services/api";
|
||||
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getProviders", getProviders);
|
||||
const { data } = useSWR("getProxyProviders", getProxyProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await providerUpdate(key);
|
||||
await proxyProviderUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProviders");
|
||||
await mutate("getProxyProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
@@ -43,7 +50,23 @@ export const ProviderButton = () => {
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Proxy Provider")}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key, item]) => {
|
||||
await proxyProviderUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProxyProviders");
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
@@ -53,30 +76,81 @@ export const ProviderButton = () => {
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
const sub = item.subscriptionInfo;
|
||||
const hasSubInfo = !!sub;
|
||||
const upload = sub?.Upload || 0;
|
||||
const download = sub?.Download || 0;
|
||||
const total = sub?.Total || 0;
|
||||
const expire = sub?.Expire || 0;
|
||||
const progress = Math.round(
|
||||
((download + upload) * 100) / (total + 0.1)
|
||||
);
|
||||
return (
|
||||
<ListItem sx={{ p: 0 }} key={key}>
|
||||
<ListItemText
|
||||
primary={key}
|
||||
secondary={
|
||||
<>
|
||||
<span style={{ marginRight: "4em" }}>
|
||||
Type: {item.vehicleType}
|
||||
</span>
|
||||
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
|
||||
Updated: {time.fromNow()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
<>
|
||||
<ListItem
|
||||
sx={(theme) => ({
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
boxShadow: theme.shadows[2],
|
||||
mb: 1,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||
<span title="Used / Total">
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title="Expire Time">
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
color="inherit"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
@@ -84,3 +158,27 @@ export const ProviderButton = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
color: alpha(theme.palette.primary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const boxStyle = {
|
||||
height: 26,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
};
|
||||
|
||||
function parseExpire(expire?: number) {
|
||||
if (!expire) return "-";
|
||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
@@ -37,6 +37,18 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
dense
|
||||
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
||||
>
|
||||
{group.icon && group.icon.trim().startsWith("http") && (
|
||||
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
{group.icon && group.icon.trim().startsWith("data") && (
|
||||
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
{group.icon && group.icon.trim().startsWith("<svg") && (
|
||||
<img
|
||||
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
|
||||
height="40px"
|
||||
/>
|
||||
)}
|
||||
<ListItemText
|
||||
primary={group.name}
|
||||
secondary={
|
||||
|
||||
159
src/components/rule/provider-button.tsx
Normal file
159
src/components/rule/provider-button.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import dayjs from "dayjs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
styled,
|
||||
Box,
|
||||
alpha,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getRuleProviders", getRuleProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await ruleProviderUpdate(key);
|
||||
await mutate("getRules");
|
||||
await mutate("getRuleProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Rule Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key, item]) => {
|
||||
await ruleProviderUpdate(key);
|
||||
await mutate("getRules");
|
||||
await mutate("getRuleProviders");
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={(theme) => ({
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
boxShadow: theme.shadows[2],
|
||||
mb: 1,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
||||
{item.ruleCount}
|
||||
</TypeBox>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{item.behavior}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
color: alpha(theme.palette.secondary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
color: alpha(theme.palette.primary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
@@ -1,10 +1,19 @@
|
||||
import { useRef } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton, MenuItem, Select, Typography } from "@mui/material";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
Input,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { exit } from "@tauri-apps/api/process";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { version } from "@root/package.json";
|
||||
import { DialogRef, Notice } from "@/components/base";
|
||||
@@ -18,8 +27,7 @@ import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { portableFlag } from "@/pages/_layout";
|
||||
|
||||
import { routers } from "@/pages/_routers";
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
}
|
||||
@@ -30,7 +38,14 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
const { theme_mode, language, tray_event, env_type } = verge ?? {};
|
||||
const {
|
||||
theme_mode,
|
||||
language,
|
||||
tray_event,
|
||||
env_type,
|
||||
startup_script,
|
||||
start_page,
|
||||
} = verge ?? {};
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
const hotkeyRef = useRef<DialogRef>(null);
|
||||
const miscRef = useRef<DialogRef>(null);
|
||||
@@ -126,6 +141,70 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Start Page")}>
|
||||
<GuardState
|
||||
value={start_page ?? "/"}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ start_page: e })}
|
||||
onGuard={(e) => patchVerge({ start_page: e })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||
{routers.map((page: { label: string; link: string }) => {
|
||||
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
|
||||
})}
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Startup Script")}>
|
||||
<GuardState
|
||||
value={startup_script ?? ""}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ startup_script: e })}
|
||||
onGuard={(e) => patchVerge({ startup_script: e })}
|
||||
>
|
||||
<Input
|
||||
value={startup_script}
|
||||
disabled
|
||||
endAdornment={
|
||||
<>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const path = await open({
|
||||
directory: false,
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Shell Script",
|
||||
extensions: ["sh", "bat", "ps1"],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (path?.length) {
|
||||
onChangeData({ startup_script: `${path}` });
|
||||
patchVerge({ startup_script: `${path}` });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Browse")}
|
||||
</Button>
|
||||
{startup_script && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
onChangeData({ startup_script: "" });
|
||||
patchVerge({ startup_script: "" });
|
||||
}}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
></Input>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
<SettingItem label={t("Theme Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@@ -214,18 +293,29 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{!portableFlag && (
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Exit")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => {
|
||||
exit(0);
|
||||
}}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Verge Version")}>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||
|
||||
42
src/components/test/test-box.tsx
Normal file
42
src/components/test/test-box.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { alpha, Box, styled } from "@mui/material";
|
||||
|
||||
export const TestBox = styled(Box)(({ theme, "aria-selected": selected }) => {
|
||||
const { mode, primary, text, grey, background } = theme.palette;
|
||||
const key = `${mode}-${!!selected}`;
|
||||
|
||||
const backgroundColor = {
|
||||
"light-true": alpha(primary.main, 0.2),
|
||||
"light-false": alpha(background.paper, 0.75),
|
||||
"dark-true": alpha(primary.main, 0.45),
|
||||
"dark-false": alpha(grey[700], 0.45),
|
||||
}[key]!;
|
||||
|
||||
const color = {
|
||||
"light-true": text.secondary,
|
||||
"light-false": text.secondary,
|
||||
"dark-true": alpha(text.secondary, 0.85),
|
||||
"dark-false": alpha(text.secondary, 0.65),
|
||||
}[key]!;
|
||||
|
||||
const h2color = {
|
||||
"light-true": primary.main,
|
||||
"light-false": text.primary,
|
||||
"dark-true": primary.light,
|
||||
"dark-false": text.primary,
|
||||
}[key]!;
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shadows[2],
|
||||
padding: "8px 16px",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor,
|
||||
color,
|
||||
"& h2": { color: h2color },
|
||||
};
|
||||
});
|
||||
217
src/components/test/test-item.tsx
Normal file
217
src/components/test/test-item.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Divider,
|
||||
MenuItem,
|
||||
Menu,
|
||||
styled,
|
||||
alpha,
|
||||
} from "@mui/material";
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import { LanguageTwoTone } from "@mui/icons-material";
|
||||
import { Notice } from "@/components/base";
|
||||
import { TestBox } from "./test-box";
|
||||
import delayManager from "@/services/delay";
|
||||
import { cmdTestDelay } from "@/services/cmds";
|
||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
itemData: IVergeTestItem;
|
||||
onEdit: () => void;
|
||||
onDelete: (uid: string) => void;
|
||||
}
|
||||
|
||||
let eventListener: UnlistenFn | null = null;
|
||||
|
||||
export const TestItem = (props: Props) => {
|
||||
const { itemData, onEdit, onDelete: onDeleteItem } = props;
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: props.id });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
const [delay, setDelay] = useState(-1);
|
||||
const { uid, name, icon, url } = itemData;
|
||||
|
||||
const onDelay = async () => {
|
||||
setDelay(-2);
|
||||
const result = await cmdTestDelay(url);
|
||||
setDelay(result);
|
||||
};
|
||||
|
||||
const onEditTest = () => {
|
||||
setAnchorEl(null);
|
||||
onEdit();
|
||||
};
|
||||
|
||||
const onDelete = useLockFn(async () => {
|
||||
setAnchorEl(null);
|
||||
try {
|
||||
onDeleteItem(uid);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const menu = [
|
||||
{ label: "Edit", handler: onEditTest },
|
||||
{ label: "Delete", handler: onDelete },
|
||||
];
|
||||
|
||||
const listenTsetEvent = async () => {
|
||||
if (eventListener !== null) {
|
||||
eventListener();
|
||||
}
|
||||
eventListener = await listen("verge://test-all", () => {
|
||||
onDelay();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onDelay();
|
||||
listenTsetEvent();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
>
|
||||
<TestBox
|
||||
onClick={onEditTest}
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
sx={{ cursor: "move" }}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
{icon && icon.trim() !== "" ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
{icon.trim().startsWith("http") && (
|
||||
<img src={icon} height="40px" style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
{icon.trim().startsWith("data") && (
|
||||
<img src={icon} height="40px" style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
{icon.trim().startsWith("<svg") && (
|
||||
<img
|
||||
src={`data:image/svg+xml;base64,${btoa(icon)}`}
|
||||
height="40px"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<LanguageTwoTone sx={{ height: "40px" }} fontSize="large" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<Typography variant="h6" component="h2" noWrap title={name}>
|
||||
{name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider sx={{ marginTop: "8px" }} />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "8px",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{delay === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay === -1 && (
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay >= 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) => ({
|
||||
":hover": {
|
||||
bgcolor: alpha(palette.primary.main, 0.15),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
</Box>
|
||||
</TestBox>
|
||||
|
||||
<Menu
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorPosition={position}
|
||||
anchorReference="anchorPosition"
|
||||
transitionDuration={225}
|
||||
MenuListProps={{ sx: { py: 0.5 } }}
|
||||
onContextMenu={(e) => {
|
||||
setAnchorEl(null);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
onClick={item.handler}
|
||||
sx={{ minWidth: 120 }}
|
||||
dense
|
||||
>
|
||||
{t(item.label)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
padding: "3px 6px",
|
||||
fontSize: 14,
|
||||
fontFamily: typography.fontFamily,
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
154
src/components/test/test-viewer.tsx
Normal file
154
src/components/test/test-viewer.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { TextField } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, Notice } from "@/components/base";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface Props {
|
||||
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void;
|
||||
}
|
||||
|
||||
export interface TestViewerRef {
|
||||
create: () => void;
|
||||
edit: (item: IVergeTestItem) => void;
|
||||
}
|
||||
|
||||
// create or edit the test item
|
||||
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const testList = verge?.test_list ?? [];
|
||||
const { control, watch, register, ...formIns } = useForm<IVergeTestItem>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
icon: "",
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
});
|
||||
await patchVerge({ ...verge, test_list: newList });
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!form.name) throw new Error("`Name` should not be null");
|
||||
if (!form.url) throw new Error("`Url` should not be null");
|
||||
let newList;
|
||||
let uid;
|
||||
|
||||
if (openType === "new") {
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
newList = [...testList, item];
|
||||
await patchVerge({ test_list: newList });
|
||||
props.onChange(uid);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
uid = form.uid;
|
||||
|
||||
await patchTestList(uid, form);
|
||||
props.onChange(uid, form);
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
};
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Test") : t("Edit Test")}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={5}
|
||||
label={t("Icon")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={3}
|
||||
label={t("Test URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"Label-Proxies": "Proxies",
|
||||
"Label-Test": "Test",
|
||||
"Label-Profiles": "Profiles",
|
||||
"Label-Connections": "Connections",
|
||||
"Label-Logs": "Logs",
|
||||
@@ -11,11 +12,17 @@
|
||||
"Clear": "Clear",
|
||||
"Proxies": "Proxies",
|
||||
"Proxy Groups": "Proxy Groups",
|
||||
"Test": "Test",
|
||||
"rule": "rule",
|
||||
"global": "global",
|
||||
"direct": "direct",
|
||||
"script": "script",
|
||||
|
||||
"Edit": "Edit",
|
||||
"Icon": "Icon",
|
||||
"Test URL": "Test URL",
|
||||
"Test All": "Test All",
|
||||
|
||||
"Profiles": "Profiles",
|
||||
"Profile URL": "Profile URL",
|
||||
"Import": "Import",
|
||||
@@ -50,6 +57,8 @@
|
||||
"Filter conditions": "Filter conditions",
|
||||
"Refresh profiles": "Refresh profiles",
|
||||
"Rules": "Rules",
|
||||
"Update All": "Update All",
|
||||
"Update At": "Update At",
|
||||
|
||||
"Type": "Type",
|
||||
"Name": "Name",
|
||||
@@ -92,6 +101,9 @@
|
||||
"Theme Mode": "Theme Mode",
|
||||
"Tray Click Event": "Tray Click Event",
|
||||
"Copy Env Type": "Copy Env Type",
|
||||
"Start Page": "Start Page",
|
||||
"Startup Script": "Startup Script",
|
||||
"Browse": "Browse",
|
||||
"Show Main Window": "Show Main Window",
|
||||
"Theme Setting": "Theme Setting",
|
||||
"Layout Setting": "Layout Setting",
|
||||
@@ -117,6 +129,7 @@
|
||||
"Back": "Back",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"Exit": "Exit",
|
||||
|
||||
"Default": "Default",
|
||||
"Download Speed": "Download Speed",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"Label-Proxies": "Прокси",
|
||||
"Label-Test": "Тест",
|
||||
"Label-Profiles": "Профили",
|
||||
"Label-Connections": "Соединения",
|
||||
"Label-Logs": "Логи",
|
||||
@@ -10,12 +11,18 @@
|
||||
"Logs": "Логи",
|
||||
"Clear": "Очистить",
|
||||
"Proxies": "Прокси",
|
||||
"Test": "Тест",
|
||||
"Proxy Groups": "Группы прокси",
|
||||
"rule": "правила",
|
||||
"global": "глобальный",
|
||||
"direct": "прямой",
|
||||
"script": "скриптовый",
|
||||
|
||||
"Edit": "Редактировать",
|
||||
"Icon": "Икона",
|
||||
"Test URL": "Тестовый URL",
|
||||
"Test All": "Тест Все",
|
||||
|
||||
"Profiles": "Профили",
|
||||
"Profile URL": "URL профиля",
|
||||
"Import": "Импорт",
|
||||
@@ -49,6 +56,9 @@
|
||||
"Filter": "Фильтр",
|
||||
"Filter conditions": "Условия фильтрации",
|
||||
"Refresh profiles": "Обновить профили",
|
||||
"Rules": "Правила",
|
||||
"Update All": "Обновить все",
|
||||
"Update At": "Обновлено в",
|
||||
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
@@ -82,7 +92,10 @@
|
||||
"Current System Proxy": "Текущий системный прокси",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Tray Click Event": "Событие щелчка в лотке",
|
||||
"Start Page": "Главная страница",
|
||||
"Copy Env Type": "Скопировать тип Env",
|
||||
"Startup Script": "Скрипт запуска",
|
||||
"Browse": "Просмотреть",
|
||||
"Show Main Window": "Показать главное окно",
|
||||
"Theme Setting": "Настройка темы",
|
||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||
@@ -104,6 +117,7 @@
|
||||
"Back": "Назад",
|
||||
"Save": "Сохранить",
|
||||
"Cancel": "Отмена",
|
||||
"Exit": "Выход",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"Label-Proxies": "代 理",
|
||||
"Label-Test": "测 试",
|
||||
"Label-Profiles": "订 阅",
|
||||
"Label-Connections": "连 接",
|
||||
"Label-Logs": "日 志",
|
||||
@@ -11,11 +12,17 @@
|
||||
"Clear": "清除",
|
||||
"Proxies": "代理",
|
||||
"Proxy Groups": "代理组",
|
||||
"Test": "测试",
|
||||
"rule": "规则",
|
||||
"global": "全局",
|
||||
"direct": "直连",
|
||||
"script": "脚本",
|
||||
|
||||
"Edit": "编辑",
|
||||
"Icon": "图标",
|
||||
"Test URL": "测试地址",
|
||||
"Test All": "测试全部",
|
||||
|
||||
"Profiles": "订阅",
|
||||
"Profile URL": "订阅文件链接",
|
||||
"Import": "导入",
|
||||
@@ -50,6 +57,8 @@
|
||||
"Filter conditions": "过滤条件",
|
||||
"Refresh profiles": "刷新订阅",
|
||||
"Rules": "规则",
|
||||
"Update All": "更新全部",
|
||||
"Update At": "更新于",
|
||||
|
||||
"Type": "类型",
|
||||
"Name": "名称",
|
||||
@@ -92,6 +101,9 @@
|
||||
"Theme Mode": "主题模式",
|
||||
"Tray Click Event": "托盘点击事件",
|
||||
"Copy Env Type": "复制环境变量类型",
|
||||
"Start Page": "启动页面",
|
||||
"Startup Script": "启动脚本",
|
||||
"Browse": "浏览",
|
||||
"Show Main Window": "显示主窗口",
|
||||
"Theme Setting": "主题设置",
|
||||
"Layout Setting": "界面设置",
|
||||
@@ -117,6 +129,7 @@
|
||||
"Back": "返回",
|
||||
"Save": "保存",
|
||||
"Cancel": "取消",
|
||||
"Exit": "退出",
|
||||
|
||||
"Default": "默认",
|
||||
"Download Speed": "下载速度",
|
||||
|
||||
@@ -23,7 +23,7 @@ import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { getPortableFlag } from "@/services/cmds";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
export let portableFlag = false;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -36,8 +36,8 @@ const Layout = () => {
|
||||
const { theme } = useCustomTheme();
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { language } = verge || {};
|
||||
|
||||
const { language, start_page } = verge || {};
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,7 +54,7 @@ const Layout = () => {
|
||||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProviders");
|
||||
mutate("getProxyProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
@@ -88,7 +88,10 @@ const Layout = () => {
|
||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||
i18next.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
if (start_page) {
|
||||
navigate(start_page);
|
||||
}
|
||||
}, [language, start_page]);
|
||||
|
||||
return (
|
||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import LogsPage from "./logs";
|
||||
import ProxiesPage from "./proxies";
|
||||
import TestPage from "./test";
|
||||
import ProfilesPage from "./profiles";
|
||||
import SettingsPage from "./settings";
|
||||
import ConnectionsPage from "./connections";
|
||||
@@ -11,6 +12,11 @@ export const routers = [
|
||||
link: "/",
|
||||
ele: ProxiesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Test",
|
||||
link: "/test",
|
||||
ele: TestPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Profiles",
|
||||
link: "/profile",
|
||||
|
||||
@@ -2,10 +2,11 @@ import useSWR from "swr";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Box, Paper, TextField } from "@mui/material";
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import { getRules } from "@/services/api";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import RuleItem from "@/components/rule/rule-item";
|
||||
import { ProviderButton } from "@/components/rule/provider-button";
|
||||
|
||||
const RulesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,7 +19,16 @@ const RulesPage = () => {
|
||||
}, [data, filterText]);
|
||||
|
||||
return (
|
||||
<BasePage full title={t("Rules")} contentStyle={{ height: "100%" }}>
|
||||
<BasePage
|
||||
full
|
||||
title={t("Rules")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
|
||||
165
src/pages/test.tsx
Normal file
165
src/pages/test.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Box, Button, Grid } from "@mui/material";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from "@dnd-kit/sortable";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
|
||||
import { TestItem } from "@/components/test/test-item";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const TestPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||
|
||||
// test list
|
||||
const testList = verge?.test_list ?? [
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: "https://www.apple.com/favicon.ico",
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: `<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#000000"/></svg>`,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: `<svg enable-background="new 0 0 48 48" height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-11.045 0-20 8.955-20 20s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z" fill="#ffc107"/><path d="m6.306 14.691 6.571 4.819c1.778-4.402 6.084-7.51 11.123-7.51 3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-7.682 0-14.344 4.337-17.694 10.691z" fill="#ff3d00"/><path d="m24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238c-2.008 1.521-4.504 2.43-7.219 2.43-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025c3.31 6.477 10.032 10.921 17.805 10.921z" fill="#4caf50"/><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-.792 2.237-2.231 4.166-4.087 5.571.001-.001.002-.001.003-.002l6.19 5.238c-.438.398 6.591-4.807 6.591-14.807 0-1.341-.138-2.65-.389-3.917z" fill="#1976d2"/></svg>`,
|
||||
},
|
||||
];
|
||||
|
||||
const onTestListItemChange = (
|
||||
uid: string,
|
||||
patch?: Partial<IVergeTestItem>
|
||||
) => {
|
||||
if (patch) {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
});
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
} else {
|
||||
mutateVerge();
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteTestListItem = (uid: string) => {
|
||||
const newList = testList.filter((x) => x.uid !== uid);
|
||||
patchVerge({ test_list: newList });
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
};
|
||||
|
||||
const reorder = (list: any[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
return result;
|
||||
};
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let old_index = testList.findIndex((x) => x.uid === active.id);
|
||||
let new_index = testList.findIndex((x) => x.uid === over.id);
|
||||
if (old_index < 0 || new_index < 0) {
|
||||
return;
|
||||
}
|
||||
let newList = reorder(testList, old_index, new_index);
|
||||
await mutateVerge({ ...verge, test_list: newList }, false);
|
||||
await patchVerge({ test_list: newList });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!verge) return;
|
||||
if (!verge?.test_list) {
|
||||
patchVerge({ test_list: testList });
|
||||
}
|
||||
}, [verge]);
|
||||
|
||||
const viewerRef = useRef<TestViewerRef>(null);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Test")}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => emit("verge://test-all")}
|
||||
>
|
||||
{t("Test All")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => viewerRef.current?.create()}
|
||||
>
|
||||
{t("New")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Box sx={{ mb: 4.5 }}>
|
||||
<Grid container spacing={{ xs: 1, lg: 1 }}>
|
||||
<SortableContext
|
||||
items={testList.map((x) => {
|
||||
return x.uid;
|
||||
})}
|
||||
>
|
||||
{testList.map((item) => (
|
||||
<Grid item xs={6} sm={4} md={3} lg={2} key={item.uid}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
</Box>
|
||||
</DndContext>
|
||||
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestPage;
|
||||
@@ -105,7 +105,7 @@ export const getProxiesInner = async () => {
|
||||
export const getProxies = async () => {
|
||||
const [proxyRecord, providerRecord] = await Promise.all([
|
||||
getProxiesInner(),
|
||||
getProviders(),
|
||||
getProxyProviders(),
|
||||
]);
|
||||
|
||||
// provider name map
|
||||
@@ -166,11 +166,31 @@ export const getProxies = async () => {
|
||||
};
|
||||
|
||||
// get proxy providers
|
||||
export const getProviders = async () => {
|
||||
export const getProxyProviders = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
|
||||
const providers = (response.providers || {}) as Record<string, IProviderItem>;
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
IProxyProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuleProviders = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/rules");
|
||||
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
IRuleProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
@@ -188,11 +208,16 @@ export const providerHealthCheck = async (name: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const providerUpdate = async (name: string) => {
|
||||
export const proxyProviderUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const ruleProviderUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/rules/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const getConnections = async () => {
|
||||
const instance = await getAxios();
|
||||
const result = await instance.get("/connections");
|
||||
|
||||
@@ -165,6 +165,10 @@ export async function cmdGetProxyDelay(name: string, url?: string) {
|
||||
return invoke<{ delay: number }>("clash_api_get_proxy_delay", { name, url });
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>("test_delay", { url });
|
||||
}
|
||||
|
||||
/// service mode
|
||||
|
||||
export async function checkService() {
|
||||
|
||||
@@ -109,17 +109,16 @@ class DelayManager {
|
||||
}
|
||||
|
||||
formatDelay(delay: number) {
|
||||
if (delay < 0) return "-";
|
||||
if (delay <= 0) return "Error";
|
||||
if (delay > 1e5) return "Error";
|
||||
if (delay >= 10000) return "Timeout"; // 10s
|
||||
return `${delay}`;
|
||||
return `${delay} ms`;
|
||||
}
|
||||
|
||||
formatDelayColor(delay: number) {
|
||||
if (delay >= 10000) return "error.main";
|
||||
/*if (delay <= 0) return "text.secondary";
|
||||
if (delay <= 0) return "error.main";
|
||||
if (delay > 500) return "warning.main";
|
||||
if (delay > 100) return "text.secondary";*/
|
||||
return "success.main";
|
||||
}
|
||||
}
|
||||
|
||||
29
src/services/types.d.ts
vendored
29
src/services/types.d.ts
vendored
@@ -53,6 +53,7 @@ interface IProxyItem {
|
||||
all?: string[];
|
||||
now?: string;
|
||||
hidden?: boolean;
|
||||
icon?: string;
|
||||
provider?: string; // 记录是否来自provider
|
||||
}
|
||||
|
||||
@@ -60,12 +61,28 @@ type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
||||
all: IProxyItem[];
|
||||
};
|
||||
|
||||
interface IProviderItem {
|
||||
interface IProxyProviderItem {
|
||||
name: string;
|
||||
type: string;
|
||||
proxies: IProxyItem[];
|
||||
updatedAt: string;
|
||||
vehicleType: string;
|
||||
subscriptionInfo?: {
|
||||
Upload: number;
|
||||
Download: number;
|
||||
Total: number;
|
||||
Expire: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IRuleProviderItem {
|
||||
name: string;
|
||||
behavior: string;
|
||||
format: string;
|
||||
ruleCount: number;
|
||||
type: string;
|
||||
updatedAt: string;
|
||||
vehicleType: string;
|
||||
}
|
||||
|
||||
interface ITrafficItem {
|
||||
@@ -154,11 +171,20 @@ interface IProfilesConfig {
|
||||
items?: IProfileItem[];
|
||||
}
|
||||
|
||||
interface IVergeTestItem {
|
||||
uid: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface IVergeConfig {
|
||||
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
|
||||
language?: string;
|
||||
tray_event?: "main_window" | "system_proxy" | "tun_mode" | string;
|
||||
env_type?: "bash" | "cmd" | "powershell" | string;
|
||||
startup_script?: string;
|
||||
start_page?: string;
|
||||
clash_core?: string;
|
||||
theme_mode?: "light" | "dark" | "system";
|
||||
traffic_graph?: boolean;
|
||||
@@ -194,6 +220,7 @@ interface IVergeConfig {
|
||||
enable_builtin_enhanced?: boolean;
|
||||
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||
proxy_layout_column?: number;
|
||||
test_list?: IVergeTestItem[];
|
||||
}
|
||||
|
||||
type IClashConfigValue = any;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { GridComparatorFn } from "@mui/x-data-grid";
|
||||
|
||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const unitMap = new Map<string, number>();
|
||||
unitMap.set("分钟前", 60);
|
||||
unitMap.set("小时前", 60 * 60);
|
||||
unitMap.set("天前", 60 * 60 * 24);
|
||||
unitMap.set("个月前", 60 * 60 * 24 * 30);
|
||||
unitMap.set("年前", 60 * 60 * 24 * 30 * 12);
|
||||
|
||||
export const sortWithUnit: GridComparatorFn<string> = (v1, v2) => {
|
||||
const [ret1, unit1] = v1.split(" ");
|
||||
const [ret2, unit2] = v2.split(" ");
|
||||
let value1 =
|
||||
parseFloat(ret1) *
|
||||
Math.pow(1024, UNITS.indexOf(unit1.replace("/s", "").trim()));
|
||||
let value2 =
|
||||
parseFloat(ret2) *
|
||||
Math.pow(1024, UNITS.indexOf(unit2.replace("/s", "").trim()));
|
||||
return value1 - value2;
|
||||
};
|
||||
|
||||
export const sortStringTime: GridComparatorFn<string> = (v1, v2) => {
|
||||
if (v1 === "几秒前") {
|
||||
return -1;
|
||||
}
|
||||
if (v2 === "几秒前") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const matches1 = v1.match(/[0-9]+/);
|
||||
const num1 = matches1 !== null ? parseInt(matches1[0]) : 0;
|
||||
const matches2 = v2.match(/[0-9]+/);
|
||||
const num2 = matches2 !== null ? parseInt(matches2[0]) : 0;
|
||||
const unit1 = unitMap.get(v1.replace(num1.toString(), "").trim()) || 0;
|
||||
const unit2 = unitMap.get(v2.replace(num2.toString(), "").trim()) || 0;
|
||||
return num1 * unit1 - num2 * unit2;
|
||||
};
|
||||
Reference in New Issue
Block a user