Compare commits

..

34 Commits

53 changed files with 1234 additions and 265 deletions

View File

@@ -1,7 +1,7 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ] || [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
pnpm build --target $INPUT_TARGET
else
pnpm build --target $INPUT_TARGET -b deb

View File

@@ -10,12 +10,6 @@ rustup target add "$INPUT_TARGET"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
elif [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
dpkg --add-architecture i386
apt-get update
apt-get install -y libstdc++6:i386 libgdk-pixbuf2.0-dev:i386 libatomic1:i386 gcc-multilib g++-multilib libwebkit2gtk-4.0-dev:i386 libssl-dev:i386 libgtk-3-dev:i386 librsvg2-dev:i386 patchelf:i386 libayatana-appindicator3-dev:i386
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64
apt-get update

169
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,169 @@
name: Alpha Build
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
alpha:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: alpha
releaseName: "Clash Verge Rev Alpha"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
alpha-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build for Linux
uses: ./.github/build-for-linux
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- 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: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
cat > release.txt << 'EOF'
## Clash Verge Rev Alpha
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true

View File

@@ -2,9 +2,6 @@ name: Release Build
on:
workflow_dispatch:
push:
tags:
- v**
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -18,8 +15,6 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
@@ -78,7 +73,7 @@ jobs:
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
@@ -100,12 +95,8 @@ jobs:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# - os: ubuntu-latest
# target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
@@ -124,11 +115,11 @@ jobs:
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64') || startsWith(matrix.target, 'i686')
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge v${{env.VERSION}}"
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
@@ -136,21 +127,20 @@ jobs:
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge v${{env.VERSION}}"
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
release-update:
needs: [release]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
needs: [release, release-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: "20"

View File

@@ -13,18 +13,15 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
Click on the corresponding link below to download the installation package. Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
[[Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x64-setup.exe)]
[[Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x86-setup.exe)]
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_arm64-setup.exe)]
[[Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_x64-setup.exe)]
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_arm64-setup.exe)]
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x64.dmg)]
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_aarch64.dmg)]
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_x64.dmg)]
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_aarch64.dmg)]
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_amd64.AppImage)]
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_amd64.deb)]
[[Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_i386.AppImage)]
[[Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_i386.deb)]
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_arm64.deb)]
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_amd64.AppImage)]
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_amd64.deb)]
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_arm64.deb)]
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+

View File

@@ -1,9 +1,50 @@
## v1.5.4
### Features
- 支持自定义托盘图标
- 支持禁用代理组图标
- 代理组显示当前代理
- 修改 `打开面板` 快捷键为`打开/关闭面板`
---
## v1.5.3
### Features
- Tun 设置添加重置按钮
### Bugs Fixes
- Tun 设置项显示错误的问题
- 修改一些默认值
- 启动时不更改启动项设置
---
## v1.5.2
### Features
- 支持自定义延迟测试超时时间
- 优化 Tun 相关设置
### Bugs Fixes
- Merge 操作出错
- 安装后重启服务
- 修复管理员权限启动时开机启动失效的问题
---
## v1.5.1
### Features
- 保存窗口最大化状态
- Proxy Provider 显示数量
- 不再提供 32 位安装包(因为 32 位经常出现各种奇怪问题,比如 tun 模式无法开启;现在系统也几乎没有 32 位了)
### Bugs Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.5.1",
"version": "1.5.4",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",

View File

@@ -1,4 +1,4 @@
From 8b085aea2f11e64f433244eda092c178a2bb50bc Mon Sep 17 00:00:00 2001
From 871c9a6d1ed014c93da2436a437df03734e9f76c Mon Sep 17 00:00:00 2001
From: MystiPanda <mystipanda@proton.me>
Date: Sun, 10 Dec 2023 19:47:45 +0800
Subject: [PATCH] feat: Support windows aarch64
@@ -22,7 +22,7 @@ index 0000000..2eda7e4
+ path = src-tauri/quick-rs
+ url = https://github.com/clash-verge-rev/quick-rs.git
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 4c6dde5..5fd9ad8 100644
index 2f1a3be..d67f6ed 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -25,7 +25,6 @@ log4rs = "1"
@@ -32,8 +32,8 @@ index 4c6dde5..5fd9ad8 100644
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
auto-launch = "0.5"
@@ -34,6 +33,7 @@ port_scanner = "0.1.5"
once_cell = "1.18"
@@ -33,6 +32,7 @@ port_scanner = "0.1.5"
delay_timer = "0.11.5"
parking_lot = "0.12"
percent-encoding = "2.3.1"
@@ -49,7 +49,7 @@ index 0000000..78277c4
@@ -0,0 +1 @@
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
index 6c207d9..d47dc33 100644
index 30a922f..d47dc33 100644
--- a/src-tauri/src/enhance/script.rs
+++ b/src-tauri/src/enhance/script.rs
@@ -3,61 +3,83 @@ use anyhow::Result;

View File

