Compare commits

...
50 Commits
Author SHA1 Message Date
wonfen 7d2b18759e Release 1.7.7 2024-08-22 03:47:33 +08:00
wonfen aa8f6f4ad6 chore: Shorten text to prevent Windows tooltip truncation. 2024-08-22 03:41:16 +08:00
wonfen 592167ffb7 fix: profiles will not be selected after import 2024-08-22 00:57:04 +08:00
wonfen 12a6bfad00 Release 1.7.6 2024-08-18 00:49:23 +08:00
tony-sung 1ea3b956ad Update web-ui-viewer.tsx Fix yacd parameter hostname error (#1484) 2024-08-18 00:23:24 +08:00
MystiPanda 1b7dafe743 feat: auto select profile 2024-07-30 09:06:12 +08:00
MystiPanda 624eb2a2ba chore: prepend new added rules 2024-07-24 14:25:06 +08:00
MystiPanda 88d98517c8 fix: wrap the password in quotes
#1460
2024-07-24 14:22:23 +08:00
Avan 9528606906 style: AddressDisplay padding (#1457) 2024-07-23 22:17:51 +08:00
nhsmw 09b44a3af3 fix: update test url 2024-07-19 10:32:41 +08:00
MystiPanda dc861eca7d chore: remove unnecessary console log 2024-07-18 23:37:54 +08:00
MystiPanda e749fe70e2 fix: use group testUrl
#1384
2024-07-18 15:04:55 +08:00
MystiPanda d20c082deb fix: windows x86 updater url 2024-07-18 14:38:31 +08:00
MystiPanda c4abc3ade3 fix: error command status 2024-07-16 18:37:56 +08:00
MystiPanda ff28aab56e fix: MacOS service install error 2024-07-16 13:01:27 +08:00
MystiPanda 1a6f842492 fix(#1335): support cidr 2024-07-16 12:22:11 +08:00
MystiPanda af0cd4a342 fix: try to fix install service 2024-07-16 12:07:23 +08:00
MystiPanda d98b3224cf chore: disable okBtn 2024-07-15 23:38:49 +08:00
MystiPanda f2e5f0754c ci: fix winget uploader 2024-07-15 23:27:38 +08:00
MystiPanda ec1601f317 Release 1.7.5 2024-07-15 22:35:33 +08:00
MystiPanda 54116608dc ci: fix winget uploader 2024-07-15 22:33:57 +08:00
MystiPanda fea6735ea1 fix: copy necessary files 2024-07-15 22:25:04 +08:00
MystiPanda 2f77a07b3d Release 1.7.4 2024-07-15 20:56:18 +08:00
MystiPanda 4cd7ccda57 fix: unified switch style 2024-07-15 20:42:00 +08:00
MystiPanda ef337ffb69 feat: optimize the service mode interaction logic. 2024-07-15 20:02:05 +08:00
MystiPanda 9ff5d8d4e0 Revert "fix: try to fix abnormal stuck"
This reverts commit e14ddbe23c.
2024-07-14 12:49:06 +08:00
Sukka 199a5eed60 fix(#1387): avoid catastrophic backtracking (#1396) 2024-07-14 12:26:02 +08:00
MystiPanda e14ddbe23c fix: try to fix abnormal stuck
#1387
2024-07-13 21:41:04 +08:00
MystiPanda 3878e66ea4 chore: update locale 2024-07-13 19:15:07 +08:00
MystiPanda c14a33444b chore: unified icon style 2024-07-13 19:01:16 +08:00
MystiPanda 48f7c15035 feat: display network interface 2024-07-13 14:15:13 +08:00
Avan 32212a46e2 feature: copy clash env (#1391)
* feat: copy clash env

* style: use ContentCopyRounded replace CopyAll
2024-07-13 01:03:46 +08:00
Avan b4025c45da style: EditorViewer title align center (#1390) 2024-07-12 23:29:55 +08:00
MystiPanda bd589c4422 fix: service install failed on macos 2024-07-12 20:54:31 +08:00
dongchengjie f85f7758e6 chore: checkbox items' title & button icons 2024-07-12 18:24:19 +08:00
dongchengjie 49e36b6e00 chore: editor resizing debounce 2024-07-08 23:18:06 +08:00
dongchengjie df6e245e5c chore: remove dialog overflow-scroll 2024-07-08 22:07:43 +08:00
dongchengjie 4e4f9af5b7 fix: editor fails to resize after toggling on macos 2024-07-08 21:40:56 +08:00
MystiPanda aa9e63b51a fix dev.yml 2024-07-08 16:17:23 +08:00
MystiPanda 82340582b2 ci: add dev workflow 2024-07-08 13:20:36 +08:00
Akioe Yu 9d045e24db fix: dnd box z-index (#1353)
* fix: dnd box z-index

* fix: dnd boxes
2024-07-08 10:49:57 +08:00
MystiPanda 1dcc95d16a perf: optimized response speed 2024-07-08 00:29:49 +08:00
dongchengjie 78c32d51db chore: group types locale 2024-07-07 21:56:20 +08:00
❤是纱雾酱哟~ 545a2c6688 ci: Integrate "Winget Releaser" (#1326) 2024-07-07 20:42:17 +08:00
MystiPanda 359d9285fe perf: change port too slow 2024-07-07 18:22:02 +08:00
MystiPanda 1bd51be99c feat: get network interface 2024-07-07 18:13:10 +08:00
dongchengjie ee56080af0 fix: usage percent style 2024-07-07 18:08:02 +08:00
MystiPanda 18196c4a77 fix: trojan uri parser 2024-07-07 11:47:49 +08:00
MystiPanda 14cd3b99cc fix: tray icon size
#1341
2024-07-07 11:18:22 +08:00
MystiPanda ff75fe47ee chore: disable browser autocomplete 2024-07-07 11:16:59 +08:00
71 changed files with 1533 additions and 925 deletions
+86
View File
@@ -0,0 +1,86 @@
name: Development Test
on:
workflow_dispatch:
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
dev:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
bundle: nsis
- os: macos-latest
target: aarch64-apple-darwin
bundle: dmg
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
if-no-files-found: error
- name: Upload Artifacts
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
+22
View File
@@ -256,3 +256,25 @@ jobs:
run: pnpm updater-fixed-webview2 run: pnpm updater-fixed-webview2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
runs-on: ubuntu-latest
needs: [release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Submit to Winget
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: ClashVergeRev.ClashVergeRev
version: ${{env.VERSION}}
release-tag: v${{env.VERSION}}
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
+1
View File
@@ -8,3 +8,4 @@ update.json
scripts/_env.sh scripts/_env.sh
.vscode .vscode
.tool-versions .tool-versions
.idea
+58
View File
@@ -1,3 +1,61 @@
## v1.7.7
### Bugs Fixes
- 修复导入订阅没有自动重载(不显示节点)的问题
- 英语状态下修复 Windows 工具栏提示文本超过限制的问题
---
## v1.7.6
### Notice
- Clash Verge Rev 目前已进入稳定周期,日后更新将着重于 bug 修复与内核常规升级
### Features
- Meta(mihomo)内核升级 1.18.7
- 界面细节调整
- 优化服务模式安装逻辑
- 移除无用的 console log
- 能自动选择第一个订阅
### Bugs Fixes
- 修复服务模式安装问题
- 修复 Mac 下的代理绕过 CIDR 写法过滤
- 修复 32 位升级 URL
- 修复不同分组 URL 测试地址配置无效的问题
- 修复 Web UI 下的一处 hostname 参数
---
## v1.7.5
### Features
- 展示局域网 IP 地址信息
- 在设置页面直接复制环境变量
- 优化服务模式安装逻辑
### Performance
- 优化切换订阅速度
- 优化更改端口速度
### Bugs Fixes
- 调整 MacOS 托盘图标大小
- Trojan URI 解析错误
- 卡片拖动显示层级错误
- 代理绕过格式检查错误
- MacOS 下编辑器最大化失败
- MacOS 服务安装失败
- 更改窗口大小导致闪退的问题
---
## v1.7.3 ## v1.7.3
### Features ### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "1.7.3", "version": "1.7.7",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "tauri dev", "dev": "tauri dev",
+2 -2
View File
@@ -72,12 +72,12 @@ async function resolveUpdater() {
} }
// win32 url // win32 url
if (name.endsWith("x64-setup.nsis.zip")) { if (name.endsWith("x86-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url; updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url; updateData.platforms["windows-i686"].url = browser_download_url;
} }
// win32 signature // win32 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) { if (name.endsWith("x86-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig; updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig; updateData.platforms["windows-i686"].signature = sig;
+15 -1
View File
@@ -790,7 +790,7 @@ dependencies = [
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "1.7.3" version = "1.7.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"auto-launch", "auto-launch",
@@ -803,6 +803,7 @@ dependencies = [
"log 0.4.22", "log 0.4.22",
"log4rs", "log4rs",
"nanoid", "nanoid",
"network-interface",
"once_cell", "once_cell",
"open 5.2.0", "open 5.2.0",
"parking_lot", "parking_lot",
@@ -3243,6 +3244,19 @@ dependencies = [
"jni-sys", "jni-sys",
] ]
[[package]]
name = "network-interface"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5"
dependencies = [
"cc",
"libc",
"serde",
"thiserror",
"winapi",
]
[[package]] [[package]]
name = "new_debug_unreachable" name = "new_debug_unreachable"
version = "1.0.6" version = "1.0.6"
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "1.7.3" version = "1.7.7"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -38,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" } sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version="1", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] } tauri = { version="1", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
network-interface = { version = "2.0.0", features = ["serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+41 -4
View File
@@ -6,12 +6,19 @@ use crate::{
}; };
use crate::{ret_err, wrap_err}; use crate::{ret_err, wrap_err};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use network_interface::NetworkInterface;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use sysproxy::{Autoproxy, Sysproxy}; use sysproxy::{Autoproxy, Sysproxy};
use tauri::{api, Manager}; use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>; type CmdResult<T = ()> = Result<T, String>;
#[tauri::command]
pub fn copy_clash_env(app_handle: tauri::AppHandle) -> CmdResult {
feat::copy_clash_env(&app_handle);
Ok(())
}
#[tauri::command] #[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> { pub fn get_profiles() -> CmdResult<IProfiles> {
Ok(Config::profiles().data().clone()) Ok(Config::profiles().data().clone())
@@ -322,6 +329,36 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
} }
} }
#[tauri::command]
pub fn get_network_interfaces() -> Vec<String> {
use sysinfo::Networks;
let mut result = Vec::new();
let networks = Networks::new_with_refreshed_list();
for (interface_name, _) in &networks {
result.push(interface_name.clone());
}
return result;
}
#[tauri::command]
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
use network_interface::NetworkInterface;
use network_interface::NetworkInterfaceConfig;
let names = get_network_interfaces();
let interfaces = wrap_err!(NetworkInterface::show())?;
let mut result = Vec::new();
for interface in interfaces {
if names.contains(&interface.name) {
result.push(interface);
}
}
Ok(result)
}
#[tauri::command] #[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) { pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_window("main") {
@@ -352,13 +389,13 @@ pub mod service {
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { pub async fn install_service(passwd: String) -> CmdResult {
wrap_err!(service::install_service().await) wrap_err!(service::install_service(passwd).await)
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { pub async fn uninstall_service(passwd: String) -> CmdResult {
wrap_err!(service::uninstall_service().await) wrap_err!(service::uninstall_service(passwd).await)
} }
} }
+17 -4
View File
@@ -111,6 +111,7 @@ impl IProfiles {
if item.uid.is_none() { if item.uid.is_none() {
bail!("the uid should not be null"); bail!("the uid should not be null");
} }
let uid = item.uid.clone();
// save the file data // save the file data
// move the field value after save // move the field value after save
@@ -128,6 +129,12 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{}\"", file))?; .with_context(|| format!("failed to write to file \"{}\"", file))?;
} }
if self.current.is_none()
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
{
self.current = uid;
}
if self.items.is_none() { if self.items.is_none() {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
@@ -135,6 +142,7 @@ impl IProfiles {
if let Some(items) = self.items.as_mut() { if let Some(items) = self.items.as_mut() {
items.push(item) items.push(item)
} }
self.save_file() self.save_file()
} }
@@ -355,10 +363,15 @@ impl IProfiles {
} }
// delete the original uid // delete the original uid
if current == uid { if current == uid {
self.current = match !items.is_empty() { self.current = None;
true => items[0].uid.clone(), for item in items.iter() {
false => None, if item.itype == Some("remote".to_string())
}; || item.itype == Some("local".to_string())
{
self.current = item.uid.clone();
break;
}
}
} }
self.items = Some(items); self.items = Some(items);
+34 -31
View File
@@ -7,7 +7,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use sysinfo::System; use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep; use tokio::time::sleep;
@@ -44,13 +44,27 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?; let config_path = dirs::path_to_str(&config_path)?;
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into()); let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
let app_dir = dirs::app_home_dir()?; // compatibility
let app_dir = dirs::path_to_str(&app_dir)?; if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let test_dir = dirs::app_home_dir()?.join("test");
let test_dir = dirs::path_to_str(&test_dir)?;
let output = Command::new_sidecar(clash_core)? let output = Command::new_sidecar(clash_core)?
.args(["-t", "-d", app_dir, "-f", config_path]) .args(["-t", "-d", test_dir, "-f", config_path])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
@@ -70,12 +84,6 @@ impl CoreManager {
pub async fn run_core(&self) -> Result<()> { pub async fn run_core(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Run)?; let config_path = Config::generate_file(ConfigType::Run)?;
#[allow(unused_mut)]
let mut should_kill = match self.sidecar.lock().take() {
Some(_) => true,
None => false,
};
// 关闭tun模式 // 关闭tun模式
let mut disable = Mapping::new(); let mut disable = Mapping::new();
let mut tun = Mapping::new(); let mut tun = Mapping::new();
@@ -84,23 +92,19 @@ impl CoreManager {
log::debug!(target: "app", "disable tun mode"); log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await; let _ = clash_api::patch_configs(&disable).await;
let mut system = System::new();
system.refresh_all();
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
}
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await); log_err!(service::stop_core_by_service().await);
should_kill = true; } else {
} let system = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo");
// 这里得等一会儿 for proc in procs {
if should_kill { log::debug!(target: "app", "kill all clash process");
sleep(Duration::from_millis(500)).await; proc.kill();
}
} }
// 服务模式 // 服务模式
@@ -237,8 +241,9 @@ impl CoreManager {
let mut sidecar = self.sidecar.lock(); let mut sidecar = self.sidecar.lock();
let _ = sidecar.take(); let _ = sidecar.take();
let mut system = System::new(); let system = System::new_with_specifics(
system.refresh_all(); RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo"); let procs = system.processes_by_name("verge-mihomo");
for proc in procs { for proc in procs {
log::debug!(target: "app", "kill all clash process"); log::debug!(target: "app", "kill all clash process");
@@ -287,7 +292,6 @@ impl CoreManager {
/// 如果涉及端口和外部控制则需要重启 /// 如果涉及端口和外部控制则需要重启
pub async fn update_config(&self) -> Result<()> { pub async fn update_config(&self) -> Result<()> {
log::debug!(target: "app", "try to update clash config"); log::debug!(target: "app", "try to update clash config");
// 更新订阅 // 更新订阅
Config::generate().await?; Config::generate().await?;
@@ -299,20 +303,19 @@ impl CoreManager {
let path = dirs::path_to_str(&path)?; let path = dirs::path_to_str(&path)?;
// 发送请求 发送5次 // 发送请求 发送5次
for i in 0..5 { for i in 0..10 {
match clash_api::put_configs(path).await { match clash_api::put_configs(path).await {
Ok(_) => break, Ok(_) => break,
Err(err) => { Err(err) => {
if i < 4 { if i < 9 {
log::info!(target: "app", "{err}"); log::info!(target: "app", "{err}");
} else { } else {
bail!(err); bail!(err);
} }
} }
} }
sleep(Duration::from_millis(250)).await; sleep(Duration::from_millis(100)).await;
} }
Ok(()) Ok(())
} }
} }
+94 -47
View File
@@ -28,11 +28,19 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>, pub data: Option<ResponseBody>,
} }
#[cfg(not(target_os = "windows"))]
pub fn sudo(passwd: &String, cmd: String) -> StdCommand {
let shell = format!("echo \"{}\" | sudo -S {}", passwd, cmd);
let mut command = StdCommand::new("bash");
command.arg("-c").arg(shell);
command
}
/// Install the Clash Verge Service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
/// ///
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> { pub async fn install_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -65,30 +73,45 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() { if !installer_path.exists() {
bail!("installer not found"); bail!("installer not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(installer_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(installer_path.clone())
.arg("-c") .output()?;
.arg(installer_path) StdCommand::new("chmod")
.status()?, .arg("+x")
.arg(binary_path)
.output()?;
StdCommand::new(installer_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", installer_path.to_string_lossy()),
)
.output()?;
sudo(
&passwd,
format!("chmod +x {}", binary_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", installer_path.to_string_lossy())).output()?
}
}; };
if !output.status.success() {
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -96,24 +119,32 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() { if !installer_path.exists() {
bail!("installer not found"); bail!("installer not found");
} }
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript") sudo(
.args(vec!["-e", &command]) &passwd,
.status()?; format!(
"chmod +x {}",
installer_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", installer_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
if !status.success() { if !output.status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -122,7 +153,7 @@ pub async fn install_service() -> Result<()> {
/// Uninstall the Clash Verge Service /// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -155,7 +186,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
@@ -165,20 +196,29 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(uninstaller_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(uninstaller_path.clone())
.arg("-c") .output()?;
.arg(uninstaller_path) StdCommand::new(uninstaller_path.clone()).output()?
.status()?, }
_ => {
sudo(
&passwd,
format!("chmod +x {}", uninstaller_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
}
}; };
if !status.success() { if !output.status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -186,7 +226,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service"); let uninstaller_path = binary_path.with_file_name("uninstall-service");
@@ -194,17 +234,24 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ "); sudo(
let command = format!(r#"do shell script "{shell}" with administrator privileges"#); &passwd,
format!(
"chmod +x {}",
uninstaller_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", uninstaller_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
let status = StdCommand::new("osascript") if !output.status.success() {
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to uninstall service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
+3 -3
View File
@@ -297,11 +297,11 @@ impl Tray {
}; };
let _ = tray.set_tooltip(&format!( let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"), t!("SysProxy", "系统代理"),
switch_map[system_proxy], switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"), t!("TUN", "Tun模式"),
switch_map[tun_mode], switch_map[tun_mode],
t!("Curent Profile", "当前订阅"), t!("Profile", "当前订阅"),
current_profile_name current_profile_name
)); ));
+37 -52
View File
@@ -107,48 +107,13 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone()); Config::clash().draft().patch_config(patch.clone());
let res = { let res = {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port");
let port = patch.get("port");
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
}
}
}
};
// 激活订阅 // 激活订阅
if redir_port.is_some() if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|| tproxy_port.is_some()
|| mixed_port.is_some()
|| socks_port.is_some()
|| port.is_some()
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
Config::generate().await?; Config::generate().await?;
CoreManager::global().run_core().await?; CoreManager::global().run_core().await?;
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
// 更新系统代理
if mixed_port.is_some() {
log_err!(sysopt::Sysopt::global().init_sysproxy());
}
if patch.get("mode").is_some() { if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part()); log_err!(handle::Handle::update_systray_part());
} }
@@ -174,7 +139,6 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 一般都是一个个的修改 /// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> { pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone()); Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode; let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch; let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy; let system_proxy = patch.enable_system_proxy;
@@ -182,7 +146,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let pac_content = patch.pac_file_content; let pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass; let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language; let language = patch.language;
let port = patch.verge_mixed_port; let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon; let tray_icon = patch.tray_icon;
let common_tray_icon = patch.common_tray_icon; let common_tray_icon = patch.common_tray_icon;
@@ -190,41 +154,62 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tun_tray_icon = patch.tun_tray_icon; let tun_tray_icon = patch.tun_tray_icon;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let redir_enabled = patch.verge_redir_enabled; let redir_enabled = patch.verge_redir_enabled;
#[cfg(not(target_os = "windows"))]
let redir_port = patch.verge_redir_port;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let tproxy_enabled = patch.verge_tproxy_enabled; let tproxy_enabled = patch.verge_tproxy_enabled;
#[cfg(target_os = "linux")]
let tproxy_port = patch.verge_tproxy_port;
let socks_enabled = patch.verge_socks_enabled; let socks_enabled = patch.verge_socks_enabled;
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled; let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let res = { let res = {
let service_mode = patch.enable_service_mode; let service_mode = patch.enable_service_mode;
let mut generated = false;
if service_mode.is_some() { if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap()); log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
if !generated {
Config::generate().await?; Config::generate().await?;
CoreManager::global().run_core().await?; CoreManager::global().run_core().await?;
generated = true;
}
} else if tun_mode.is_some() { } else if tun_mode.is_some() {
update_core_config().await?; update_core_config().await?;
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() { if redir_enabled.is_some() || redir_port.is_some() {
Config::generate().await?; if !generated {
CoreManager::global().run_core().await?; Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if tproxy_enabled.is_some() { if tproxy_enabled.is_some() || tproxy_port.is_some() {
Config::generate().await?; if !generated {
CoreManager::global().run_core().await?; Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} }
if socks_enabled.is_some() || http_enabled.is_some() { if socks_enabled.is_some()
Config::generate().await?; || http_enabled.is_some()
CoreManager::global().run_core().await?; || socks_port.is_some()
|| http_port.is_some()
|| mixed_port.is_some()
{
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
} }
if auto_launch.is_some() { if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?; sysopt::Sysopt::global().update_launch()?;
} }
if system_proxy.is_some() if system_proxy.is_some()
|| proxy_bypass.is_some() || proxy_bypass.is_some()
|| port.is_some() || mixed_port.is_some()
|| pac.is_some() || pac.is_some()
|| pac_content.is_some() || pac_content.is_some()
{ {
+3
View File
@@ -51,6 +51,7 @@ fn main() -> std::io::Result<()> {
cmds::open_web_url, cmds::open_web_url,
cmds::open_core_dir, cmds::open_core_dir,
cmds::get_portable_flag, cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar, // cmds::kill_sidecar,
cmds::restart_sidecar, cmds::restart_sidecar,
// clash // clash
@@ -63,6 +64,7 @@ fn main() -> std::io::Result<()> {
cmds::get_runtime_exists, cmds::get_runtime_exists,
cmds::get_runtime_logs, cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool, cmds::uwp::invoke_uwp_tool,
cmds::copy_clash_env,
// verge // verge
cmds::get_verge_config, cmds::get_verge_config,
cmds::patch_verge_config, cmds::patch_verge_config,
@@ -72,6 +74,7 @@ fn main() -> std::io::Result<()> {
cmds::download_icon_cache, cmds::download_icon_cache,
cmds::open_devtools, cmds::open_devtools,
cmds::exit_app, cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys, // cmds::update_hotkeys,
// profile // profile
cmds::get_profiles, cmds::get_profiles,
+13 -5
View File
@@ -191,11 +191,15 @@ pub fn init_config() -> Result<()> {
/// after tauri setup /// after tauri setup
pub fn init_resources() -> Result<()> { pub fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let test_dir = app_dir.join("test");
let res_dir = dirs::app_resources_dir()?; let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() { if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir); let _ = fs::create_dir_all(&app_dir);
} }
if !test_dir.exists() {
let _ = fs::create_dir_all(&test_dir);
}
if !res_dir.exists() { if !res_dir.exists() {
let _ = fs::create_dir_all(&res_dir); let _ = fs::create_dir_all(&res_dir);
} }
@@ -210,9 +214,10 @@ pub fn init_resources() -> Result<()> {
for file in file_list.iter() { for file in file_list.iter() {
let src_path = res_dir.join(file); let src_path = res_dir.join(file);
let dest_path = app_dir.join(file); let dest_path = app_dir.join(file);
let test_dest_path = test_dir.join(file);
let handle_copy = || { let handle_copy = |dest: &PathBuf| {
match fs::copy(&src_path, &dest_path) { match fs::copy(&src_path, dest) {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"), Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
Err(err) => { Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}', {err}") log::error!(target: "app", "failed to copy resources '{file}', {err}")
@@ -220,8 +225,11 @@ pub fn init_resources() -> Result<()> {
}; };
}; };
if src_path.exists() && !test_dest_path.exists() {
handle_copy(&test_dest_path);
}
if src_path.exists() && !dest_path.exists() { if src_path.exists() && !dest_path.exists() {
handle_copy(); handle_copy(&dest_path);
continue; continue;
} }
@@ -231,14 +239,14 @@ pub fn init_resources() -> Result<()> {
match (src_modified, dest_modified) { match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => { (Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified { if src_modified > dest_modified {
handle_copy(); handle_copy(&dest_path);
} else { } else {
log::debug!(target: "app", "skipping resource copy '{file}'"); log::debug!(target: "app", "skipping resource copy '{file}'");
} }
} }
_ => { _ => {
log::debug!(target: "app", "failed to get modified '{file}'"); log::debug!(target: "app", "failed to get modified '{file}'");
handle_copy(); handle_copy(&dest_path);
} }
}; };
} }
-1
View File
@@ -4,4 +4,3 @@ pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod tmpl; pub mod tmpl;
pub mod unix_helper;
+1
View File
@@ -8,6 +8,7 @@ use serde_yaml::Mapping;
use std::net::TcpListener; use std::net::TcpListener;
use tauri::api::notification; use tauri::api::notification;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
#[cfg(not(target_os = "linux"))]
use window_shadows::set_shadow; use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new(); pub static VERSION: OnceCell<String> = OnceCell::new();
-14
View File
@@ -1,14 +0,0 @@
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> &'static str {
use std::process::Command;
match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "1.7.3" "version": "1.7.7"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
+6 -2
View File
@@ -1,7 +1,11 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { ReactNode, useState, useEffect } from "react"; import { ReactNode, useState, useEffect } from "react";
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material"; import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material"; import {
CloseRounded,
CheckCircleRounded,
ErrorRounded,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
interface InnerProps { interface InnerProps {
@@ -81,7 +85,7 @@ const NoticeInner = (props: InnerProps) => {
transitionDuration={200} transitionDuration={200}
action={ action={
<IconButton size="small" color="inherit" onClick={onBtnClose}> <IconButton size="small" color="inherit" onClick={onBtnClose}>
<Close fontSize="inherit" /> <CloseRounded fontSize="inherit" />
</IconButton> </IconButton>
} }
/> />
+2 -2
View File
@@ -26,7 +26,7 @@ type SearchProps = {
export const BaseSearchBox = styled((props: SearchProps) => { export const BaseSearchBox = styled((props: SearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? true); const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
const [matchWholeWord, setMatchWholeWord] = useState( const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false props.matchWholeWord ?? false
); );
@@ -96,7 +96,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
return ( return (
<Tooltip title={errorMessage} placement="bottom-start"> <Tooltip title={errorMessage} placement="bottom-start">
<TextField <TextField
autoComplete="off" autoComplete="new-password"
inputRef={inputRef} inputRef={inputRef}
hiddenLabel hiddenLabel
fullWidth fullWidth
+1 -1
View File
@@ -4,7 +4,7 @@ export const BaseStyledSelect = styled((props: SelectProps<string>) => {
return ( return (
<Select <Select
size="small" size="small"
autoComplete="off" autoComplete="new-password"
sx={{ sx={{
width: 120, width: 120,
height: 33.375, height: 33.375,
@@ -6,7 +6,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
return ( return (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
hiddenLabel hiddenLabel
fullWidth fullWidth
size="small" size="small"
+1
View File
@@ -11,6 +11,7 @@ export const Switch = styled((props: SwitchProps) => (
width: 42, width: 42,
height: 26, height: 26,
padding: 0, padding: 0,
marginRight: 1,
"& .MuiSwitch-switchBase": { "& .MuiSwitch-switchBase": {
padding: 0, padding: 0,
margin: 2, margin: 2,
+6 -6
View File
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { import {
ArrowDownward, ArrowDownwardRounded,
ArrowUpward, ArrowUpwardRounded,
MemoryOutlined, MemoryRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -153,7 +153,7 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}> <Box display="flex" flexDirection="column" gap={0.75}>
<Box title={t("Upload Speed")} {...boxStyle}> <Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpward <ArrowUpwardRounded
{...iconStyle} {...iconStyle}
color={+up > 0 ? "secondary" : "disabled"} color={+up > 0 ? "secondary" : "disabled"}
/> />
@@ -164,7 +164,7 @@ export const LayoutTraffic = () => {
</Box> </Box>
<Box title={t("Download Speed")} {...boxStyle}> <Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownward <ArrowDownwardRounded
{...iconStyle} {...iconStyle}
color={+down > 0 ? "primary" : "disabled"} color={+down > 0 ? "primary" : "disabled"}
/> />
@@ -184,7 +184,7 @@ export const LayoutTraffic = () => {
isDebug && (await gc()); isDebug && (await gc());
}} }}
> >
<MemoryOutlined {...iconStyle} /> <MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography> <Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography> <Typography {...unitStyle}>{inuseUnit}</Typography>
</Box> </Box>
+25 -11
View File
@@ -10,14 +10,17 @@ import {
DialogTitle, DialogTitle,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import FormatPaintIcon from "@mui/icons-material/FormatPaint"; import {
import OpenInFullIcon from "@mui/icons-material/OpenInFull"; FormatPaintRounded,
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen"; OpenInFullRounded,
CloseFullscreenRounded,
} from "@mui/icons-material";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import debounce from "@/utils/debounce";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
@@ -144,12 +147,19 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
} }
}); });
const editorResize = debounce(() => {
editorRef.current?.layout();
setTimeout(() => editorRef.current?.layout(), 500);
}, 100);
useEffect(() => { useEffect(() => {
const unlistenResized = appWindow.onResized(() => { const onResized = debounce(() => {
editorResize();
appWindow.isMaximized().then((maximized) => { appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized); setIsMaximized(() => maximized);
}); });
}); }, 100);
const unlistenResized = appWindow.onResized(onResized);
return () => { return () => {
unlistenResized.then((fn) => fn()); unlistenResized.then((fn) => fn());
@@ -162,7 +172,13 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ width: "auto", height: "calc(100vh - 185px)" }}> <DialogContent
sx={{
width: "auto",
height: "calc(100vh - 185px)",
overflow: "hidden",
}}
>
<MonacoEditor <MonacoEditor
language={language} language={language}
theme={themeMode === "light" ? "vs" : "vs-dark"} theme={themeMode === "light" ? "vs" : "vs-dark"}
@@ -209,17 +225,15 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
?.run() ?.run()
} }
> >
<FormatPaintIcon fontSize="inherit" /> <FormatPaintRounded fontSize="inherit" />
</IconButton> </IconButton>
<IconButton <IconButton
size="medium" size="medium"
color="inherit" color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")} title={t(isMaximized ? "Minimize" : "Maximize")}
onClick={() => onClick={() => appWindow.toggleMaximize().then(editorResize)}
appWindow.toggleMaximize().then(() => editorRef.current?.layout())
}
> >
{isMaximized ? <CloseFullscreenIcon /> : <OpenInFullIcon />} {isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
</IconButton> </IconButton>
</ButtonGroup> </ButtonGroup>
</DialogContent> </DialogContent>
+11 -1
View File
@@ -22,7 +22,14 @@ export const GroupItem = (props: Props) => {
let { type, group, onDelete } = props; let { type, group, onDelete } = props;
const sortable = type === "prepend" || type === "append"; const sortable = type === "prepend" || type === "append";
const { attributes, listeners, setNodeRef, transform, transition } = sortable const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: group.name }) ? useSortable({ id: group.name })
: { : {
attributes: {}, attributes: {},
@@ -30,6 +37,7 @@ export const GroupItem = (props: Props) => {
setNodeRef: null, setNodeRef: null,
transform: null, transform: null,
transition: null, transition: null,
isDragging: false,
}; };
const [iconCachePath, setIconCachePath] = useState(""); const [iconCachePath, setIconCachePath] = useState("");
@@ -55,6 +63,7 @@ export const GroupItem = (props: Props) => {
<ListItem <ListItem
dense dense
sx={({ palette }) => ({ sx={({ palette }) => ({
position: "relative",
background: background:
type === "original" type === "original"
? palette.mode === "dark" ? palette.mode === "dark"
@@ -68,6 +77,7 @@ export const GroupItem = (props: Props) => {
borderRadius: "8px", borderRadius: "8px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})} })}
> >
{group.icon && group.icon?.trim().startsWith("http") && ( {group.icon && group.icon?.trim().startsWith("http") && (
+85 -42
View File
@@ -1,4 +1,4 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -23,14 +23,23 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
InputAdornment,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
TextField, TextField,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { GroupItem } from "@/components/profile/group-item"; import { GroupItem } from "@/components/profile/group-item";
import { readProfileFile, saveProfileFile } from "@/services/cmds"; import {
getNetworkInterfaces,
readProfileFile,
saveProfileFile,
} from "@/services/cmds";
import { Notice, Switch } from "@/components/base"; import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { BaseSearchBox } from "../base/base-search-box"; import { BaseSearchBox } from "../base/base-search-box";
@@ -60,7 +69,7 @@ export const GroupsEditorViewer = (props: Props) => {
const [currData, setCurrData] = useState(""); const [currData, setCurrData] = useState("");
const [visualization, setVisualization] = useState(true); const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const [interfaceNameList, setInterfaceNameList] = useState<string[]>([]);
const { control, watch, register, ...formIns } = useForm<IProxyGroupConfig>({ const { control, watch, register, ...formIns } = useForm<IProxyGroupConfig>({
defaultValues: { defaultValues: {
type: "select", type: "select",
@@ -251,6 +260,10 @@ export const GroupsEditorViewer = (props: Props) => {
setProxyProviderList(Object.keys(provider)); setProxyProviderList(Object.keys(provider));
setGroupList(originGroupsObj?.["proxy-groups"] || []); setGroupList(originGroupsObj?.["proxy-groups"] || []);
}; };
const getInterfaceNameList = async () => {
let list = await getNetworkInterfaces();
setInterfaceNameList(list);
};
useEffect(() => { useEffect(() => {
fetchProxyPolicy(); fetchProxyPolicy();
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
@@ -259,12 +272,13 @@ export const GroupsEditorViewer = (props: Props) => {
fetchContent(); fetchContent();
fetchProxyPolicy(); fetchProxyPolicy();
fetchProfile(); fetchProfile();
getInterfaceNameList();
}, [open]); }, [open]);
const validateGroup = () => { const validateGroup = () => {
let group = formIns.getValues(); let group = formIns.getValues();
if (group.name === "") { if (group.name === "") {
throw new Error(t("Group Name Cannot Be Empty")); throw new Error(t("Group Name Required"));
} }
}; };
@@ -333,6 +347,11 @@ export const GroupsEditorViewer = (props: Props) => {
"relay", "relay",
]} ]}
value={field.value} value={field.value}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
onChange={(_, value) => value && field.onChange(value)} onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
/> />
@@ -346,7 +365,7 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Group Name")} /> <ListItemText primary={t("Group Name")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} {...field}
@@ -361,9 +380,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Item> <Item>
<ListItemText primary={t("Icon")} /> <ListItemText primary={t("Proxy Group Icon")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} {...field}
@@ -387,6 +406,11 @@ export const GroupsEditorViewer = (props: Props) => {
disableCloseOnSelect disableCloseOnSelect
onChange={(_, value) => value && field.onChange(value)} onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
/> />
</Item> </Item>
)} )}
@@ -409,7 +433,6 @@ export const GroupsEditorViewer = (props: Props) => {
</Item> </Item>
)} )}
/> />
<Controller <Controller
name="url" name="url"
control={control} control={control}
@@ -417,7 +440,8 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Health Check Url")} /> <ListItemText primary={t("Health Check Url")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
placeholder="https://www.gstatic.com/generate_204"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} {...field}
@@ -425,6 +449,24 @@ export const GroupsEditorViewer = (props: Props) => {
</Item> </Item>
)} )}
/> />
<Controller
name="expected-status"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Expected Status")} />
<TextField
autoComplete="new-password"
placeholder="*"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller <Controller
name="interval" name="interval"
control={control} control={control}
@@ -432,13 +474,21 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Interval")} /> <ListItemText primary={t("Interval")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
placeholder="300"
type="number" type="number"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => { onChange={(e) => {
field.onChange(parseInt(e.target.value)); field.onChange(parseInt(e.target.value));
}} }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
</InputAdornment>
),
}}
/> />
</Item> </Item>
)} )}
@@ -450,13 +500,21 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Timeout")} /> <ListItemText primary={t("Timeout")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
placeholder="5000"
type="number" type="number"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => { onChange={(e) => {
field.onChange(parseInt(e.target.value)); field.onChange(parseInt(e.target.value));
}} }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{t("millis")}
</InputAdornment>
),
}}
/> />
</Item> </Item>
)} )}
@@ -468,7 +526,8 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Max Failed Times")} /> <ListItemText primary={t("Max Failed Times")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
placeholder="5"
type="number" type="number"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
@@ -485,11 +544,13 @@ export const GroupsEditorViewer = (props: Props) => {
render={({ field }) => ( render={({ field }) => (
<Item> <Item>
<ListItemText primary={t("Interface Name")} /> <ListItemText primary={t("Interface Name")} />
<TextField <Autocomplete
autoComplete="off"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} options={interfaceNameList}
value={field.value}
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/> />
</Item> </Item>
)} )}
@@ -501,7 +562,7 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Routing Mark")} /> <ListItemText primary={t("Routing Mark")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
type="number" type="number"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
@@ -519,7 +580,7 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Filter")} /> <ListItemText primary={t("Filter")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} {...field}
@@ -534,7 +595,7 @@ export const GroupsEditorViewer = (props: Props) => {
<Item> <Item>
<ListItemText primary={t("Exclude Filter")} /> <ListItemText primary={t("Exclude Filter")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: "calc(100% - 150px)" }} sx={{ width: "calc(100% - 150px)" }}
{...field} {...field}
@@ -588,23 +649,6 @@ export const GroupsEditorViewer = (props: Props) => {
</Item> </Item>
)} )}
/> />
<Controller
name="expected-status"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Expected Status")} />
<TextField
autoComplete="off"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller <Controller
name="include-all" name="include-all"
control={control} control={control}
@@ -670,15 +714,16 @@ export const GroupsEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => { onClick={() => {
try { try {
validateGroup(); validateGroup();
for (const item of prependSeq) { for (const item of [...prependSeq, ...groupList]) {
if (item.name === formIns.getValues().name) { if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists")); throw new Error(t("Group Name Already Exists"));
} }
} }
setPrependSeq([...prependSeq, formIns.getValues()]); setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
} }
@@ -691,10 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => { onClick={() => {
try { try {
validateGroup(); validateGroup();
for (const item of appendSeq) { for (const item of [...appendSeq, ...groupList]) {
if (item.name === formIns.getValues().name) { if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists")); throw new Error(t("Group Name Already Exists"));
} }
@@ -716,10 +762,7 @@ export const GroupsEditorViewer = (props: Props) => {
padding: "0 10px", padding: "0 10px",
}} }}
> >
<BaseSearchBox <BaseSearchBox onSearch={(match) => setMatch(() => match)} />
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso <Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }} style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={ totalCount={
+87 -62
View File
@@ -15,7 +15,7 @@ import {
Menu, Menu,
CircularProgress, CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material"; import { RefreshRounded, DragIndicatorRounded } from "@mui/icons-material";
import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import { import {
viewProfile, viewProfile,
@@ -51,8 +51,14 @@ interface Props {
export const ProfileItem = (props: Props) => { export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } = const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } =
props; props;
const { attributes, listeners, setNodeRef, transform, transition } = const {
useSortable({ id: props.id }); attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -73,7 +79,10 @@ export const ProfileItem = (props: Props) => {
const from = parseUrl(itemData.url); const from = parseUrl(itemData.url);
const description = itemData.desc; const description = itemData.desc;
const expire = parseExpire(extra?.expire); const expire = parseExpire(extra?.expire);
const progress = Math.round(((download + upload) * 100) / (total + 0.01) + 1); const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
);
const loading = loadingCache[itemData.uid] ?? false; const loading = loadingCache[itemData.uid] ?? false;
@@ -297,8 +306,10 @@ export const ProfileItem = (props: Props) => {
return ( return (
<Box <Box
sx={{ sx={{
position: "relative",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}} }}
> >
<ProfileBox <ProfileBox
@@ -337,7 +348,7 @@ export const ProfileItem = (props: Props) => {
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
<DragIndicator <DragIndicatorRounded
sx={[ sx={[
{ cursor: "move", marginLeft: "-6px" }, { cursor: "move", marginLeft: "-6px" },
({ palette: { text } }) => { ({ palette: { text } }) => {
@@ -472,64 +483,78 @@ export const ProfileItem = (props: Props) => {
</MenuItem> </MenuItem>
))} ))}
</Menu> </Menu>
{fileOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(uid)}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(uid, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
)}
{rulesOpen && (
<RulesEditorViewer
groupsUid={option?.groups ?? ""}
mergeUid={option?.merge ?? ""}
profileUid={uid}
property={option?.rules ?? ""}
open={true}
onSave={onSave}
onClose={() => setRulesOpen(false)}
/>
)}
{proxiesOpen && (
<ProxiesEditorViewer
profileUid={uid}
property={option?.proxies ?? ""}
open={true}
onSave={onSave}
onClose={() => setProxiesOpen(false)}
/>
)}
{groupsOpen && (
<GroupsEditorViewer
mergeUid={option?.merge ?? ""}
proxiesUid={option?.proxies ?? ""}
profileUid={uid}
property={option?.groups ?? ""}
open={true}
onSave={onSave}
onClose={() => {
setGroupsOpen(false);
}}
/>
)}
{mergeOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
)}
{scriptOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
)}
<EditorViewer
open={fileOpen}
initialData={readProfileFile(uid)}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(uid, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
<RulesEditorViewer
groupsUid={option?.groups ?? ""}
mergeUid={option?.merge ?? ""}
profileUid={uid}
property={option?.rules ?? ""}
open={rulesOpen}
onSave={onSave}
onClose={() => setRulesOpen(false)}
/>
<ProxiesEditorViewer
profileUid={uid}
property={option?.proxies ?? ""}
open={proxiesOpen}
onSave={onSave}
onClose={() => setProxiesOpen(false)}
/>
<GroupsEditorViewer
mergeUid={option?.merge ?? ""}
proxiesUid={option?.proxies ?? ""}
profileUid={uid}
property={option?.groups ?? ""}
open={groupsOpen}
onSave={onSave}
onClose={() => setGroupsOpen(false)}
/>
<EditorViewer
open={mergeOpen}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
<EditorViewer
open={scriptOpen}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
<ConfirmViewer <ConfirmViewer
title={t("Confirm deletion")} title={t("Confirm deletion")}
message={t("This operation is not reversible")} message={t("This operation is not reversible")}
+21 -19
View File
@@ -167,25 +167,27 @@ export const ProfileMore = (props: Props) => {
</MenuItem> </MenuItem>
))} ))}
</Menu> </Menu>
{fileOpen && (
<EditorViewer <EditorViewer
open={fileOpen} open={true}
title={`${t("Global " + id)}`} title={`${t("Global " + id)}`}
initialData={readProfileFile(id)} initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"} language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined} schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? ""); await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr); onSave && onSave(prev, curr);
}} }}
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
)}
<LogViewer {logOpen && (
open={logOpen} <LogViewer
logInfo={logInfo} open={logOpen}
onClose={() => setLogOpen(false)} logInfo={logInfo}
/> onClose={() => setLogOpen(false)}
/>
)}
</> </>
); );
}; };
@@ -24,10 +24,13 @@ import {
DialogTitle, DialogTitle,
List, List,
ListItem, ListItem,
ListItemText,
TextField, TextField,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { ProxyItem } from "@/components/profile/proxy-item"; import { ProxyItem } from "@/components/profile/proxy-item";
import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
@@ -256,7 +259,7 @@ export const ProxiesEditorViewer = (props: Props) => {
> >
<Item> <Item>
<TextField <TextField
autoComplete="off" autoComplete="new-password"
placeholder={t("Use newlines for multiple uri")} placeholder={t("Use newlines for multiple uri")}
fullWidth fullWidth
rows={9} rows={9}
@@ -270,9 +273,10 @@ export const ProxiesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => { onClick={() => {
let proxies = handleParse(); let proxies = handleParse();
setPrependSeq([...prependSeq, ...proxies]); setPrependSeq([...proxies, ...prependSeq]);
}} }}
> >
{t("Prepend Proxy")} {t("Prepend Proxy")}
@@ -282,6 +286,7 @@ export const ProxiesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => { onClick={() => {
let proxies = handleParse(); let proxies = handleParse();
setAppendSeq([...appendSeq, ...proxies]); setAppendSeq([...appendSeq, ...proxies]);
@@ -298,10 +303,7 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: "0 10px", padding: "0 10px",
}} }}
> >
<BaseSearchBox <BaseSearchBox onSearch={(match) => setMatch(() => match)} />
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso <Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }} style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={ totalCount={
+12 -1
View File
@@ -20,7 +20,14 @@ export const ProxyItem = (props: Props) => {
let { type, proxy, onDelete } = props; let { type, proxy, onDelete } = props;
const sortable = type === "prepend" || type === "append"; const sortable = type === "prepend" || type === "append";
const { attributes, listeners, setNodeRef, transform, transition } = sortable const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: proxy.name }) ? useSortable({ id: proxy.name })
: { : {
attributes: {}, attributes: {},
@@ -28,12 +35,14 @@ export const ProxyItem = (props: Props) => {
setNodeRef: null, setNodeRef: null,
transform: null, transform: null,
transition: null, transition: null,
isDragging: false,
}; };
return ( return (
<ListItem <ListItem
dense dense
sx={({ palette }) => ({ sx={({ palette }) => ({
position: "relative",
background: background:
type === "original" type === "original"
? palette.mode === "dark" ? palette.mode === "dark"
@@ -47,6 +56,7 @@ export const ProxyItem = (props: Props) => {
borderRadius: "8px", borderRadius: "8px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})} })}
> >
<ListItemText <ListItemText
@@ -56,6 +66,7 @@ export const ProxyItem = (props: Props) => {
sx={{ cursor: sortable ? "move" : "" }} sx={{ cursor: sortable ? "move" : "" }}
primary={ primary={
<StyledPrimary <StyledPrimary
title={proxy.name}
sx={{ textDecoration: type === "delete" ? "line-through" : "" }} sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
> >
{proxy.name} {proxy.name}
+12 -1
View File
@@ -24,7 +24,14 @@ export const RuleItem = (props: Props) => {
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? ""; const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? "";
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1); const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1);
const { attributes, listeners, setNodeRef, transform, transition } = sortable const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: ruleRaw }) ? useSortable({ id: ruleRaw })
: { : {
attributes: {}, attributes: {},
@@ -32,11 +39,13 @@ export const RuleItem = (props: Props) => {
setNodeRef: null, setNodeRef: null,
transform: null, transform: null,
transition: null, transition: null,
isDragging: false,
}; };
return ( return (
<ListItem <ListItem
dense dense
sx={({ palette }) => ({ sx={({ palette }) => ({
position: "relative",
background: background:
type === "original" type === "original"
? palette.mode === "dark" ? palette.mode === "dark"
@@ -50,6 +59,7 @@ export const RuleItem = (props: Props) => {
borderRadius: "8px", borderRadius: "8px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})} })}
> >
<ListItemText <ListItemText
@@ -59,6 +69,7 @@ export const RuleItem = (props: Props) => {
sx={{ cursor: sortable ? "move" : "" }} sx={{ cursor: sortable ? "move" : "" }}
primary={ primary={
<StyledPrimary <StyledPrimary
title={ruleContent || "-"}
sx={{ textDecoration: type === "delete" ? "line-through" : "" }} sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
> >
{ruleContent || "-"} {ruleContent || "-"}
@@ -29,7 +29,10 @@ import {
TextField, TextField,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice, Switch } from "@/components/base"; import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
@@ -495,7 +498,7 @@ export const RulesEditorViewer = (props: Props) => {
{ruleType.name !== "RULE-SET" && {ruleType.name !== "RULE-SET" &&
ruleType.name !== "SUB-RULE" && ( ruleType.name !== "SUB-RULE" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ minWidth: "240px" }} sx={{ minWidth: "240px" }}
value={ruleContent} value={ruleContent}
@@ -535,11 +538,12 @@ export const RulesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => { onClick={() => {
try { try {
let raw = validateRule(); let raw = validateRule();
if (prependSeq.includes(raw)) return; if (prependSeq.includes(raw)) return;
setPrependSeq([...prependSeq, raw]); setPrependSeq([raw, ...prependSeq]);
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
} }
@@ -552,6 +556,7 @@ export const RulesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => { onClick={() => {
try { try {
let raw = validateRule(); let raw = validateRule();
@@ -573,10 +578,7 @@ export const RulesEditorViewer = (props: Props) => {
padding: "0 10px", padding: "0 10px",
}} }}
> >
<BaseSearchBox <BaseSearchBox onSearch={(match) => setMatch(() => match)} />
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso <Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }} style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={ totalCount={
+4 -2
View File
@@ -105,8 +105,9 @@ export const ProviderButton = () => {
const download = sub?.Download || 0; const download = sub?.Download || 0;
const total = sub?.Total || 0; const total = sub?.Total || 0;
const expire = sub?.Expire || 0; const expire = sub?.Expire || 0;
const progress = Math.round( const progress = Math.min(
((download + upload) * 100) / (total + 0.1) Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
); );
return ( return (
<> <>
@@ -159,6 +160,7 @@ export const ProviderButton = () => {
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={progress} value={progress}
style={{ opacity: total > 0 ? 1 : 0 }}
/> />
</> </>
)} )}
+8 -4
View File
@@ -21,6 +21,7 @@ import delayManager from "@/services/delay";
interface Props { interface Props {
sx?: SxProps; sx?: SxProps;
url?: string;
groupName: string; groupName: string;
headState: HeadState; headState: HeadState;
onLocation: () => void; onLocation: () => void;
@@ -29,7 +30,7 @@ interface Props {
} }
export const ProxyHead = (props: Props) => { export const ProxyHead = (props: Props) => {
const { sx = {}, groupName, headState, onHeadState } = props; const { sx = {}, url, groupName, headState, onHeadState } = props;
const { showType, sortType, filterText, textState, testUrl } = headState; const { showType, sortType, filterText, textState, testUrl } = headState;
@@ -45,7 +46,10 @@ export const ProxyHead = (props: Props) => {
const { verge } = useVerge(); const { verge } = useVerge();
useEffect(() => { useEffect(() => {
delayManager.setUrl(groupName, testUrl || verge?.default_latency_test!); delayManager.setUrl(
groupName,
testUrl || url || verge?.default_latency_test!
);
}, [groupName, testUrl, verge?.default_latency_test]); }, [groupName, testUrl, verge?.default_latency_test]);
return ( return (
@@ -132,7 +136,7 @@ export const ProxyHead = (props: Props) => {
{textState === "filter" && ( {textState === "filter" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
autoFocus={autoFocus} autoFocus={autoFocus}
hiddenLabel hiddenLabel
value={filterText} value={filterText}
@@ -146,7 +150,7 @@ export const ProxyHead = (props: Props) => {
{textState === "url" && ( {textState === "url" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
autoFocus={autoFocus} autoFocus={autoFocus}
hiddenLabel hiddenLabel
autoSave="off" autoSave="off"
+1
View File
@@ -129,6 +129,7 @@ export const ProxyRender = (props: RenderProps) => {
return ( return (
<ProxyHead <ProxyHead
sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }} sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
url={group.testUrl}
groupName={group.name} groupName={group.name}
headState={headState!} headState={headState!}
onLocation={() => onLocation(group)} onLocation={() => onLocation(group)}
@@ -5,7 +5,10 @@ import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material"; import {
SwitchAccessShortcutRounded,
RestartAltRounded,
} from "@mui/icons-material";
import { import {
Box, Box,
Button, Button,
@@ -85,7 +88,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<LoadingButton <LoadingButton
variant="contained" variant="contained"
size="small" size="small"
startIcon={<SwitchAccessShortcut />} startIcon={<SwitchAccessShortcutRounded />}
loadingPosition="start" loadingPosition="start"
loading={upgrading} loading={upgrading}
sx={{ marginRight: "8px" }} sx={{ marginRight: "8px" }}
@@ -97,7 +100,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
variant="contained" variant="contained"
size="small" size="small"
onClick={onRestart} onClick={onRestart}
startIcon={<RestartAlt />} startIcon={<RestartAltRounded />}
> >
{t("Restart")} {t("Restart")}
</Button> </Button>
@@ -13,7 +13,6 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { clashInfo, patchInfo } = useClashInfo(); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge(); const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [redirPort, setRedirPort] = useState( const [redirPort, setRedirPort] = useState(
verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895 verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895
@@ -94,24 +93,57 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
return; return;
} }
try { try {
if (OS !== "windows") { if (OS === "windows") {
await patchInfo({ "redir-port": redirPort }); await patchInfo({
await patchVerge({ verge_redir_port: redirPort }); "mixed-port": mixedPort,
await patchVerge({ verge_redir_enabled: redirEnabled }); "socks-port": socksPort,
port,
});
await patchVerge({
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
}
if (OS === "macos") {
await patchInfo({
"redir-port": redirPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
} }
if (OS === "linux") { if (OS === "linux") {
await patchInfo({ "tproxy-port": tproxyPort }); await patchInfo({
await patchVerge({ verge_tproxy_port: tproxyPort }); "redir-port": redirPort,
await patchVerge({ verge_tproxy_enabled: tproxyEnabled }); "tproxy-port": tproxyPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
} }
await patchInfo({ "mixed-port": mixedPort });
await patchInfo({ "socks-port": socksPort });
await patchInfo({ port });
await patchVerge({ verge_mixed_port: mixedPort });
await patchVerge({ verge_socks_port: socksPort });
await patchVerge({ verge_port: port });
await patchVerge({ verge_socks_enabled: socksEnabled });
await patchVerge({ verge_http_enabled: httpEnabled });
setOpen(false); setOpen(false);
Notice.success(t("Clash Port Modified"), 1000); Notice.success(t("Clash Port Modified"), 1000);
} catch (err: any) { } catch (err: any) {
@@ -134,7 +166,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Mixed Port")} /> <ListItemText primary={t("Mixed Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={mixedPort} value={mixedPort}
@@ -146,7 +178,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Socks Port")} /> <ListItemText primary={t("Socks Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={socksPort} value={socksPort}
@@ -169,7 +201,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Http Port")} /> <ListItemText primary={t("Http Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={port} value={port}
@@ -193,7 +225,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Redir Port")} /> <ListItemText primary={t("Redir Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={redirPort} value={redirPort}
@@ -218,7 +250,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Tproxy Port")} /> <ListItemText primary={t("Tproxy Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={tproxyPort} value={tproxyPort}
@@ -20,11 +20,12 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
close: () => setOpen(false), close: () => setOpen(false),
})); }));
if (!open) return null;
return ( return (
<EditorViewer <EditorViewer
open={open} open={true}
title={ title={
<Box> <Box display="flex" alignItems="center" gap={2}>
{t("Runtime Config")} {t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" /> <Chip label={t("ReadOnly")} size="small" />
</Box> </Box>
@@ -48,7 +48,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("External Controller")} /> <ListItemText primary={t("External Controller")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 175 }} sx={{ width: 175 }}
value={controller} value={controller}
@@ -60,7 +60,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Core Secret")} /> <ListItemText primary={t("Core Secret")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 175 }} sx={{ width: 175 }}
value={secret} value={secret}
+2 -2
View File
@@ -198,7 +198,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
<TooltipIcon title={t("Default Latency Test Info")} /> <TooltipIcon title={t("Default Latency Test Info")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -215,7 +215,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} /> <ListItemText primary={t("Default Latency Timeout")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
type="number" type="number"
autoCorrect="off" autoCorrect="off"
@@ -0,0 +1,139 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { getNetworkInterfacesInfo } from "@/services/cmds";
import { alpha, Box, Button, Chip, IconButton } from "@mui/material";
import { ContentCopyRounded } from "@mui/icons-material";
import { writeText } from "@tauri-apps/api/clipboard";
export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [networkInterfaces, setNetworkInterfaces] = useState<
INetworkInterface[]
>([]);
const [isV4, setIsV4] = useState(true);
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
},
close: () => setOpen(false),
}));
useEffect(() => {
if (!open) return;
getNetworkInterfacesInfo().then((res) => {
setNetworkInterfaces(res);
});
}, [open]);
return (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Network Interface")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setIsV4((prev) => !prev);
}}
>
{isV4 ? "Ipv6" : "Ipv4"}
</Button>
</Box>
</Box>
}
contentSx={{ width: 450, maxHeight: 330 }}
disableOk
cancelBtn={t("Close")}
onCancel={() => setOpen(false)}
>
{networkInterfaces.map((item) => (
<Box key={item.name}>
<h4>{item.name}</h4>
<Box>
{isV4 && (
<>
{item.addr.map(
(address) =>
address.V4 && (
<AddressDisplay
key={address.V4.ip}
label={t("Ip Address")}
content={address.V4.ip}
/>
)
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
)}
{!isV4 && (
<>
{item.addr.map(
(address) =>
address.V6 && (
<AddressDisplay
key={address.V6.ip}
label={t("Ip Address")}
content={address.V6.ip}
/>
)
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
)}
</Box>
</Box>
))}
</BaseDialog>
);
});
const AddressDisplay = (props: { label: string; content: string }) => {
const { t } = useTranslation();
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "8px 0",
}}
>
<Box>{props.label}</Box>
<Box
sx={({ palette }) => ({
borderRadius: "8px",
padding: "2px 2px 2px 8px",
background:
palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3),
})}
>
<Box sx={{ display: "inline", userSelect: "text" }}>
{props.content}
</Box>
<IconButton
size="small"
onClick={async () => {
await writeText(props.content);
Notice.success(t("Copy Success"));
}}
>
<ContentCopyRounded sx={{ fontSize: "18px" }} />
</IconButton>
</Box>
</Box>
);
};
@@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
interface Props {
onConfirm: (passwd: string) => Promise<void>;
}
export const PasswordInput = (props: Props) => {
const { onConfirm } = props;
const { t } = useTranslation();
const [passwd, setPasswd] = useState("");
useEffect(() => {
if (!open) return;
}, [open]);
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t("Password")}
fullWidth
size="small"
type="password"
value={passwd}
onKeyDown={(e) => e.key === "Enter" && onConfirm(passwd)}
onChange={(e) => setPasswd(e.target.value)}
></TextField>
</DialogContent>
<DialogActions>
<Button
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t("Confirm")}
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,134 @@
import { KeyedMutator } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { installService, uninstallService } from "@/services/cmds";
import { Notice } from "@/components/base";
import { LoadingButton } from "@mui/lab";
import { PasswordInput } from "./password-input";
import getSystem from "@/utils/get-system";
interface Props {
status: "active" | "installed" | "unknown" | "uninstall";
mutate: KeyedMutator<"active" | "installed" | "unknown" | "uninstall">;
patchVerge: (value: Partial<IVergeConfig>) => Promise<void>;
onChangeData: (patch: Partial<IVergeConfig>) => void;
}
export const ServiceSwitcher = (props: Props) => {
const { status, mutate, patchVerge, onChangeData } = props;
const isWindows = getSystem() === "windows";
const isActive = status === "active";
const isInstalled = status === "installed";
const isUninstall = status === "uninstall" || status === "unknown";
const { t } = useTranslation();
const [serviceLoading, setServiceLoading] = useState(false);
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
const [openInstall, setOpenInstall] = useState(false);
const [openUninstall, setOpenUninstall] = useState(false);
async function install(passwd: string) {
try {
setOpenInstall(false);
await installService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Installed Successfully"));
setServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
async function uninstall(passwd: string) {
try {
setOpenUninstall(false);
await uninstallService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Uninstalled Successfully"));
setUninstallServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setUninstallServiceLoading(false);
}
}
const onInstallOrEnableService = useLockFn(async () => {
setServiceLoading(true);
if (isUninstall) {
// install service
if (isWindows) {
await install("");
} else {
setOpenInstall(true);
}
} else {
try {
// enable or disable service
await patchVerge({ enable_service_mode: !isActive });
onChangeData({ enable_service_mode: !isActive });
await mutate();
setTimeout(() => {
mutate();
}, 2000);
setServiceLoading(false);
} catch (err: any) {
await mutate();
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
});
const onUninstallService = useLockFn(async () => {
setUninstallServiceLoading(true);
if (isWindows) {
await uninstall("");
} else {
setOpenUninstall(true);
}
});
return (
<>
{openInstall && <PasswordInput onConfirm={install} />}
{openUninstall && <PasswordInput onConfirm={uninstall} />}
<LoadingButton
size="small"
variant={isUninstall ? "outlined" : "contained"}
onClick={onInstallOrEnableService}
loading={serviceLoading}
>
{isActive ? t("Disable") : isInstalled ? t("Enable") : t("Install")}
</LoadingButton>
{isInstalled && (
<LoadingButton
size="small"
variant="outlined"
color="error"
sx={{ ml: 1 }}
onClick={onUninstallService}
loading={uninstallServiceLoaing}
>
{t("Uninstall")}
</LoadingButton>
)}
</>
);
};
@@ -1,129 +0,0 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Button, Stack, Typography } from "@mui/material";
import {
checkService,
installService,
uninstallService,
patchVergeConfig,
} from "@/services/cmds";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
interface Props {
enable: boolean;
}
export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
const { enable } = props;
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { data: status, mutate: mutateCheck } = useSWR(
"checkService",
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const state = status != null ? status : "pending";
const onInstall = useLockFn(async () => {
try {
await installService();
await mutateCheck();
setOpen(false);
setTimeout(() => {
mutateCheck();
}, 2000);
Notice.success(t("Service Installed Successfully"));
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
const onUninstall = useLockFn(async () => {
try {
if (enable) {
await patchVergeConfig({ enable_service_mode: false });
}
await uninstallService();
mutateCheck();
setOpen(false);
Notice.success(t("Service Uninstalled Successfully"));
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
// fix unhandled error of the service mode
const onDisable = useLockFn(async () => {
try {
await patchVergeConfig({ enable_service_mode: false });
mutateCheck();
setOpen(false);
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={t("Service Mode")}
contentSx={{ width: 360, userSelect: "text" }}
disableFooter
onClose={() => setOpen(false)}
>
<Typography>
{t("Current State")}: {t(state)}
</Typography>
{(state === "unknown" || state === "uninstall") && (
<Typography>
{t(
"Information: Please make sure that the Clash Verge Service is installed and enabled"
)}
</Typography>
)}
<Stack
direction="row"
spacing={1}
sx={{ mt: 4, justifyContent: "flex-end" }}
>
{state === "uninstall" && enable && (
<Button variant="contained" onClick={onDisable}>
{t("Disable Service Mode")}
</Button>
)}
{state === "uninstall" && (
<Button variant="contained" onClick={onInstall}>
{t("Install")}
</Button>
)}
{(state === "active" || state === "installed") && (
<Button variant="outlined" onClick={onUninstall}>
{t("Uninstall")}
</Button>
)}
</Stack>
</BaseDialog>
);
});
+57 -28
View File
@@ -1,4 +1,4 @@
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -14,7 +14,7 @@ import {
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds"; import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { Edit } from "@mui/icons-material"; import { EditRounded } from "@mui/icons-material";
import { EditorViewer } from "@/components/profile/editor-viewer"; import { EditorViewer } from "@/components/profile/editor-viewer";
import { BaseFieldset } from "@/components/base/base-fieldset"; import { BaseFieldset } from "@/components/base/base-fieldset";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
@@ -23,16 +23,43 @@ const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}`; }`;
/** NO_PROXY validation */
// *., cdn*., *, etc.
const domain_subdomain_part = String.raw`(?:[a-z0-9\-\*]+\.|\*)*`;
// .*, .cn, .moe, .co*, *
const domain_tld_part = String.raw`(?:\w{2,64}\*?|\*)`;
// *epicgames*, *skk.moe, *.skk.moe, skk.*, sponsor.cdn.skk.moe, *.*, etc.
// also matches 192.168.*, 10.*, 127.0.0.*, etc. (partial ipv4)
const rDomainSimple = domain_subdomain_part + domain_tld_part;
const ipv4_part = String.raw`\d{1,3}`;
const ipv6_part = "(?:[a-fA-F0-9:])+";
const rLocal = `localhost|<local>|localdomain`;
const getValidReg = (isWindows: boolean) => {
// 127.0.0.1 (full ipv4)
const rIPv4Unix = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}(?:\/\d{1,2})?`;
const rIPv4Windows = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`;
const rIPv6Unix = String.raw`(?:${ipv6_part}:+)+${ipv6_part}(?:\/\d{1,3})?`;
const rIPv6Windows = String.raw`(?:${ipv6_part}:+)+${ipv6_part}`;
const rValidPart = `${rDomainSimple}|${
isWindows ? rIPv4Windows : rIPv4Unix
}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`;
const separator = isWindows ? ";" : ",";
const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`;
return new RegExp(rValid);
};
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => { export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
let validReg; const isWindows = getSystem() === "windows";
if (getSystem() === "windows") { const validReg = useMemo(() => getValidReg(isWindows), [isWindows]);
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.?)+(local|test|example|invalid|localhost|onion|([a-zA-Z]{2,}))|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>)(;((\*\.)?([a-zA-Z0-9-]+\.?)+(local|test|example|invalid|localhost|onion|([a-zA-Z]{2,}))|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>))*;?$/;
} else {
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.?)+(local|test|example|invalid|localhost|onion|([a-zA-Z]{2,}))|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>)(,((\*\.)?([a-zA-Z0-9-]+\.?)+(local|test|example|invalid|localhost|onion|([a-zA-Z]{2,}))|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>))*,?$/;
}
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editorOpen, setEditorOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false);
@@ -188,7 +215,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} /> <ListItemText primary={t("Guard Duration")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={!enabled} disabled={!enabled}
size="small" size="small"
value={value.duration} value={value.duration}
@@ -219,7 +246,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<> <>
<ListItemText primary={t("Proxy Bypass")} /> <ListItemText primary={t("Proxy Bypass")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
error={value.bypass ? !validReg.test(value.bypass) : false} error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled} disabled={!enabled}
size="small" size="small"
@@ -234,7 +261,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItemText primary={t("Bypass")} /> <ListItemText primary={t("Bypass")} />
<FlexBox> <FlexBox>
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={true} disabled={true}
size="small" size="small"
multiline multiline
@@ -253,7 +280,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ padding: "3px 0" }} sx={{ padding: "3px 0" }}
/> />
<Button <Button
startIcon={<Edit />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setEditorOpen(true); setEditorOpen(true);
@@ -261,20 +288,22 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
> >
{t("Edit")} PAC {t("Edit")} PAC
</Button> </Button>
<EditorViewer {editorOpen && (
open={editorOpen} <EditorViewer
title={`${t("Edit")} PAC`} open={true}
initialData={Promise.resolve(value.pac_content ?? "")} title={`${t("Edit")} PAC`}
language="javascript" initialData={Promise.resolve(value.pac_content ?? "")}
onSave={(_prev, curr) => { language="javascript"
let pac = DEFAULT_PAC; onSave={(_prev, curr) => {
if (curr && curr.trim().length > 0) { let pac = DEFAULT_PAC;
pac = curr; if (curr && curr.trim().length > 0) {
} pac = curr;
setValue((v) => ({ ...v, pac_content: pac })); }
}} setValue((v) => ({ ...v, pac_content: pac }));
onClose={() => setEditorOpen(false)} }}
/> onClose={() => setEditorOpen(false)}
/>
)}
</ListItem> </ListItem>
</> </>
)} )}
+17 -15
View File
@@ -14,7 +14,7 @@ import { useVerge } from "@/hooks/use-verge";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer"; import { EditorViewer } from "@/components/profile/editor-viewer";
import { Edit } from "@mui/icons-material"; import { EditRounded } from "@mui/icons-material";
export const ThemeViewer = forwardRef<DialogRef>((props, ref) => { export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -115,7 +115,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
<Item> <Item>
<ListItemText primary={t("CSS Injection")} /> <ListItemText primary={t("CSS Injection")} />
<Button <Button
startIcon={<Edit />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setEditorOpen(true); setEditorOpen(true);
@@ -123,19 +123,21 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
> >
{t("Edit")} CSS {t("Edit")} CSS
</Button> </Button>
<EditorViewer {editorOpen && (
open={editorOpen} <EditorViewer
title={`${t("Edit")} CSS`} open={true}
initialData={Promise.resolve(theme.css_injection ?? "")} title={`${t("Edit")} CSS`}
language="css" initialData={Promise.resolve(theme.css_injection ?? "")}
onSave={(_prev, curr) => { language="css"
theme.css_injection = curr; onSave={(_prev, curr) => {
handleChange("css_injection"); theme.css_injection = curr;
}} handleChange("css_injection");
onClose={() => { }}
setEditorOpen(false); onClose={() => {
}} setEditorOpen(false);
/> }}
/>
)}
</Item> </Item>
</List> </List>
</BaseDialog> </BaseDialog>
+3 -3
View File
@@ -144,7 +144,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} /> <ListItemText primary={t("Device")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -190,7 +190,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} /> <ListItemText primary={t("DNS Hijack")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -207,7 +207,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} /> <ListItemText primary={t("MTU")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
type="number" type="number"
autoCorrect="off" autoCorrect="off"
+1 -1
View File
@@ -43,7 +43,7 @@ export const WebUIItem = (props: Props) => {
<> <>
<Stack spacing={0.75} direction="row" mt={1} mb={1} alignItems="center"> <Stack spacing={0.75} direction="row" mt={1} mb={1} alignItems="center">
<TextField <TextField
autoComplete="off" autoComplete="new-password"
fullWidth fullWidth
size="small" size="small"
value={editValue} value={editValue}
@@ -24,7 +24,7 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
const webUIList = verge?.web_ui_list || [ const webUIList = verge?.web_ui_list || [
"https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret", "https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
"https://yacd.metacubex.one/?host=%host&port=%port&secret=%secret", "https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret",
]; ];
const handleAdd = useLockFn(async (value: string) => { const handleAdd = useLockFn(async (value: string) => {
+24 -5
View File
@@ -2,7 +2,11 @@ import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TextField, Select, MenuItem, Typography } from "@mui/material"; import { TextField, Select, MenuItem, Typography } from "@mui/material";
import { Settings, Shuffle } from "@mui/icons-material"; import {
SettingsRounded,
ShuffleRounded,
LanRounded,
} from "@mui/icons-material";
import { DialogRef, Notice, Switch } from "@/components/base"; import { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
@@ -16,6 +20,7 @@ import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api"; import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
const isWIN = getSystem() === "windows"; const isWIN = getSystem() === "windows";
@@ -37,6 +42,7 @@ const SettingClash = ({ onError }: Props) => {
const portRef = useRef<DialogRef>(null); const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null); const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null); const coreRef = useRef<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
@@ -60,8 +66,21 @@ const SettingClash = ({ onError }: Props) => {
<ClashPortViewer ref={portRef} /> <ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} /> <ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} /> <ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<SettingItem label={t("Allow Lan")}> <SettingItem
label={t("Allow Lan")}
extra={
<TooltipIcon
title={t("Network Interface")}
color={"inherit"}
icon={LanRounded}
onClick={() => {
networkRef.current?.open();
}}
/>
}
>
<GuardState <GuardState
value={allowLan ?? false} value={allowLan ?? false}
valueProps="checked" valueProps="checked"
@@ -112,7 +131,7 @@ const SettingClash = ({ onError }: Props) => {
<TooltipIcon <TooltipIcon
title={t("Random Port")} title={t("Random Port")}
color={enable_random_port ? "primary" : "inherit"} color={enable_random_port ? "primary" : "inherit"}
icon={Shuffle} icon={ShuffleRounded}
onClick={() => { onClick={() => {
Notice.success( Notice.success(
t("Restart Application to Apply Modifications"), t("Restart Application to Apply Modifications"),
@@ -125,7 +144,7 @@ const SettingClash = ({ onError }: Props) => {
} }
> >
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={enable_random_port} disabled={enable_random_port}
size="small" size="small"
value={verge_mixed_port ?? 7897} value={verge_mixed_port ?? 7897}
@@ -148,7 +167,7 @@ const SettingClash = ({ onError }: Props) => {
label={t("Clash Core")} label={t("Clash Core")}
extra={ extra={
<TooltipIcon <TooltipIcon
icon={Settings} icon={SettingsRounded}
onClick={() => coreRef.current?.open()} onClick={() => coreRef.current?.open()}
/> />
} }
+36 -41
View File
@@ -1,13 +1,13 @@
import useSWR from "swr"; import useSWR from "swr";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PrivacyTipRounded, Settings } from "@mui/icons-material"; import { SettingsRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds"; import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Switch } from "@/components/base"; import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { ServiceViewer } from "./mods/service-viewer"; import { ServiceSwitcher } from "./mods/service-switcher";
import { SysproxyViewer } from "./mods/sysproxy-viewer"; import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer"; import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@@ -20,22 +20,23 @@ const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
// service mode // service mode
const { data: serviceStatus } = useSWR("checkService", checkService, { const { data: serviceStatus, mutate: mutateServiceStatus } = useSWR(
revalidateIfStale: false, "checkService",
shouldRetryOnError: false, checkService,
focusThrottleInterval: 36e5, // 1 hour {
}); revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null); const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null); const tunRef = useRef<DialogRef>(null);
const { const {
enable_tun_mode, enable_tun_mode,
enable_auto_launch, enable_auto_launch,
enable_service_mode,
enable_silent_start, enable_silent_start,
enable_system_proxy, enable_system_proxy,
} = verge ?? {}; } = verge ?? {};
@@ -49,14 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}> <SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} /> <SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} /> <TunViewer ref={tunRef} />
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<SettingItem <SettingItem
label={t("Tun Mode")} label={t("Tun Mode")}
extra={ extra={
<TooltipIcon <TooltipIcon
title={t("Tun Mode Info")} title={t("Tun Mode Info")}
icon={Settings} icon={SettingsRounded}
onClick={() => tunRef.current?.open()} onClick={() => tunRef.current?.open()}
/> />
} }
@@ -66,38 +66,33 @@ const SettingSystem = ({ onError }: Props) => {
valueProps="checked" valueProps="checked"
onCatch={onError} onCatch={onError}
onFormat={onSwitchFormat} onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tun_mode: e })} onChange={(e) => {
onGuard={(e) => patchVerge({ enable_tun_mode: e })} if (serviceStatus !== "active") {
onChangeData({ enable_tun_mode: false });
} else {
onChangeData({ enable_tun_mode: e });
}
}}
onGuard={(e) => {
if (serviceStatus !== "active" && e) {
Notice.error(t("Please Enable Service Mode"));
return Promise.resolve();
} else {
return patchVerge({ enable_tun_mode: e });
}
}}
> >
<Switch edge="end" /> <Switch edge="end" />
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem <SettingItem label={t("Service Mode")}>
label={t("Service Mode")} <ServiceSwitcher
extra={ status={serviceStatus ?? "unknown"}
<TooltipIcon mutate={mutateServiceStatus}
title={t("Service Mode Info")} patchVerge={patchVerge}
icon={PrivacyTipRounded} onChangeData={onChangeData}
onClick={() => serviceRef.current?.open()} />
/>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
/>
</GuardState>
</SettingItem> </SettingItem>
<SettingItem <SettingItem
@@ -106,7 +101,7 @@ const SettingSystem = ({ onError }: Props) => {
<> <>
<TooltipIcon <TooltipIcon
title={t("System Proxy Info")} title={t("System Proxy Info")}
icon={Settings} icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()} onClick={() => sysproxyRef.current?.open()}
/> />
</> </>
+23 -3
View File
@@ -1,13 +1,21 @@
import { useRef } from "react"; import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/api/dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material"; import {
Button,
MenuItem,
Select,
Input,
Typography,
Box,
} from "@mui/material";
import { import {
exitApp, exitApp,
openAppDir, openAppDir,
openCoreDir, openCoreDir,
openLogsDir, openLogsDir,
openDevTools, openDevTools,
copyClashEnv,
} from "@/services/cmds"; } from "@/services/cmds";
import { checkUpdate } from "@tauri-apps/api/updater"; import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -24,6 +32,8 @@ import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material";
interface Props { interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
@@ -67,6 +77,11 @@ const SettingVerge = ({ onError }: Props) => {
} }
}; };
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
Notice.success(t("Copy Success"), 1000);
}, []);
return ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Setting")}>
<ThemeViewer ref={themeRef} /> <ThemeViewer ref={themeRef} />
@@ -123,7 +138,12 @@ const SettingVerge = ({ onError }: Props) => {
</SettingItem> </SettingItem>
)} )}
<SettingItem label={t("Copy Env Type")}> <SettingItem
label={t("Copy Env Type")}
extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
}
>
<GuardState <GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")} value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError} onCatch={onError}
+15 -9
View File
@@ -13,7 +13,7 @@ import {
alpha, alpha,
} from "@mui/material"; } from "@mui/material";
import { BaseLoading } from "@/components/base"; import { BaseLoading } from "@/components/base";
import { LanguageTwoTone } from "@mui/icons-material"; import { LanguageRounded } from "@mui/icons-material";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { TestBox } from "./test-box"; import { TestBox } from "./test-box";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
@@ -28,12 +28,18 @@ interface Props {
onDelete: (uid: string) => void; onDelete: (uid: string) => void;
} }
let eventListener: UnlistenFn | null = null; let eventListener: UnlistenFn = () => {};
export const TestItem = (props: Props) => { export const TestItem = (props: Props) => {
const { itemData, onEdit, onDelete: onDeleteItem } = props; const { itemData, onEdit, onDelete: onDeleteItem } = props;
const { attributes, listeners, setNodeRef, transform, transition } = const {
useSortable({ id: props.id }); attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -84,9 +90,7 @@ export const TestItem = (props: Props) => {
]; ];
const listenTsetEvent = async () => { const listenTsetEvent = async () => {
if (eventListener !== null) { eventListener();
eventListener();
}
eventListener = await listen("verge://test-all", () => { eventListener = await listen("verge://test-all", () => {
onDelay(); onDelay();
}); });
@@ -94,13 +98,15 @@ export const TestItem = (props: Props) => {
useEffect(() => { useEffect(() => {
listenTsetEvent(); listenTsetEvent();
}, []); }, [url]);
return ( return (
<Box <Box
sx={{ sx={{
position: "relative",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}} }}
> >
<TestBox <TestBox
@@ -138,7 +144,7 @@ export const TestItem = (props: Props) => {
</Box> </Box>
) : ( ) : (
<Box sx={{ display: "flex", justifyContent: "center" }}> <Box sx={{ display: "flex", justifyContent: "center" }}>
<LanguageTwoTone sx={{ height: "40px" }} fontSize="large" /> <LanguageRounded sx={{ height: "40px" }} fontSize="large" />
</Box> </Box>
)} )}
+16 -3
View File
@@ -1,5 +1,6 @@
{ {
"millis": "millis", "millis": "millis",
"seconds": "seconds",
"mins": "mins", "mins": "mins",
"Back": "Back", "Back": "Back",
"Close": "Close", "Close": "Close",
@@ -54,7 +55,7 @@
"Create Profile": "Create Profile", "Create Profile": "Create Profile",
"Edit Profile": "Edit Profile", "Edit Profile": "Edit Profile",
"Edit Proxies": "Edit Proxies", "Edit Proxies": "Edit Proxies",
"Use newlines for multiple uri": "Use newlines for multiple uri", "Use newlines for multiple uri": "Use newlines for multiple uri(Base64 encoding supported)",
"Edit Rules": "Edit Rules", "Edit Rules": "Edit Rules",
"Rule Type": "Rule Type", "Rule Type": "Rule Type",
"Rule Content": "Rule Content", "Rule Content": "Rule Content",
@@ -109,10 +110,16 @@
"PASS": "Skips this rule when matched", "PASS": "Skips this rule when matched",
"Edit Groups": "Edit Proxy Groups", "Edit Groups": "Edit Proxy Groups",
"Group Type": "Group Type", "Group Type": "Group Type",
"select": "Select proxy manually",
"url-test": "Select proxy based on URL test delay",
"fallback": "Switch to another proxy on error",
"load-balance": "Distribute proxy based on load balancing",
"relay": "Pass through the defined proxy chain",
"Group Name": "Group Name", "Group Name": "Group Name",
"Use Proxies": "Use Proxies", "Use Proxies": "Use Proxies",
"Use Provider": "Use Provider", "Use Provider": "Use Provider",
"Health Check Url": "Health Check Url", "Health Check Url": "Health Check Url",
"Expected Status": "Expected Status",
"Interval": "Interval", "Interval": "Interval",
"Lazy": "Lazy", "Lazy": "Lazy",
"Timeout": "Timeout", "Timeout": "Timeout",
@@ -124,9 +131,10 @@
"Include All Proxies": "Include All Proxies", "Include All Proxies": "Include All Proxies",
"Exclude Filter": "Exclude Filter", "Exclude Filter": "Exclude Filter",
"Exclude Type": "Exclude Type", "Exclude Type": "Exclude Type",
"Expected Status": "Expected Status",
"Disable UDP": "Disable UDP", "Disable UDP": "Disable UDP",
"Hidden": "Hidden", "Hidden": "Hidden",
"Group Name Required": "Group Name Required",
"Group Name Already Exists": "Group Name Already Exists",
"Extend Config": "Extend Config", "Extend Config": "Extend Config",
"Extend Script": "Extend Script", "Extend Script": "Extend Script",
"Global Merge": "Global Extend Config", "Global Merge": "Global Extend Config",
@@ -234,6 +242,9 @@
"Github Repo": "Github Repo", "Github Repo": "Github Repo",
"Clash Setting": "Clash Setting", "Clash Setting": "Clash Setting",
"Allow Lan": "Allow LAN", "Allow Lan": "Allow LAN",
"Network Interface": "Network Interface",
"Ip Address": "IP Address",
"Mac Address": "MAC Address",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Log Level", "Log Level": "Log Level",
"Port Config": "Port Config", "Port Config": "Port Config",
@@ -255,7 +266,8 @@
"Restart": "Restart", "Restart": "Restart",
"Release Version": "Release Version", "Release Version": "Release Version",
"Alpha Version": "Alpha Version", "Alpha Version": "Alpha Version",
"Tun mode requires": "Tun mode requires", "Please Enable Service Mode": "Please Install and Enable Service Mode First",
"Please enter your root password": "Please enter your root password",
"Grant": "Grant", "Grant": "Grant",
"Open UWP tool": "Open UWP tool", "Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction", "Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
@@ -269,6 +281,7 @@
"Tray Click Event": "Tray Click Event", "Tray Click Event": "Tray Click Event",
"Show Main Window": "Show Main Window", "Show Main Window": "Show Main Window",
"Copy Env Type": "Copy Env Type", "Copy Env Type": "Copy Env Type",
"Copy Success": "Copy Success",
"Start Page": "Start Page", "Start Page": "Start Page",
"Startup Script": "Startup Script", "Startup Script": "Startup Script",
"Browse": "Browse", "Browse": "Browse",
+16 -3
View File
@@ -1,5 +1,6 @@
{ {
"millis": "میلی‌ثانیه", "millis": "میلی‌ثانیه",
"seconds": "ثانیه‌ها",
"mins": "دقیقه", "mins": "دقیقه",
"Back": "بازگشت", "Back": "بازگشت",
"Close": "بستن", "Close": "بستن",
@@ -54,7 +55,7 @@
"Create Profile": "ایجاد پروفایل", "Create Profile": "ایجاد پروفایل",
"Edit Profile": "ویرایش پروفایل", "Edit Profile": "ویرایش پروفایل",
"Edit Proxies": "ویرایش پروکسی‌ها", "Edit Proxies": "ویرایش پروکسی‌ها",
"Use newlines for multiple uri": "استفاده از خطوط جدید برای چندین آدرس", "Use newlines for multiple uri": "استفاده از خطوط جدید برای چندین آدرس (پشتیبانی از رمزگذاری Base64)",
"Edit Rules": "ویرایش قوانین", "Edit Rules": "ویرایش قوانین",
"Rule Type": "نوع قانون", "Rule Type": "نوع قانون",
"Rule Content": "محتوای قانون", "Rule Content": "محتوای قانون",
@@ -107,10 +108,16 @@
"PASS": "این قانون را در صورت تطابق نادیده می‌گیرد", "PASS": "این قانون را در صورت تطابق نادیده می‌گیرد",
"Edit Groups": "ویرایش گروه‌های پروکسی", "Edit Groups": "ویرایش گروه‌های پروکسی",
"Group Type": "نوع گروه", "Group Type": "نوع گروه",
"select": "انتخاب پروکسی به صورت دستی",
"url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL",
"fallback": "تعویض به پروکسی دیگر در صورت بروز خطا",
"load-balance": "توزیع پروکسی بر اساس توازن بار",
"relay": "عبور از زنجیره پروکسی تعریف شده",
"Group Name": "نام گروه", "Group Name": "نام گروه",
"Use Proxies": "استفاده از پروکسی‌ها", "Use Proxies": "استفاده از پروکسی‌ها",
"Use Provider": "استفاده از ارائه‌دهنده", "Use Provider": "استفاده از ارائه‌دهنده",
"Health Check Url": "آدرس بررسی سلامت", "Health Check Url": "آدرس بررسی سلامت",
"Expected Status": "وضعیت مورد انتظار",
"Interval": "فاصله زمانی", "Interval": "فاصله زمانی",
"Lazy": "تنبل", "Lazy": "تنبل",
"Timeout": "زمان قطع", "Timeout": "زمان قطع",
@@ -122,9 +129,10 @@
"Include All Proxies": "شامل همه پروکسی‌ها", "Include All Proxies": "شامل همه پروکسی‌ها",
"Exclude Filter": "فیلتر استثناء", "Exclude Filter": "فیلتر استثناء",
"Exclude Type": "نوع استثناء", "Exclude Type": "نوع استثناء",
"Expected Status": "وضعیت مورد انتظار",
"Disable UDP": "غیرفعال کردن UDP", "Disable UDP": "غیرفعال کردن UDP",
"Hidden": "مخفی", "Hidden": "مخفی",
"Group Name Required": "نام گروه مورد نیاز است",
"Group Name Already Exists": "نام گروه قبلا وجود دارد",
"Extend Config": "توسعه پیکربندی", "Extend Config": "توسعه پیکربندی",
"Extend Script": "ادغام اسکریپت", "Extend Script": "ادغام اسکریپت",
"Global Merge": "تنظیمات گسترده‌ی سراسری", "Global Merge": "تنظیمات گسترده‌ی سراسری",
@@ -229,6 +237,9 @@
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید", "Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash", "Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN", "Allow Lan": "اجازه LAN",
"Network Interface": "رابط شبکه",
"Ip Address": "آدرس IP",
"Mac Address": "آدرس MAC",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "سطح لاگ", "Log Level": "سطح لاگ",
"Port Config": "پیکربندی پورت", "Port Config": "پیکربندی پورت",
@@ -250,7 +261,8 @@
"Restart": "راه‌اندازی مجدد", "Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی", "Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا", "Alpha Version": "نسخه آلفا",
"Tun mode requires": "Tun mode نیاز دارد", "Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
"Grant": "اعطا", "Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP", "Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود", "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
@@ -267,6 +279,7 @@
"Tray Click Event": "رویداد کلیک در سینی سیستم", "Tray Click Event": "رویداد کلیک در سینی سیستم",
"Show Main Window": "نمایش پنجره اصلی", "Show Main Window": "نمایش پنجره اصلی",
"Copy Env Type": "کپی نوع محیط", "Copy Env Type": "کپی نوع محیط",
"Copy Success": "کپی با موفقیت انجام شد",
"Start Page": "صفحه شروع", "Start Page": "صفحه شروع",
"Startup Script": "اسکریپت راه‌اندازی", "Startup Script": "اسکریپت راه‌اندازی",
"Browse": "مرور کردن", "Browse": "مرور کردن",
+16 -3
View File
@@ -1,5 +1,6 @@
{ {
"millis": "миллисекунды", "millis": "миллисекунды",
"seconds": "секунды",
"mins": "минуты", "mins": "минуты",
"Back": "Назад", "Back": "Назад",
"Close": "Закрыть", "Close": "Закрыть",
@@ -54,7 +55,7 @@
"Create Profile": "Создать профиль", "Create Profile": "Создать профиль",
"Edit Profile": "Изменить профиль", "Edit Profile": "Изменить профиль",
"Edit Proxies": "Редактировать прокси", "Edit Proxies": "Редактировать прокси",
"Use newlines for multiple uri": "Используйте новые строки для нескольких URI", "Use newlines for multiple uri": "Используйте символы новой строки для нескольких URI (поддерживается кодировка Base64)",
"Edit Rules": "Редактировать правила", "Edit Rules": "Редактировать правила",
"Rule Type": "Тип правила", "Rule Type": "Тип правила",
"Rule Content": "Содержимое правила", "Rule Content": "Содержимое правила",
@@ -107,10 +108,16 @@
"PASS": "Пропускает это правило при совпадении", "PASS": "Пропускает это правило при совпадении",
"Edit Groups": "Редактировать группы прокси", "Edit Groups": "Редактировать группы прокси",
"Group Type": "Тип группы", "Group Type": "Тип группы",
"select": "Выбор прокси вручную",
"url-test": "Выбор прокси на основе задержки теста URL",
"fallback": "Переключение на другой прокси при ошибке",
"load-balance": "Распределение прокси на основе балансировки нагрузки",
"relay": "Передача через определенную цепочку прокси",
"Group Name": "Имя группы", "Group Name": "Имя группы",
"Use Proxies": "Использовать прокси", "Use Proxies": "Использовать прокси",
"Use Provider": "Использовать провайдера", "Use Provider": "Использовать провайдера",
"Health Check Url": "URL проверки здоровья", "Health Check Url": "URL проверки здоровья",
"Expected Status": "Ожидаемый статус",
"Interval": "Интервал", "Interval": "Интервал",
"Lazy": "Ленивый", "Lazy": "Ленивый",
"Timeout": "Таймаут", "Timeout": "Таймаут",
@@ -122,9 +129,10 @@
"Include All Proxies": "Включить все прокси", "Include All Proxies": "Включить все прокси",
"Exclude Filter": "Исключить фильтр", "Exclude Filter": "Исключить фильтр",
"Exclude Type": "Тип исключения", "Exclude Type": "Тип исключения",
"Expected Status": "Ожидаемый статус",
"Disable UDP": "Отключить UDP", "Disable UDP": "Отключить UDP",
"Hidden": "Скрытый", "Hidden": "Скрытый",
"Group Name Required": "Требуется имя группы",
"Group Name Already Exists": "Имя группы уже существует",
"Extend Config": "Изменить Merge.", "Extend Config": "Изменить Merge.",
"Extend Script": "Изменить Script", "Extend Script": "Изменить Script",
"Global Merge": "Глобальный расширенный Настройки", "Global Merge": "Глобальный расширенный Настройки",
@@ -232,6 +240,9 @@
"Github Repo": "GitHub репозиторий", "Github Repo": "GitHub репозиторий",
"Clash Setting": "Настройки Clash", "Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть", "Allow Lan": "Разрешить локальную сеть",
"Network Interface": "Сетевой интерфейс",
"Ip Address": "IP адрес",
"Mac Address": "MAC адрес",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Уровень логов", "Log Level": "Уровень логов",
"Port Config": "Настройка порта", "Port Config": "Настройка порта",
@@ -253,7 +264,8 @@
"Restart": "Перезапуск", "Restart": "Перезапуск",
"Release Version": "Официальная версия", "Release Version": "Официальная версия",
"Alpha Version": "Альфа-версия", "Alpha Version": "Альфа-версия",
"Tun mode requires": "Требуется Режим туннеля", "Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
"Grant": "Предоставить", "Grant": "Предоставить",
"Open UWP tool": "Открыть UWP инструмент", "Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение", "Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
@@ -267,6 +279,7 @@
"Tray Click Event": "Событие щелчка в лотке", "Tray Click Event": "Событие щелчка в лотке",
"Show Main Window": "Показать главное окно", "Show Main Window": "Показать главное окно",
"Copy Env Type": "Скопировать тип Env", "Copy Env Type": "Скопировать тип Env",
"Copy Success": "Скопировано",
"Start Page": "Главная страница", "Start Page": "Главная страница",
"Startup Script": "Скрипт запуска", "Startup Script": "Скрипт запуска",
"Browse": "Просмотреть", "Browse": "Просмотреть",
+18 -5
View File
@@ -1,6 +1,7 @@
{ {
"millis": "毫秒", "millis": "毫秒",
"mins": "分钟", "mins": "分钟",
"seconds": "秒",
"Back": "返回", "Back": "返回",
"Close": "关闭", "Close": "关闭",
"Cancel": "取消", "Cancel": "取消",
@@ -54,7 +55,7 @@
"Create Profile": "新建配置", "Create Profile": "新建配置",
"Edit Profile": "编辑配置", "Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点", "Edit Proxies": "编辑节点",
"Use newlines for multiple uri": "多条URI请使用换行分隔", "Use newlines for multiple uri": "多条URI请使用换行分隔(支持Base64编码)",
"Edit Rules": "编辑规则", "Edit Rules": "编辑规则",
"Rule Type": "规则类型", "Rule Type": "规则类型",
"Rule Content": "规则内容", "Rule Content": "规则内容",
@@ -109,24 +110,31 @@
"PASS": "跳过此规则", "PASS": "跳过此规则",
"Edit Groups": "编辑代理组", "Edit Groups": "编辑代理组",
"Group Type": "代理组类型", "Group Type": "代理组类型",
"select": "手动选择代理",
"url-test": "根据URL测试延迟选择代理",
"fallback": "不可用时切换到另一个代理",
"load-balance": "根据负载均衡分配代理",
"relay": "根据定义的代理链传递",
"Group Name": "代理组组名", "Group Name": "代理组组名",
"Use Proxies": "引入代理", "Use Proxies": "引入代理",
"Use Provider": "引入代理集合", "Use Provider": "引入代理集合",
"Health Check Url": "健康检查测试地址", "Health Check Url": "健康检查测试地址",
"Expected Status": "期望状态码",
"Interval": "检查间隔", "Interval": "检查间隔",
"Lazy": "懒惰状态", "Lazy": "懒惰状态",
"Timeout": "超时时间", "Timeout": "超时时间",
"Max Failed Times": "最大失败次数", "Max Failed Times": "最大失败次数",
"Interface Name": "出站接口", "Interface Name": "出站接口",
"Routing Mark": "路由标记", "Routing Mark": "路由标记",
"Include All": "引入所有出站代理以及代理集合", "Include All": "引入所有出站代理、代理集合",
"Include All Providers": "引入所有代理集合", "Include All Providers": "引入所有代理集合",
"Include All Proxies": "引入所有出站代理", "Include All Proxies": "引入所有出站代理",
"Exclude Filter": "排除节点", "Exclude Filter": "排除节点",
"Exclude Type": "排除节点类型", "Exclude Type": "排除节点类型",
"Expected Status": "期望状态码",
"Disable UDP": "禁用UDP", "Disable UDP": "禁用UDP",
"Hidden": "隐藏该组", "Hidden": "隐藏代理组",
"Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置", "Extend Config": "扩展配置",
"Extend Script": "扩展脚本", "Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置", "Global Merge": "全局扩展配置",
@@ -234,6 +242,9 @@
"Github Repo": "GitHub 项目地址", "Github Repo": "GitHub 项目地址",
"Clash Setting": "Clash 设置", "Clash Setting": "Clash 设置",
"Allow Lan": "局域网连接", "Allow Lan": "局域网连接",
"Network Interface": "网络接口",
"Ip Address": "IP 地址",
"Mac Address": "MAC 地址",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "日志等级", "Log Level": "日志等级",
"Port Config": "端口设置", "Port Config": "端口设置",
@@ -255,7 +266,8 @@
"Restart": "重启内核", "Restart": "重启内核",
"Release Version": "正式版", "Release Version": "正式版",
"Alpha Version": "预览版", "Alpha Version": "预览版",
"Tun mode requires": "如需启用 Tun 模式需要授权", "Please Enable Service Mode": "请先安装并启用服务模式",
"Please enter your root password": "请输入您的 root 密码",
"Grant": "授权", "Grant": "授权",
"Open UWP tool": "UWP 工具", "Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制", "Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
@@ -269,6 +281,7 @@
"Tray Click Event": "托盘点击事件", "Tray Click Event": "托盘点击事件",
"Show Main Window": "显示主窗口", "Show Main Window": "显示主窗口",
"Copy Env Type": "复制环境变量类型", "Copy Env Type": "复制环境变量类型",
"Copy Success": "复制成功",
"Start Page": "启动页面", "Start Page": "启动页面",
"Startup Script": "启动脚本", "Startup Script": "启动脚本",
"Browse": "浏览", "Browse": "浏览",
+5 -4
View File
@@ -137,7 +137,7 @@ const ProfilePage = () => {
mutate("getProfiles", newProfiles); mutate("getProfiles", newProfiles);
const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
if (!newProfiles.current && remoteItem) { if (newProfiles.current && remoteItem) {
const current = remoteItem.uid; const current = remoteItem.uid;
await patchProfiles({ current }); await patchProfiles({ current });
mutateLogs(); mutateLogs();
@@ -171,10 +171,11 @@ const ProfilePage = () => {
}, 100); }, 100);
try { try {
await patchProfiles({ current }); await patchProfiles({ current });
mutateLogs(); await mutateLogs();
closeAllConnections(); closeAllConnections();
setTimeout(() => activateSelected(), 2000); activateSelected().then(() => {
Notice.success(t("Profile Switched"), 1000); Notice.success(t("Profile Switched"), 1000);
});
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err.toString(), 4000); Notice.error(err?.message || err.toString(), 4000);
} finally { } finally {
+2 -2
View File
@@ -2,7 +2,7 @@ import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base"; import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineSharp, Telegram } from "@mui/icons-material"; import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds"; import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge"; import SettingVerge from "@/components/setting/setting-verge";
import SettingClash from "@/components/setting/setting-clash"; import SettingClash from "@/components/setting/setting-clash";
@@ -42,7 +42,7 @@ const SettingPage = () => {
title={t("Manual")} title={t("Manual")}
onClick={toGithubDoc} onClick={toGithubDoc}
> >
<HelpOutlineSharp fontSize="inherit" /> <HelpOutlineRounded fontSize="inherit" />
</IconButton> </IconButton>
<IconButton <IconButton
size="medium" size="medium"
+16 -4
View File
@@ -2,6 +2,10 @@ import dayjs from "dayjs";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
export async function copyClashEnv() {
return invoke<void>("copy_clash_env");
}
export async function getClashLogs() { export async function getClashLogs() {
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/; const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
const newRegex = /(.+?)\s+(.+?)\s+(.+)/; const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
@@ -197,12 +201,12 @@ export async function checkService() {
} }
} }
export async function installService() { export async function installService(passwd: string) {
return invoke<void>("install_service"); return invoke<void>("install_service", { passwd });
} }
export async function uninstallService() { export async function uninstallService(passwd: string) {
return invoke<void>("uninstall_service"); return invoke<void>("uninstall_service", { passwd });
} }
export async function invoke_uwp_tool() { export async function invoke_uwp_tool() {
@@ -233,3 +237,11 @@ export async function copyIconFile(
export async function downloadIconCache(url: string, name: string) { export async function downloadIconCache(url: string, name: string) {
return invoke<string>("download_icon_cache", { url, name }); return invoke<string>("download_icon_cache", { url, name });
} }
export async function getNetworkInterfaces() {
return invoke<string[]>("get_network_interfaces");
}
export async function getNetworkInterfacesInfo() {
return invoke<INetworkInterface[]>("get_network_interfaces_info");
}
+34 -6
View File
@@ -59,6 +59,7 @@ interface IProxyItem {
time: string; time: string;
delay: number; delay: number;
}[]; }[];
testUrl?: string;
all?: string[]; all?: string[];
now?: string; now?: string;
hidden?: boolean; hidden?: boolean;
@@ -197,6 +198,24 @@ interface IVergeTestItem {
icon?: string; icon?: string;
url: string; url: string;
} }
interface IAddress {
V4?: {
ip: string;
broadcast?: string;
netmask?: string;
};
V6?: {
ip: string;
broadcast?: string;
netmask?: string;
};
}
interface INetworkInterface {
name: string;
addr: IAddress[];
mac_addr?: string;
index: number;
}
interface ISeqProfileConfig { interface ISeqProfileConfig {
prepend: []; prepend: [];
@@ -260,8 +279,17 @@ interface RealityOptions {
"public-key"?: string; "public-key"?: string;
"short-id"?: string; "short-id"?: string;
} }
type ClientFingerprint =
type NetworkType = "ws" | "http" | "h2" | "grpc"; | "chrome"
| "firefox"
| "safari"
| "iOS"
| "android"
| "edge"
| "360"
| "qq"
| "random";
type NetworkType = "ws" | "http" | "h2" | "grpc" | "tcp";
type CipherType = type CipherType =
| "none" | "none"
| "auto" | "auto"
@@ -376,7 +404,7 @@ interface IProxyTrojanConfig extends IProxyBaseConfig {
method?: string; method?: string;
password?: string; password?: string;
}; };
"client-fingerprint"?: string; "client-fingerprint"?: ClientFingerprint;
} }
// tuic // tuic
interface IProxyTuicConfig extends IProxyBaseConfig { interface IProxyTuicConfig extends IProxyBaseConfig {
@@ -438,7 +466,7 @@ interface IProxyVlessConfig extends IProxyBaseConfig {
"skip-cert-verify"?: boolean; "skip-cert-verify"?: boolean;
fingerprint?: string; fingerprint?: string;
servername?: string; servername?: string;
"client-fingerprint"?: string; "client-fingerprint"?: ClientFingerprint;
} }
// vmess // vmess
interface IProxyVmessConfig extends IProxyBaseConfig { interface IProxyVmessConfig extends IProxyBaseConfig {
@@ -466,7 +494,7 @@ interface IProxyVmessConfig extends IProxyBaseConfig {
"packet-encoding"?: string; "packet-encoding"?: string;
"global-padding"?: boolean; "global-padding"?: boolean;
"authenticated-length"?: boolean; "authenticated-length"?: boolean;
"client-fingerprint"?: string; "client-fingerprint"?: ClientFingerprint;
} }
interface WireGuardPeerOptions { interface WireGuardPeerOptions {
server?: string; server?: string;
@@ -574,7 +602,7 @@ interface IProxyShadowsocksConfig extends IProxyBaseConfig {
}; };
"udp-over-tcp"?: boolean; "udp-over-tcp"?: boolean;
"udp-over-tcp-version"?: number; "udp-over-tcp-version"?: number;
"client-fingerprint"?: string; "client-fingerprint"?: ClientFingerprint;
} }
// shadowsocksR // shadowsocksR
interface IProxyshadowsocksRConfig extends IProxyBaseConfig { interface IProxyshadowsocksRConfig extends IProxyBaseConfig {
+12
View File
@@ -0,0 +1,12 @@
export default function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
): T {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func.apply(this, args), wait);
} as T;
}
-131
View File
@@ -1,131 +0,0 @@
// global initializer
{{
function $set(obj, path, value) {
if (Object(obj) !== obj) return obj;
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path
.slice(0, -1)
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
path[path.length - 1]
] = value;
return obj;
}
function toBool(str) {
if (typeof str === 'undefined' || str === null) return undefined;
return /(TRUE)|1/i.test(str);
}
}}
{
const proxy = {};
const obfs = {};
const $ = {};
const params = {};
}
start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
proxy.port = port;
proxy.name = name;
// name may be empty
if (!proxy.name) {
proxy.name = server + ":" + port;
}
};
password = match:$[^@]+ {
return decodeURIComponent(match);
};
server = ip/domain;
domain = match:[0-9a-zA-z-_.]+ {
const domain = match.join("");
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
return domain;
}
}
ip = & {
const start = peg$currPos;
let end;
let j = start;
while (j < input.length) {
if (input[j] === ",") break;
if (input[j] === ":") end = j;
j++;
}
peg$currPos = end || j;
$.ip = input.substring(start, end).trim();
return true;
} { return $.ip; }
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 0 && port <= 65535) {
return port;
} else {
throw new Error("Invalid port: " + port);
}
}
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
if (toBool(params["ws"])) {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
}
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}
single = key:$[a-z]i+ {
params[key] = true;
};
name = "#" + match:$.* {
return decodeURIComponent(match);
}
-141
View File
@@ -1,141 +0,0 @@
import * as peggy from "peggy";
const grammars = String.raw`
// global initializer
{{
function $set(obj, path, value) {
if (Object(obj) !== obj) return obj;
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path
.slice(0, -1)
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
path[path.length - 1]
] = value;
return obj;
}
function toBool(str) {
if (typeof str === 'undefined' || str === null) return undefined;
return /(TRUE)|1/i.test(str);
}
}}
{
const proxy = {};
const obfs = {};
const $ = {};
const params = {};
}
start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
proxy.port = port;
proxy.name = name;
// name may be empty
if (!proxy.name) {
proxy.name = server + ":" + port;
}
};
password = match:$[^@]+ {
return decodeURIComponent(match);
};
server = ip/domain;
domain = match:[0-9a-zA-z-_.]+ {
const domain = match.join("");
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
return domain;
}
}
ip = & {
const start = peg$currPos;
let end;
let j = start;
while (j < input.length) {
if (input[j] === ",") break;
if (input[j] === ":") end = j;
j++;
}
peg$currPos = end || j;
$.ip = input.substring(start, end).trim();
return true;
} { return $.ip; }
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 0 && port <= 65535) {
return port;
} else {
throw new Error("Invalid port: " + port);
}
}
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
if (toBool(params["ws"])) {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
}
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}
single = key:$[a-z]i+ {
params[key] = true;
};
name = "#" + match:$.* {
return decodeURIComponent(match);
}
`;
let parser: any;
export default function getParser() {
if (!parser) {
parser = peggy.generate(grammars);
}
return parser;
}
+95 -21
View File
@@ -1,5 +1,3 @@
import getTrojanURIParser from "@/utils/trojan-uri";
export default function parseUri(uri: string): IProxyConfig { export default function parseUri(uri: string): IProxyConfig {
const head = uri.split("://")[0]; const head = uri.split("://")[0];
switch (head) { switch (head) {
@@ -467,7 +465,19 @@ function URI_VMESS(line: string): IProxyVmessConfig {
opts["v2ray-http-upgrade"] = true; opts["v2ray-http-upgrade"] = true;
opts["v2ray-http-upgrade-fast-open"] = true; opts["v2ray-http-upgrade-fast-open"] = true;
} }
proxy[`${proxy.network}-opts`] = opts; switch (proxy.network) {
case "ws":
proxy["ws-opts"] = opts;
break;
case "http":
proxy["http-opts"] = opts;
break;
case "h2":
proxy["h2-opts"] = opts;
break;
default:
break;
}
} }
} else { } else {
delete proxy.network; delete proxy.network;
@@ -530,16 +540,7 @@ function URI_VLESS(line: string): IProxyVlessConfig {
proxy.servername = params.sni || params.peer; proxy.servername = params.sni || params.peer;
proxy.flow = params.flow ? "xtls-rprx-vision" : undefined; proxy.flow = params.flow ? "xtls-rprx-vision" : undefined;
proxy["client-fingerprint"] = params.fp as proxy["client-fingerprint"] = params.fp as ClientFingerprint;
| "chrome"
| "firefox"
| "safari"
| "iOS"
| "android"
| "edge"
| "360"
| "qq"
| "random";
proxy.alpn = params.alpn ? params.alpn.split(",") : undefined; proxy.alpn = params.alpn ? params.alpn.split(",") : undefined;
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.allowInsecure); proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.allowInsecure);
@@ -635,16 +636,89 @@ function URI_VLESS(line: string): IProxyVlessConfig {
} }
function URI_Trojan(line: string): IProxyTrojanConfig { function URI_Trojan(line: string): IProxyTrojanConfig {
let [newLine, name] = line.split(/#(.+)/, 2); line = line.split("trojan://")[1];
const parser = getTrojanURIParser(); let [__, password, server, ___, port, ____, addons = "", name] =
const proxy: IProxyTrojanConfig = parser.parse(newLine); /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
if (isNotBlank(name)) {
try { let portNum = parseInt(`${port}`, 10);
proxy.name = decodeURIComponent(name).trim(); if (isNaN(portNum)) {
} catch (e) { portNum = 443;
throw Error("Can not get proxy name"); }
password = decodeURIComponent(password);
let decodedName = trimStr(decodeURIComponent(name));
name = decodedName ?? `Trojan ${server}:${portNum}`;
const proxy: IProxyTrojanConfig = {
type: "trojan",
name,
server,
port: portNum,
password,
};
let host = "";
let path = "";
for (const addon of addons.split("&")) {
let [key, value] = addon.split("=");
value = decodeURIComponent(value);
switch (key) {
case "type":
if (["ws", "h2"].includes(value)) {
proxy.network = value as NetworkType;
} else {
proxy.network = "tcp";
}
break;
case "host":
host = value;
break;
case "path":
path = value;
break;
case "alpn":
proxy["alpn"] = value ? value.split(",") : undefined;
break;
case "sni":
proxy["sni"] = value;
break;
case "skip-cert-verify":
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
break;
case "fingerprint":
proxy["fingerprint"] = value;
break;
case "fp":
proxy["fingerprint"] = value;
break;
case "encryption":
let encryption = value.split(";");
if (encryption.length === 3) {
proxy["ss-opts"] = {
enabled: true,
method: encryption[1],
password: encryption[2],
};
}
case "client-fingerprint":
proxy["client-fingerprint"] = value as ClientFingerprint;
break;
default:
break;
} }
} }
if (proxy.network === "ws") {
proxy["ws-opts"] = {
headers: { Host: host },
path,
} as WsOptions;
} else if (proxy.network === "grpc") {
proxy["grpc-opts"] = {
"grpc-service-name": path,
} as GrpcOptions;
}
return proxy; return proxy;
} }