@@ -13,24 +13,20 @@ const FORCE = process.argv.includes("--force");
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
"i686-pc-windows-msvc": "win32",
"aarch64-pc-windows-msvc": "win32",
"x86_64-apple-darwin": "darwin",
"aarch64-apple-darwin": "darwin",
"x86_64-unknown-linux-gnu": "linux",
"i686-unknown-linux-gnu": "linux",
"aarch64-unknown-linux-gnu": "linux",
"armv7-unknown-linux-gnueabihf": "linux",
"loongarch64-unknown-linux-gnu": "linux",
};
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "ia32",
"aarch64-pc-windows-msvc": "arm64",
"x86_64-apple-darwin": "x64",
"aarch64-apple-darwin": "arm64",
"x86_64-unknown-linux-gnu": "x64",
"i686-unknown-linux-gnu": "ia32",
"aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm",
"loongarch64-unknown-linux-gnu": "loong64",
@@ -57,12 +53,10 @@ let META_ALPHA_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
@@ -103,12 +97,10 @@ let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
@@ -317,6 +309,38 @@ async function downloadFile(url, path) {
console.log(`[INFO]: download finished "${url}"`);
}
// SimpleSC.dll
const resolvePlugin = async () => {
const url =
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fs.mkdirp(pluginDir);
await fs.mkdirp(tempDir);
if (!FORCE && (await fs.pathExists(pluginPath))) return;
try {
if (!(await fs.pathExists(tempZip))) {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.copyFile(tempDll, pluginPath);
console.log(`[INFO]: "SimpleSC" unzip finished`);
} finally {
await fs.remove(tempDir);
}
};
/**
* main
*/
@@ -373,6 +397,7 @@ const tasks = [
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
},
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },

View File

@@ -5,9 +5,9 @@ import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"i686-pc-windows-msvc": "x86",
"x86_64-pc-windows-msvc": "x64",
"aarch64-pc-windows-msvc": "arm64",
};
@@ -53,14 +53,25 @@ async function resolvePortable() {
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
console.log("[INFO]: upload to ", tag);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag: process.env.TAG_NAME || `v${version}`,
tag,
});
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
});
}
console.log(release.name);
await github.rest.repos.uploadReleaseAsset({

View File

@@ -45,11 +45,9 @@ async function resolveUpdater() {
"darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
"linux-i686": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
},
};
@@ -69,16 +67,6 @@ async function resolveUpdater() {
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x86-setup.nsis.zip")) {
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-i686"].signature = sig;
}
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
@@ -130,16 +118,6 @@ async function resolveUpdater() {
updateData.platforms["linux-aarch64"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;
}
// linux x86 url
if (name.endsWith("i386.AppImage.tar.gz")) {
updateData.platforms["linux-i686"].url = browser_download_url;
}
// linux x86 signature
if (name.endsWith("i386.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["linux-i686"].signature = sig;
}
});
await Promise.allSettled(promises);

5
src-tauri/Cargo.lock generated
View File

@@ -346,8 +346,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
source = "git+https://github.com/zzzgydi/auto-launch?branch=main#2d94a103ca20652a3baf581ca2c296791c35c09b"
dependencies = [
"dirs 4.0.0",
"thiserror",
@@ -599,7 +598,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "1.5.1"
version = "1.5.4"
dependencies = [
"anyhow",
"auto-launch",

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.5.1"
version = "1.5.4"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -28,7 +28,6 @@ sysinfo = "0.30"
rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
auto-launch = "0.5"
once_cell = "1.18"
port_scanner = "0.1.5"
delay_timer = "0.11.5"
@@ -39,7 +38,8 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version = "1.5", features = [ "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { version = "1.5", features = [ "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.0.0" # 高版本会返回错误 Status

View File

@@ -249,8 +249,9 @@ pub mod uwp {
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url).await {
match clash_api::get_proxy_delay(name, url, timeout).await {
Ok(res) => Ok(res),
Err(err) => Err(err.to_string()),
}
@@ -266,6 +267,33 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
Ok(app_home_dir)
}
#[tauri::command]
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
let file_path = std::path::Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
}
let dest_path = icon_dir.join(name);
if file_path.exists() {
match std::fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
return Err("file not found".to_string());
}
}
#[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);

View File

@@ -12,17 +12,33 @@ pub struct IClashTemp(pub Mapping);
impl IClashTemp {
pub fn new() -> Self {
let template = Self::template();
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
Ok(map) => Self(Self::guard(map)),
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
map.insert(key.clone(), template.0.get(key).unwrap().clone());
}
});
Self(Self::guard(map))
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
template
}
}
}
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Meta".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 9000.into());
map.insert("mixed-port".into(), 7897.into());
map.insert("socks-port".into(), 7898.into());
@@ -32,6 +48,7 @@ impl IClashTemp {
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
map.insert("secret".into(), "".into());
map.insert("tun".into(), tun.into());
Self(map)
}

View File

@@ -1,7 +1,7 @@
use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use serde_yaml::{Mapping, Value};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime {
pub config: Option<Mapping>,
@@ -16,7 +16,7 @@ impl IRuntime {
Self::default()
}
// 这里只更改 allow-lan | ipv6 | log-level
// 这里只更改 allow-lan | ipv6 | log-level | tun
pub fn patch_config(&mut self, patch: Mapping) {
if let Some(config) = self.config.as_mut() {
["allow-lan", "ipv6", "log-level"]
@@ -26,6 +26,20 @@ impl IRuntime {
config.insert(key.into(), value.clone());
}
});
let tun = config.get("tun");
let mut tun = tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = patch.get("tun");
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
use_keys(&patch_tun).into_iter().for_each(|key| {
if let Some(value) = patch_tun.get(&key).to_owned() {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
}
}
}

View File

@@ -36,6 +36,18 @@ pub struct IVerge {
/// show memory info (only for Clash Meta)
pub enable_memory_usage: Option<bool>,
/// enable group icon
pub enable_group_icon: Option<bool>,
/// common tray icon
pub common_tray_icon: Option<bool>,
/// sysproxy tray icon
pub sysproxy_tray_icon: Option<bool>,
/// tun tray icon
pub tun_tray_icon: Option<bool>,
/// clash tun mode
pub enable_tun_mode: Option<bool>,
@@ -81,6 +93,9 @@ pub struct IVerge {
/// 默认的延迟测试连接
pub default_latency_test: Option<String>,
/// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i32>,
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
@@ -160,6 +175,10 @@ impl IVerge {
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_group_icon: Some(true),
common_tray_icon: Some(false),
sysproxy_tray_icon: Some(false),
tun_tray_icon: Some(false),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_system_proxy: Some(false),
@@ -201,6 +220,10 @@ impl IVerge {
patch!(startup_script);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_group_icon);
patch!(common_tray_icon);
patch!(sysproxy_tray_icon);
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);
@@ -222,6 +245,7 @@ impl IVerge {
patch!(auto_close_connection);
patch!(default_latency_test);
patch!(default_latency_timeout);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
patch!(test_list);

View File

@@ -44,7 +44,11 @@ pub struct DelayRes {
/// GET /proxies/{name}/delay
/// 获取代理延迟
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
pub async fn get_proxy_delay(
name: String,
test_url: Option<String>,
timeout: i32,
) -> Result<DelayRes> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
@@ -57,7 +61,7 @@ pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<D
let builder = client
.get(&url)
.headers(headers)
.query(&[("timeout", "10000"), ("url", &test_url)]);
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
let response = builder.send().await?;
Ok(response.json::<DelayRes>().await?)

View File

@@ -144,10 +144,9 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?;
// fix #212
let args = match clash_core.as_str() {
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta" => vec!["-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-d", app_dir, "-f", config_path],
_ => vec!["-d", app_dir, "-f", config_path],
};

View File

@@ -65,7 +65,7 @@ impl Hotkey {
}
let f = match func.trim() {
"open_dashboard" => feat::open_dashboard,
"open_or_close_dashboard" => feat::open_or_close_dashboard,
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),

View File

@@ -148,9 +148,6 @@ impl Sysopt {
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
@@ -204,28 +201,6 @@ impl Sysopt {
.set_app_path(&app_path)
.build()?;
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
return Ok(());
}
#[cfg(target_os = "macos")]
{
if enable && !auto.is_enabled().unwrap_or(false) {
// 避免重复设置登录项
let _ = auto.disable();
auto.enable()?;
} else if !enable {
let _ = auto.disable();
}
}
#[cfg(not(target_os = "macos"))]
if enable {
auto.enable()?;
}
*self.auto_launch.lock() = Some(auto);
Ok(())

View File

@@ -1,4 +1,9 @@
use crate::{cmds, config::Config, feat, utils::resolve};
use crate::{
cmds,
config::Config,
feat,
utils::{dirs, resolve},
};
use anyhow::Result;
use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
@@ -129,26 +134,47 @@ impl Tray {
let verge = verge.latest();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let mut indication_icon = if *system_proxy {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
if *sysproxy_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("sysproxy.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
icon
} else {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
if *common_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("common.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
icon
};
if *tun_mode {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
if *tun_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("tun.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
indication_icon = icon
}

View File

@@ -14,6 +14,7 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段
use_lowercase(merge.clone())
.into_iter()
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
.for_each(|(key, value)| {
config.insert(key, value);
});

View File

@@ -1,5 +1,5 @@
mod chain;
mod field;
pub mod field;
mod merge;
mod script;
mod tun;
@@ -78,7 +78,18 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// 合并默认的config
for (key, value) in clash_config.into_iter() {
config.insert(key, value);
if key.as_str() == Some("tun") {
let mut tun = config.get_mut("tun").map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() {
tun.insert(key, value);
}
config.insert("tun".into(), tun.into());
} else {
config.insert(key, value);
}
}
// 内建脚本最后跑

View File

@@ -30,12 +30,6 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
});
revise!(tun_val, "enable", enable);
if enable {
append!(tun_val, "stack", "gvisor");
append!(tun_val, "dns-hijack", vec!["any:53"]);
append!(tun_val, "auto-route", true);
append!(tun_val, "auto-detect-interface", true);
}
revise!(config, "tun", tun_val);

View File

@@ -10,13 +10,19 @@ use crate::log_err;
use crate::utils::resolve;
use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value};
use tauri::{AppHandle, ClipboardManager};
use tauri::{AppHandle, ClipboardManager, Manager};
// 打开面板
pub fn open_dashboard() {
pub fn open_or_close_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
if let Some(window) = app_handle.get_window("main") {
if let Ok(true) = window.is_focused() {
let _ = window.close();
return;
}
}
resolve::create_window(app_handle);
}
}
@@ -230,6 +236,9 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
let common_tray_icon = patch.common_tray_icon;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
match {
#[cfg(target_os = "windows")]
@@ -269,7 +278,12 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if language.is_some() {
handle::Handle::update_systray()?;
} else if system_proxy.or(tun_mode).is_some() {
} else if system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
{
handle::Handle::update_systray_part()?;
}

View File

@@ -55,6 +55,8 @@ fn main() -> std::io::Result<()> {
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::get_app_dir,
cmds::copy_icon_file,
cmds::exit_app,
// cmds::update_hotkeys,
// profile

View File

@@ -92,14 +92,9 @@ pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid"))
}
#[cfg(windows)]
pub fn service_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("service"))
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
Ok(service_dir()?.join("clash-verge-service.exe"))
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
}
#[cfg(windows)]

View File

@@ -240,67 +240,6 @@ pub fn init_resources() -> Result<()> {
Ok(())
}
/// initialize service resources
/// after tauri setup
#[cfg(target_os = "windows")]
pub fn init_service() -> Result<()> {
let service_dir = dirs::service_dir()?;
let res_dir = dirs::app_resources_dir()?;
if !service_dir.exists() {
let _ = fs::create_dir_all(&service_dir);
}
if !res_dir.exists() {
let _ = fs::create_dir_all(&res_dir);
}
let file_list = [
"clash-verge-service.exe",
"install-service.exe",
"uninstall-service.exe",
];
// copy the resource file
// if the source file is newer than the destination file, copy it over
for file in file_list.iter() {
let src_path = res_dir.join(file);
let dest_path = service_dir.join(file);
let handle_copy = || {
match fs::copy(&src_path, &dest_path) {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}', {err}")
}
};
};
if src_path.exists() && !dest_path.exists() {
handle_copy();
continue;
}
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy();
} else {
log::debug!(target: "app", "skipping resource copy '{file}'");
}
}
_ => {
log::debug!(target: "app", "failed to get modified '{file}'");
handle_copy();
}
};
}
Ok(())
}
/// initialize url scheme
#[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> {

View File

@@ -42,8 +42,6 @@ pub fn resolve_setup(app: &mut App) {
VERSION.get_or_init(|| version.clone());
log_err!(init::init_resources());
#[cfg(target_os = "windows")]
log_err!(init::init_service());
log_err!(init::init_scheme());
log_err!(init::startup_script());
// 处理随机端口
@@ -186,9 +184,9 @@ pub fn create_window(app_handle: &AppHandle) {
let pos = win.outer_position()?;
if pos.x < -400
|| pos.x > (size.width - 200).try_into()?
|| pos.x > (size.width - 200) as i32
|| pos.y < -200
|| pos.y > (size.height - 200).try_into()?
|| pos.y > (size.height - 200) as i32
{
center = true;
}

View File

@@ -1,7 +1,7 @@
{
"package": {
"productName": "Clash Verge",
"version": "1.5.1"
"version": "1.5.4"
},
"build": {
"distDir": "../dist",
@@ -58,11 +58,18 @@
"dialog": {
"all": false,
"open": true
},
"protocol": {
"asset": true,
"assetScope": ["**"]
},
"path": {
"all": true
}
},
"windows": [],
"security": {
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src http: https: data: 'self';"
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src asset: http: https: data: 'self';"
}
}
}

View File

@@ -15,6 +15,7 @@ Unicode true
!include WordFunc.nsh
!include "LogicLib.nsh"
!include "StrFunc.nsh"
!addplugindir "$%AppData%\Local\NSIS\"
${StrCase}
${StrLoc}
@@ -423,6 +424,7 @@ FunctionEnd
nsis_tauri_utils::FindProcess "Clash Verge.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill Clash Verge.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "Clash Verge.exe"
!else
@@ -435,6 +437,7 @@ FunctionEnd
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-verge-service.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
!else
@@ -447,6 +450,7 @@ FunctionEnd
nsis_tauri_utils::FindProcess "clash-meta-alpha.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta-alpha.exe"
!else
@@ -458,6 +462,7 @@ FunctionEnd
nsis_tauri_utils::FindProcess "clash-meta.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta.exe"
!else
@@ -466,9 +471,70 @@ FunctionEnd
${EndIf}
!macroend
Section
!insertmacro CheckAllVergeProcesses
SectionEnd
!macro StartVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Clash Verge Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
!macro RemoveVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Clash Verge Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
Section EarlyChecks
; Abort silent installer if downgrades is disabled
@@ -608,6 +674,8 @@ Section Install
File /a "/oname={{this}}" "{{@key}}"
{{/each}}
!insertmacro StartVergeService
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -679,6 +747,7 @@ FunctionEnd
Section Uninstall
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"

View File

@@ -55,6 +55,7 @@ export const ProviderButton = () => {
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Button
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await proxyProviderUpdate(key);

View File

@@ -25,6 +25,7 @@ export const ProxyGroups = (props: Props) => {
const { verge } = useVerge();
const { current, patchCurrent } = useProfiles();
const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null);
@@ -83,7 +84,7 @@ export const ProxyGroups = (props: Props) => {
}
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
await delayManager.checkListDelay(names, groupName);
await delayManager.checkListDelay(names, groupName, timeout);
onProxies();
});

View File

@@ -4,6 +4,7 @@ import { CheckCircleOutlineRounded } from "@mui/icons-material";
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
import { BaseLoading } from "@/components/base";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
@@ -20,6 +21,8 @@ export const ProxyItemMini = (props: Props) => {
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
@@ -36,7 +39,7 @@ export const ProxyItemMini = (props: Props) => {
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName));
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
});
return (
@@ -92,7 +95,32 @@ export const ProxyItemMini = (props: Props) => {
</Typography>
{showType && (
<Box sx={{ display: "flex", flexWrap: "nowrap", flex: "none" }}>
<Box
sx={{
display: "flex",
flexWrap: "nowrap",
flex: "none",
marginTop: "4px",
}}
>
{proxy.now && (
<Typography
variant="body2"
component="div"
color="text.secondary"
sx={{
display: "block",
textOverflow: "ellipsis",
wordBreak: "break-all",
overflow: "hidden",
whiteSpace: "nowrap",
fontSize: "0.75rem",
marginRight: "8px",
}}
>
{proxy.now}
</Typography>
)}
{!!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
)}
@@ -139,14 +167,14 @@ export const ProxyItemMini = (props: Props) => {
e.stopPropagation();
onDelay();
}}
color={delayManager.formatDelayColor(delay)}
color={delayManager.formatDelayColor(delay, timeout)}
sx={({ palette }) =>
!proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {}
}
>
{delayManager.formatDelay(delay)}
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
@@ -178,6 +206,16 @@ const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
fontSize: 10,
fontFamily: typography.fontFamily,
marginRight: "4px",
marginTop: "auto",
padding: "0 2px",
lineHeight: 1.25,
}));
const TypeTypo = styled(Box)(({ theme: { palette, typography } }) => ({
display: "inline-block",
fontSize: 10,
fontFamily: typography.fontFamily,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));

View File

@@ -14,6 +14,7 @@ import {
} from "@mui/material";
import { BaseLoading } from "@/components/base";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
@@ -48,7 +49,8 @@ export const ProxyItem = (props: Props) => {
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
@@ -64,7 +66,7 @@ export const ProxyItem = (props: Props) => {
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName));
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
});
return (
@@ -98,7 +100,9 @@ export const ProxyItem = (props: Props) => {
secondary={
<>
<span style={{ marginRight: 4 }}>{proxy.name}</span>
{showType && proxy.now && (
<TypeBox component="span">{proxy.now}</TypeBox>
)}
{showType && !!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
)}
@@ -149,14 +153,14 @@ export const ProxyItem = (props: Props) => {
e.stopPropagation();
onDelay();
}}
color={delayManager.formatDelayColor(delay)}
color={delayManager.formatDelayColor(delay, timeout)}
sx={({ palette }) =>
!proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {}
}
>
{delayManager.formatDelay(delay)}
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}

View File

@@ -16,6 +16,7 @@ import { ProxyHead } from "./proxy-head";
import { ProxyItem } from "./proxy-item";
import { ProxyItemMini } from "./proxy-item-mini";
import type { IRenderItem } from "./use-render-list";
import { useVerge } from "@/hooks/use-verge";
interface RenderProps {
item: IRenderItem;
@@ -30,6 +31,8 @@ export const ProxyRender = (props: RenderProps) => {
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
props;
const { type, group, headState, proxy, proxyCol } = item;
const { verge } = useVerge();
const enable_group_icon = verge?.enable_group_icon ?? true;
if (type === 0 && !group.hidden) {
return (
@@ -37,18 +40,32 @@ export const ProxyRender = (props: RenderProps) => {
dense
onClick={() => onHeadState(group.name, { open: !headState?.open })}
>
{group.icon && group.icon.trim().startsWith("http") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("data") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="40px"
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("http") && (
<img
src={group.icon}
height="40px"
style={{ marginRight: "8px" }}
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("data") && (
<img
src={group.icon}
height="40px"
style={{ marginRight: "8px" }}
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="40px"
/>
)}
<ListItemText
primary={group.name}
secondary={

View File

@@ -16,7 +16,7 @@ type HeadStateStorage = Record<string, Record<string, HeadState>>;
const HEAD_STATE_KEY = "proxy-head-state";
export const DEFAULT_STATE: HeadState = {
open: false,
showType: false,
showType: true,
sortType: 0,
filterText: "",
textState: null,

View File

@@ -53,6 +53,7 @@ export const ProviderButton = () => {
<Typography variant="h6">{t("Rule Provider")}</Typography>
<Button
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await ruleProviderUpdate(key);

View File

@@ -14,7 +14,7 @@ const ItemWrapper = styled("div")`
`;
const HOTKEY_FUNC = [
"open_dashboard",
"open_or_close_dashboard",
"clash_mode_rule",
"clash_mode_global",
"clash_mode_direct",

View File

@@ -1,16 +1,38 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { List, Switch } from "@mui/material";
import { List, Switch, Button } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { SettingItem } from "./setting-comp";
import { GuardState } from "./guard-state";
import { open as openDialog } from "@tauri-apps/api/dialog";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { copyIconFile, getAppDir } from "@/services/cmds";
import { join } from "@tauri-apps/api/path";
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
const [commonIcon, setCommonIcon] = useState("");
const [sysproxyIcon, setSysproxyIcon] = useState("");
const [tunIcon, setTunIcon] = useState("");
useEffect(() => {
initIconPath();
}, []);
async function initIconPath() {
const appDir = await getAppDir();
const icon_dir = await join(appDir, "icons");
const common_icon = await join(icon_dir, "common.png");
const sysproxy_icon = await join(icon_dir, "sysproxy.png");
const tun_icon = await join(icon_dir, "tun.png");
setCommonIcon(common_icon);
setSysproxyIcon(sysproxy_icon);
setTunIcon(tun_icon);
}
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
@@ -61,6 +83,149 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Proxy Group Icon")}>
<GuardState
value={verge?.enable_group_icon ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_group_icon: e })}
onGuard={(e) => patchVerge({ enable_group_icon: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Common Tray Icon")}>
<GuardState
value={verge?.common_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ common_tray_icon: e })}
onGuard={(e) => patchVerge({ common_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.common_tray_icon &&
commonIcon && (
<img height="20px" src={convertFileSrc(commonIcon)} />
)
}
onClick={async () => {
if (verge?.common_tray_icon) {
onChangeData({ common_tray_icon: false });
patchVerge({ common_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "common.png");
onChangeData({ common_tray_icon: true });
patchVerge({ common_tray_icon: true });
}
}
}}
>
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
<SettingItem label={t("System Proxy Tray Icon")}>
<GuardState
value={verge?.sysproxy_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ sysproxy_tray_icon: e })}
onGuard={(e) => patchVerge({ sysproxy_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.sysproxy_tray_icon &&
sysproxyIcon && (
<img height="20px" src={convertFileSrc(sysproxyIcon)} />
)
}
onClick={async () => {
if (verge?.sysproxy_tray_icon) {
onChangeData({ sysproxy_tray_icon: false });
patchVerge({ sysproxy_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "sysproxy.png");
onChangeData({ sysproxy_tray_icon: true });
patchVerge({ sysproxy_tray_icon: true });
}
}
}}
>
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
<SettingItem label={t("Tun Tray Icon")}>
<GuardState
value={verge?.tun_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ tun_tray_icon: e })}
onGuard={(e) => patchVerge({ tun_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.tun_tray_icon &&
tunIcon && <img height="20px" src={convertFileSrc(tunIcon)} />
}
onClick={async () => {
if (verge?.tun_tray_icon) {
onChangeData({ tun_tray_icon: false });
patchVerge({ tun_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "tun.png");
onChangeData({ tun_tray_icon: true });
patchVerge({ tun_tray_icon: true });
}
}
}}
>
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
</List>
</BaseDialog>
);

View File

@@ -25,6 +25,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
proxyLayoutColumn: 6,
defaultLatencyTest: "",
autoLogClean: 0,
defaultLatencyTimeout: 10000,
});
useImperativeHandle(ref, () => ({
@@ -37,6 +38,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
proxyLayoutColumn: verge?.proxy_layout_column || 6,
defaultLatencyTest: verge?.default_latency_test || "",
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
});
},
close: () => setOpen(false),
@@ -50,6 +52,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
enable_builtin_enhanced: values.enableBuiltinEnhanced,
proxy_layout_column: values.proxyLayoutColumn,
default_latency_test: values.defaultLatencyTest,
default_latency_timeout: values.defaultLatencyTimeout,
auto_log_clean: values.autoLogClean as any,
});
setOpen(false);
@@ -179,6 +182,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<TextField
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.defaultLatencyTimeout}
placeholder="10000"
onChange={(e) =>
setValues((v) => ({
...v,
defaultLatencyTimeout: parseInt(e.target.value),
}))
}
/>
</ListItem>
</List>
</BaseDialog>
);

View File

@@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
import { Button, ButtonGroup, Tooltip } from "@mui/material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import getSystem from "@/utils/get-system";
import useSWR from "swr";
const isWIN = getSystem() === "windows";
interface Props {
value?: string;
onChange?: (value: string) => void;
}
export const StackModeSwitch = (props: Props) => {
const { value, onChange } = props;
const { verge } = useVerge();
const { enable_service_mode } = verge ?? {};
// service mode
const { data: serviceStatus } = useSWR(
isWIN ? "checkService" : null,
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
}
);
const { t } = useTranslation();
return (
<Tooltip
title={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
? t("System and Mixed Can Only be Used in Service Mode")
: ""
}
>
<ButtonGroup size="small" sx={{ my: "4px" }}>
<Button
variant={value?.toLowerCase() === "system" ? "contained" : "outlined"}
onClick={() => onChange?.("system")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
System
</Button>
<Button
variant={value?.toLowerCase() === "gvisor" ? "contained" : "outlined"}
onClick={() => onChange?.("gvisor")}
sx={{ textTransform: "capitalize" }}
>
gVisor
</Button>
<Button
variant={value?.toLowerCase() === "mixed" ? "contained" : "outlined"}
onClick={() => onChange?.("mixed")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
Mixed
</Button>
</ButtonGroup>
</Tooltip>
);
};

View File

@@ -0,0 +1,224 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
List,
ListItem,
ListItemText,
Box,
Typography,
Button,
Switch,
TextField,
} from "@mui/material";
import { useClash } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { StackModeSwitch } from "./stack-mode-switch";
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { clash, mutateClash, patchClash } = useClash();
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gvisor",
device: "Meta",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
});
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
setValues({
stack: clash?.tun.stack ?? "gvisor",
device: clash?.tun.device ?? "Meta",
autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
strictRoute: clash?.tun["strict-route"] ?? false,
mtu: clash?.tun.mtu ?? 9000,
});
},
close: () => setOpen(false),
}));
const onSave = useLockFn(async () => {
try {
let tun = {
stack: values.stack,
device: values.device,
"auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack,
"strict-route": values.strictRoute,
mtu: values.mtu,
};
await patchClash({ tun });
await mutateClash(
(old) => ({
...(old! || {}),
tun,
}),
false
);
setOpen(false);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Tun Mode")}</Typography>
<Button
variant="outlined"
size="small"
onClick={async () => {
let tun = {
stack: "gvisor",
device: "Meta",
"auto-route": true,
"auto-detect-interface": true,
"dns-hijack": ["any:53"],
"strict-route": false,
mtu: 9000,
};
setValues({
stack: "gvisor",
device: "Meta",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
});
await patchClash({ tun });
await mutateClash(
(old) => ({
...(old! || {}),
tun,
}),
false
);
}}
>
{t("Reset to Default")}
</Button>
</Box>
}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Stack")} />
<StackModeSwitch
value={values.stack}
onChange={(value) => {
setValues((v) => ({
...v,
stack: value,
}));
}}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<TextField
size="small"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.device}
placeholder="Meta"
onChange={(e) =>
setValues((v) => ({ ...v, device: e.target.value }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Route")} />
<Switch
edge="end"
checked={values.autoRoute}
onChange={(_, c) => setValues((v) => ({ ...v, autoRoute: c }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Strict Route")} />
<Switch
edge="end"
checked={values.strictRoute}
onChange={(_, c) => setValues((v) => ({ ...v, strictRoute: c }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Detect Interface")} />
<Switch
edge="end"
checked={values.autoDetectInterface}
onChange={(_, c) =>
setValues((v) => ({ ...v, autoDetectInterface: c }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<TextField
size="small"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.dnsHijack.join(",")}
placeholder="Please use , to separate multiple DNS servers"
onChange={(e) =>
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<TextField
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.mtu}
placeholder="9000"
onChange={(e) =>
setValues((v) => ({
...v,
mtu: parseInt(e.target.value),
}))
}
/>
</ListItem>
</List>
</BaseDialog>
);
});

View File

@@ -41,7 +41,6 @@ const SettingClash = ({ onError }: Props) => {
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
const webRef = useRef<DialogRef>(null);
const fieldRef = useRef<DialogRef>(null);
const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);

View File

@@ -1,8 +1,8 @@
import useSWR from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { IconButton, Switch } from "@mui/material";
import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material";
import { IconButton, Switch, Tooltip } from "@mui/material";
import { PrivacyTipRounded, Settings, InfoRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef } from "@/components/base";
@@ -10,6 +10,7 @@ import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state";
import { ServiceViewer } from "./mods/service-viewer";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import getSystem from "@/utils/get-system";
interface Props {
@@ -36,6 +37,7 @@ const SettingSystem = ({ onError }: Props) => {
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
const {
enable_tun_mode,
@@ -53,11 +55,41 @@ const SettingSystem = ({ onError }: Props) => {
return (
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
{isWIN && (
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
<SettingItem label={t("Tun Mode")}>
<SettingItem
label={t("Tun Mode")}
extra={
<>
<Tooltip
title={
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
}
placement="top"
>
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<IconButton
color="inherit"
size="small"
onClick={() => tunRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</>
}
>
<GuardState
value={enable_tun_mode ?? false}
valueProps="checked"

View File

@@ -1,17 +1,16 @@
import useSWR, { mutate } from "swr";
import { useLockFn } from "ahooks";
import { getAxios, getVersion, updateConfigs } from "@/services/api";
import {
getAxios,
getClashConfig,
getVersion,
updateConfigs,
} from "@/services/api";
import { getClashInfo, patchClashConfig } from "@/services/cmds";
getClashInfo,
patchClashConfig,
getRuntimeConfig,
} from "@/services/cmds";
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig
"getRuntimeConfig",
getRuntimeConfig
);
const { data: versionData, mutate: mutateVersion } = useSWR(

View File

@@ -110,6 +110,10 @@
"Hotkey Setting": "Hotkey Setting",
"Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Proxy Group Icon": "Proxy Group Icon",
"Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon",
"Language": "Language",
"Open App Dir": "Open App Dir",
"Open Core Dir": "Open Core Dir",
@@ -134,7 +138,7 @@
"Download Speed": "Download Speed",
"Upload Speed": "Upload Speed",
"open_dashboard": "Open Dashboard",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode",
"clash_mode_direct": "Direct Mode",
@@ -152,6 +156,7 @@
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Proxy Layout Column": "Proxy Layout Column",
"Default Latency Test": "Default Latency Test",
"Defaule Latency Timeout": "Defaule Latency Timeout",
"Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean",
@@ -159,5 +164,16 @@
"Retain 30 Days": "Retain 30 Days",
"Retain 90 Days": "Retain 90 Days",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it"
"Stack": "Tun Stack",
"Device": "Device Name",
"Auto Route": "Auto Route",
"Strict Route": "Strict Route",
"Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode"
}

View File

@@ -118,7 +118,7 @@
"Cancel": "Отмена",
"Exit": "Выход",
"open_dashboard": "Open Dashboard",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Режим правил",
"clash_mode_global": "Глобальный режим",
"clash_mode_direct": "Прямой режим",

View File

@@ -110,6 +110,10 @@
"Hotkey Setting": "热键设置",
"Traffic Graph": "流量图显",
"Memory Usage": "内存使用",
"Proxy Group Icon": "代理组图标",
"Common Tray Icon": "常规托盘图标",
"System Proxy Tray Icon": "系统代理托盘图标",
"Tun Tray Icon": "Tun模式托盘图标",
"Language": "语言设置",
"Open App Dir": "应用目录",
"Open Core Dir": "内核目录",
@@ -134,7 +138,7 @@
"Download Speed": "下载速度",
"Upload Speed": "上传速度",
"open_dashboard": "打开面板",
"open_or_close_dashboard": "打开/关闭面板",
"clash_mode_rule": "规则模式",
"clash_mode_global": "全局模式",
"clash_mode_direct": "直连模式",
@@ -152,6 +156,7 @@
"Enable Builtin Enhanced": "开启内建增强功能",
"Proxy Layout Column": "代理页布局列数",
"Default Latency Test": "默认测试链接",
"Default Latency Timeout": "测试超时时间",
"Auto Log Clean": "自动清理日志",
"Never Clean": "不清理",
@@ -159,5 +164,17 @@
"Retain 30 Days": "保留30天",
"Retain 90 Days": "保留90天",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换"
"Stack": "Tun 模式堆栈",
"Device": "Tun 网卡名称",
"Auto Route": "自动设置全局路由",
"Strict Route": "严格路由",
"Auto Detect Interface": "自动选择流量出口接口",
"DNS Hijack": "DNS 劫持",
"MTU": "最大传输单元",
"Reset to Default": "重置为默认值",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限使用前请先开启服务模式",
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限使用前请先在内核设置中给内核授权",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用"
}

View File

@@ -90,8 +90,9 @@ export async function getClashInfo() {
return invoke<IClashInfo | null>("get_clash_info");
}
// Get runtime config which controlled by verge
export async function getRuntimeConfig() {
return invoke<any | null>("get_runtime_config");
return invoke<IConfigData | null>("get_runtime_config");
}
export async function getRuntimeYaml() {
@@ -138,6 +139,10 @@ export async function grantPermission(core: string) {
return invoke<void>("grant_permission", { core });
}
export async function getAppDir() {
return invoke<string>("get_app_dir");
}
export async function openAppDir() {
return invoke<void>("open_app_dir").catch((err) =>
Notice.error(err?.message || err.toString(), 1500)
@@ -160,9 +165,17 @@ export async function openWebUrl(url: string) {
return invoke<void>("open_web_url", { url });
}
export async function cmdGetProxyDelay(name: string, url?: string) {
export async function cmdGetProxyDelay(
name: string,
timeout: number,
url?: string
) {
name = encodeURIComponent(name);
return invoke<{ delay: number }>("clash_api_get_proxy_delay", { name, url });
return invoke<{ delay: number }>("clash_api_get_proxy_delay", {
name,
url,
timeout,
});
}
export async function cmdTestDelay(url: string) {
@@ -203,3 +216,10 @@ export async function getPortableFlag() {
export async function exitApp() {
return invoke("exit_app");
}
export async function copyIconFile(
path: string,
name: "common.png" | "sysproxy.png" | "tun.png"
) {
return invoke<void>("copy_icon_file", { path, name });
}

View File

@@ -69,12 +69,12 @@ class DelayManager {
return -1;
}
async checkDelay(name: string, group: string) {
async checkDelay(name: string, group: string, timeout: number) {
let delay = -1;
try {
const url = this.getUrl(group);
const result = await cmdGetProxyDelay(name, url);
const result = await cmdGetProxyDelay(name, timeout, url);
delay = result.delay;
} catch {
delay = 1e6; // error
@@ -84,7 +84,12 @@ class DelayManager {
return delay;
}
async checkListDelay(nameList: string[], group: string, concurrency = 36) {
async checkListDelay(
nameList: string[],
group: string,
timeout: number,
concurrency = 36
) {
const names = nameList.filter(Boolean);
// 设置正在延迟测试中
names.forEach((name) => this.setDelay(name, group, -2));
@@ -98,7 +103,7 @@ class DelayManager {
const task = names.shift();
if (!task) return;
current += 1;
await this.checkDelay(task, group);
await this.checkDelay(task, group, timeout);
current -= 1;
total -= 1;
if (total <= 0) resolve(null);
@@ -108,15 +113,15 @@ class DelayManager {
});
}
formatDelay(delay: number) {
formatDelay(delay: number, timeout = 10000) {
if (delay <= 0) return "Error";
if (delay > 1e5) return "Error";
if (delay >= 10000) return "Timeout"; // 10s
if (delay >= timeout) return "Timeout"; // 10s
return `${delay}`;
}
formatDelayColor(delay: number) {
if (delay >= 10000) return "error.main";
formatDelayColor(delay: number, timeout = 10000) {
if (delay >= timeout) return "error.main";
if (delay <= 0) return "error.main";
if (delay > 500) return "warning.main";
return "success.main";

View File

@@ -32,6 +32,15 @@ interface IConfigData {
"tproxy-port": number;
"external-controller": string;
secret: string;
tun: {
stack: string;
device: string;
"auto-route": boolean;
"auto-detect-interface": boolean;
"dns-hijack": string[];
"strict-route": boolean;
mtu: number;
};
}
interface IRuleItem {
@@ -191,6 +200,10 @@ interface IVergeConfig {
theme_mode?: "light" | "dark" | "system";
traffic_graph?: boolean;
enable_memory_usage?: boolean;
enable_group_icon?: boolean;
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean;
enable_tun_mode?: boolean;
enable_auto_launch?: boolean;
enable_service_mode?: boolean;
@@ -219,6 +232,7 @@ interface IVergeConfig {
};
auto_close_connection?: boolean;
default_latency_test?: string;
default_latency_timeout?: number;
enable_builtin_enhanced?: boolean;
auto_log_clean?: 0 | 1 | 2 | 3;
proxy_layout_column?: number;