Compare commits

...

56 Commits

54 changed files with 1003 additions and 382 deletions

View File

@@ -57,6 +57,6 @@ body:
- type: textarea
attributes:
label: 日志 / Log
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debugVerge错误请把“杂项设置”->“app日志等级”调整到trace并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debugVerge错误请把“杂项设置”->“app日志等级”调整到debug/trace并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
validations:
required: true

View File

@@ -48,7 +48,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
@@ -119,7 +119,7 @@ jobs:
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
@@ -129,7 +129,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -141,10 +141,10 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: 'Setup for linux'
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
@@ -170,20 +170,20 @@ jobs:
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: 'Install aarch64 tools'
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: 'Install armv7 tools'
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -196,7 +196,7 @@ jobs:
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}

View File

@@ -50,7 +50,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -67,8 +67,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@@ -109,7 +109,7 @@ jobs:
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
@@ -119,7 +119,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -131,10 +131,10 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: 'Setup for linux'
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
@@ -160,20 +160,20 @@ jobs:
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: 'Install aarch64 tools'
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: 'Install armv7 tools'
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -186,7 +186,7 @@ jobs:
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@@ -195,6 +195,7 @@ jobs:
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -237,7 +238,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -262,8 +263,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
@@ -298,7 +299,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -323,7 +324,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm

View File

@@ -12,7 +12,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -36,7 +36,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm

View File

@@ -1,16 +1,48 @@
## v2.0.0
## v2.0.2
### Notice
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了巨量改进与性能、稳定性提升目前Clash Verge Rev已经有了比肩cfw的健壮性而且更强大易用
- 由于更改了服务安装逻辑Mac/Linux 首次安装需要输入 2 遍系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug关闭窗口暂时修改为最小化功能
- 由于更改了服务安装逻辑Mac/Linux 首次安装需要输入系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug关闭窗口后保留webview进程优点是再次打开面板更快缺点是内存使用略有增加
### 2.0.2相对于2.0.1改进了:
- MacOS 下自定义图标可以支持彩色、单色切换
- 修正了 Linux 下多个内核僵尸进程的问题
- 修正了 DNS ipv6 强制覆盖的逻辑
- 修改了 MacOS tun 模式下覆盖设置 dns 字段的问题
- 修正了 MacOS tray 图标不会随代理模式更改的问题
- 静默启动下重复运行会出现多个实例的bug
- 安装的时候自动删除历史残留启动项
- Tun模式默认是还用内核推荐的 mixed 堆栈
- 改进了默认窗口大小(启动软件窗口不会那么小了)
- 改进了 WebDAV 备份超时时间机制
- 测试菜单添加滚动条
- 改进和修正了 Tun 模式下对设置的覆盖逻辑
- 修复了打开配置出错的问题
- 修复了配置文件无法拖拽添加的问题
- 改善了浅色模式的对比度
### 2.0.1相对于2.0.0改进了:
- 无法从 2.0rc和2.0.0 升级的问题已经安装了2.0版本的需手动下载安装)
- MacOS 系统下少有的无法安装服务,无法启动的问题,目前更健壮了
- 当系统中没有 yaml 编辑器的情况下,打开文件程序崩溃的问题
- Windows 应用内升级和覆盖安装不会删除老执行文件的问题
- 修改优化了 mac 下 fakeip 段和 dns
- 测试菜单 svg 图标格式检查
- 应用内升级重复安装 vs runtime 的问题
- 修复外部控制下密码有特殊字符认证出错的问题
- 修复恢复 Webdav 备份设置后, Webdav 设置丢失的问题
- 代理页面增加快速回到顶部的按钮
### Breaking changes
- 重大框架升级:使用 Tauri 2.0(巨量改进与性能提升)
- 强烈建议完全删除 1.x 老版本再安装此版本
- 出现 bug 到 issues 中提出以后不再接受1.x版本的bug反馈。
- 强烈建议完全删除 1.x 老版本再安装此版本 !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!
### Features
@@ -30,7 +62,7 @@
- 添加统一延迟的设置开关
- 添加 Windows 下自动检测并下载 vc runtime 的功能
- 支持显示 mux 和 mptcp 的节点标识
- 延迟测试连接更换 https 的 cp.cloudflare.com/generate_204 以防止机场劫持(关闭统一延迟的情况下延迟测试结果会有所增加)
- 延迟测试连接更换 http 的 cp.cloudflare.com/generate_204 (关闭统一延迟的情况下延迟测试结果会有所增加)
- 重构日志记录逻辑可以收集和筛选所有日志类型了之前无法记录debug的日志类型
### Performance
@@ -58,10 +90,10 @@
- 修复快捷键设置的相关 bug
- 修复 Win 下点左键菜单闪现的问题Mac 下的操作逻辑相反,默认情况下不管点左/右键均会打开菜单,闪现不属于 bug
### Know issues
### Known issues
- Windows 下窗口大小无法记忆(等待上游修复)
- Webdav 备份因为安全性和兼容性问题,暂不支持同步 Webdav 服务器地址和登录信息;跨平台配置同步
- Webdav 备份因为安全性和兼容性问题,暂不支持跨平台配置同步
---

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.0.0",
"version": "2.0.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",

View File

@@ -153,13 +153,13 @@ async function getLatestReleaseVersion() {
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
@@ -354,7 +354,7 @@ const resolvePlugin = async () => {
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
@@ -488,8 +488,8 @@ const tasks = [
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
@@ -514,10 +514,10 @@ const tasks = [
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
if (task.winOnly && platform !== "win32") return runTask();
if (task.macosOnly && platform !== "darwin") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
for (let i = 0; i < task.retry; i++) {
try {
@@ -532,4 +532,3 @@ async function runTask() {
}
runTask();
runTask();

View File

@@ -49,9 +49,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
);
zip.addLocalFolder(configDir, ".config");

332
src-tauri/Cargo.lock generated
View File

@@ -85,6 +85,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@@ -160,6 +166,17 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -478,6 +495,29 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "av1-grain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom 7.1.3",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
dependencies = [
"arrayvec",
]
[[package]]
name = "axum"
version = "0.6.20"
@@ -556,6 +596,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -571,6 +617,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitstream-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]]
name = "block"
version = "0.1.6"
@@ -776,6 +828,12 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "built"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -988,7 +1046,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "2.0.0"
version = "2.0.2"
dependencies = [
"aes-gcm",
"anyhow",
@@ -1000,6 +1058,7 @@ dependencies = [
"dirs 5.0.1",
"dunce",
"getrandom 0.2.15",
"image",
"log",
"log4rs",
"nanoid",
@@ -1109,6 +1168,12 @@ dependencies = [
"objc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colored"
version = "2.1.0"
@@ -1934,6 +1999,21 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.73.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fast-float"
version = "0.2.0"
@@ -2353,6 +2433,16 @@ dependencies = [
"polyval",
]
[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -2562,6 +2652,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "handlebars"
version = "3.5.5"
@@ -3068,11 +3168,37 @@ checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp",
"num-traits",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imgref"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -3138,6 +3264,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "intrusive-collections"
version = "0.9.7"
@@ -3345,6 +3482,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -3375,6 +3518,16 @@ version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libfuzzer-sys"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "libloading"
version = "0.7.4"
@@ -3495,6 +3648,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "lru"
version = "0.7.8"
@@ -3577,6 +3739,16 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
name = "md-5"
version = "0.10.6"
@@ -3857,6 +4029,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "notify-rust"
version = "4.11.3"
@@ -3917,6 +4095,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -3926,6 +4115,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -4855,6 +5055,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
dependencies = [
"quote",
"syn 2.0.87",
]
[[package]]
name = "prost"
version = "0.12.6"
@@ -4887,6 +5106,15 @@ dependencies = [
"prost",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -5062,6 +5290,56 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rav1e"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
dependencies = [
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive 0.4.2",
"num-traits",
"once_cell",
"paste",
"profiling",
"rand 0.8.5",
"rand_chacha 0.3.1",
"simd_helpers",
"system-deps",
"thiserror 1.0.69",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "raw-window-handle"
version = "0.5.2"
@@ -5261,6 +5539,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]]
name = "ring"
version = "0.17.8"
@@ -5853,6 +6137,15 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -6740,7 +7033,7 @@ dependencies = [
"libc",
"log",
"memmem",
"num-derive",
"num-derive 0.3.3",
"num-traits",
"ordered-float",
"regex",
@@ -7494,6 +7787,17 @@ dependencies = [
"serde",
]
[[package]]
name = "v_frame"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
@@ -8823,6 +9127,30 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "4.2.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.0.0"
version = "2.0.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -36,6 +36,7 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
image = "0.25.5"
tauri = { version = "2.1.1", features = [
"protocol-asset",
"devtools",

View File

@@ -16,6 +16,12 @@
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"fs:allow-app-read",
"fs:allow-app-read-recursive",
"fs:allow-appcache-read",
"fs:allow-appcache-read-recursive",
"fs:allow-appconfig-read",
"fs:allow-appconfig-read-recursive",
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",

View File

@@ -697,25 +697,62 @@ Var VC_REDIST_URL
Var VC_REDIST_EXE
Section CheckAndInstallVSRuntime
; 检查是否已安装 Visual C++ Redistributable
${If} ${IsNativeARM64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${ElseIf} ${RunningX64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
IfFileExists "$WINDIR\SysWOW64\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${Else}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${EndIf}
; 下载并安装VC运行库
checkInstall:
; 检查注册表
${If} ${RunningX64}
SetRegView 64
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${ARCH}" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${Else}
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${EndIf}
; 如果没有安装,则下载并安装
DetailPrint "正在下载 Visual C++ Redistributable..."
nsisdl::download "$VC_REDIST_URL" "$TEMP\$VC_REDIST_EXE"
Pop $0
${If} $0 == "success"
nsExec::Exec '"$TEMP\$VC_REDIST_EXE" /quiet /norestart'
DetailPrint "正在安装 Visual C++ Redistributable..."
ExecWait '"$TEMP\$VC_REDIST_EXE" /quiet /norestart' $0
${If} $0 == 0
DetailPrint "Visual C++ Redistributable 安装成功"
${Else}
DetailPrint "Visual C++ Redistributable 安装失败"
${EndIf}
Delete "$TEMP\$VC_REDIST_EXE"
${Else}
DetailPrint "Visual C++ Redistributable 下载失败"
${EndIf}
Done:
@@ -728,6 +765,40 @@ Section Install
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete old files before installation
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -848,6 +919,35 @@ Section Uninstall
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
@@ -862,6 +962,10 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"

View File

@@ -33,7 +33,7 @@ impl IClashTemp {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into());
tun.insert("stack".into(), "mixed".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());

View File

@@ -4,114 +4,184 @@ use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use std::collections::HashMap;
use std::env::{consts::OS, temp_dir};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use zip::write::SimpleFileOptions;
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
const TIMEOUT_DELETE: u64 = 3; // 删除超时 30 秒
#[derive(Clone)]
struct WebDavConfig {
url: String,
username: String,
password: String,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
enum Operation {
Upload,
Download,
List,
Delete,
}
impl Operation {
fn timeout(&self) -> u64 {
match self {
Operation::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE,
}
}
}
pub struct WebDavClient {
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
client: Arc::new(Mutex::new(None)),
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
if self.client.lock().is_none() {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg =
"Unable to create web dav client, please make sure the webdav config is correct"
.to_string();
log::error!(target: "app","{}",msg);
return Err(anyhow::Error::msg(msg));
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
return Ok(client.clone());
}
let url = verge.webdav_url.unwrap_or_default();
let username = verge.webdav_username.unwrap_or_default();
let password = verge.webdav_password.unwrap_or_default();
let url = url.trim_end_matches('/');
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(3))
.build()
.unwrap(),
)
.set_host(url.to_owned())
.set_auth(reqwest_dav::Auth::Basic(
username.to_owned(),
password.to_owned(),
))
.build()?;
if (client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await)
.is_err()
{
client.mkcol(dirs::BACKUP_DIR).await?;
}
*self.client.lock() = Some(client.clone());
}
Ok(self.client.lock().clone().unwrap())
// 获取或创建配置
let config = {
let mut lock = self.config.lock();
if let Some(cfg) = lock.as_ref() {
cfg.clone()
} else {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg = "Unable to create web dav client, please make sure the webdav config is correct".to_string();
return Err(anyhow::Error::msg(msg));
}
let config = WebDavConfig {
url: verge
.webdav_url
.unwrap_or_default()
.trim_end_matches('/')
.to_string(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
};
*lock = Some(config.clone());
config
}
};
// 创建新的客户端
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.build()
.unwrap(),
)
.set_host(config.url)
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
.build()?;
// 确保备份目录存在
let list_result = client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await;
if list_result.is_err() {
client.mkcol(dirs::BACKUP_DIR).await?;
}
// 缓存客户端
{
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
}
Ok(client)
}
pub fn reset(&self) {
if !self.client.lock().is_none() {
self.client.lock().take();
}
*self.config.lock() = None;
self.clients.lock().clear();
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Upload).await?;
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client
.put(webdav_path.as_ref(), fs::read(file_path)?)
.await?;
let fut = client.put(webdav_path.as_ref(), fs::read(file_path)?);
timeout(Duration::from_secs(TIMEOUT_UPLOAD), fut).await??;
Ok(())
}
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Download).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
Ok::<(), Error>(())
};
timeout(Duration::from_secs(TIMEOUT_DOWNLOAD), fut).await??;
Ok(())
}
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::List).await?;
let path = format!("{}/", dirs::BACKUP_DIR);
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
let fut = async {
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
}
}
}
Ok(final_files)
Ok::<Vec<ListFile>, Error>(final_files)
};
Ok(timeout(Duration::from_secs(TIMEOUT_LIST), fut).await??)
}
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Delete).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client.delete(&path).await?;
let fut = client.delete(&path);
timeout(Duration::from_secs(TIMEOUT_DELETE), fut).await??;
Ok(())
}
}

View File

@@ -96,16 +96,11 @@ impl Tray {
*tun_mode,
)?));
#[cfg(target_os = "macos")]
let mut use_custom_icon = false;
#[allow(unused)]
let mut indication_icon = if *system_proxy && !*tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-sys.ico").to_vec()
}
"colorful" => include_bytes!("../../icons/tray-icon-sys.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
};
@@ -120,19 +115,12 @@ impl Tray {
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-tun.ico").to_vec()
}
"colorful" => include_bytes!("../../icons/tray-icon-tun.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
};
@@ -147,19 +135,12 @@ impl Tray {
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon.ico").to_vec()
}
"colorful" => include_bytes!("../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
};
@@ -174,22 +155,16 @@ impl Tray {
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
};
#[cfg(target_os = "macos")]
{
if use_custom_icon {
let _ = tray.set_icon_as_template(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
} else {
let _ = tray.set_icon_as_template(true);
}
let is_template = crate::utils::help::is_monochrome_image_from_bytes(&indication_icon)
.unwrap_or(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
let _ = tray.set_icon_as_template(is_template);
}
#[cfg(not(target_os = "macos"))]

View File

@@ -60,6 +60,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_merge,
global_script,
profile_name,
dns,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
@@ -130,6 +131,10 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.and_then(|item| item.name.clone())
.unwrap_or_default();
let dns = current.get("dns").map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
(
current,
merge,
@@ -140,6 +145,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_merge,
global_script,
name,
dns,
)
};
@@ -259,7 +265,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
}
config = use_tun(config, enable_tun).await;
config = use_tun(config, enable_tun, dns).await;
config = use_sort(config);
let mut exists_set = HashSet::new();

View File

@@ -18,7 +18,7 @@ macro_rules! append {
};
}
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
pub async fn use_tun(mut config: Mapping, enable: bool, origin_dns_val: Mapping) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
@@ -29,19 +29,52 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let ipv6_key = Value::from("ipv6");
let ipv6_val = config
.get(&ipv6_key)
.and_then(|v| v.as_bool())
.unwrap_or(false);
if enable {
revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", true);
revise!(dns_val, "ipv6", ipv6_val);
revise!(dns_val, "enhanced-mode", "fake-ip");
revise!(dns_val, "fake-ip-range", "10.96.0.0/16");
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("10.96.0.2".to_string()).await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
}
} else {
revise!(dns_val, "enhanced-mode", "redir-host");
revise!(
dns_val,
"enable",
origin_dns_val
.get("enable")
.and_then(|v| v.as_bool())
.unwrap_or(true)
);
revise!(dns_val, "ipv6", ipv6_val);
revise!(
dns_val,
"enhanced-mode",
origin_dns_val
.get("enhanced-mode")
.and_then(|v| v.as_str())
.unwrap_or("redir-host")
);
revise!(
dns_val,
"fake-ip-range",
origin_dns_val
.get("fake-ip-range")
.and_then(|v| v.as_str())
.unwrap_or("198.18.0.1/16")
);
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
}

View File

@@ -20,12 +20,16 @@ use tauri_plugin_window_state::{AppHandleExt, StateFlags};
// 打开面板
pub fn open_or_close_dashboard() {
if let Some(window) = handle::Handle::global().get_window() {
if let Ok(true) = window.is_focused() {
// 如果窗口存在,则切换其显示状态
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
return;
} else {
let _ = window.show();
let _ = window.set_focus();
}
} else {
resolve::create_window();
}
resolve::create_window();
}
// 重启clash
@@ -121,14 +125,6 @@ pub fn quit(code: Option<i32>) {
handle::Handle::global().set_is_exiting();
resolve::resolve_reset();
log_err!(handle::Handle::global().get_window().unwrap().close());
match app_handle.save_window_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state saved successfully");
}
Err(e) => {
log::error!(target: "app", "failed to save window state: {}", e);
}
};
app_handle.exit(code.unwrap_or(0));
}
@@ -141,16 +137,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
CoreManager::global().restart_core().await?;
handle::Handle::refresh_clash();
} else {
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
}
Config::runtime().latest().patch_config(patch);
update_core_config(false).await?;
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
<Result<()>>::Ok(())
};
match res {
@@ -250,11 +244,11 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
should_update_systray_part = true;
}
if should_restart_core {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if should_update_clash_config {
update_core_config(false).await?;
CoreManager::global().update_config().await?;
handle::Handle::refresh_clash();
}
if should_update_launch {
sysopt::Sysopt::global().update_launch()?;
@@ -320,31 +314,20 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
};
if should_update {
update_core_config(true).await?;
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target: "app", "{err}");
}
}
}
Ok(())
}
/// 更新订阅
async fn update_core_config(notice: bool) -> Result<()> {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
if notice {
handle::Handle::notice_message("set_config::ok", "ok");
}
Ok(())
}
Err(err) => {
if notice {
handle::Handle::notice_message("set_config::error", format!("{err}"));
}
Err(err)
}
}
}
/// copy env variable
pub fn copy_clash_env() {
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -464,6 +447,12 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
}
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge();
let verge_data = verge.data().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let backup_storage_path = app_home_dir().unwrap().join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
@@ -477,6 +466,15 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
zip.extract(app_home_dir()?)?;
log_err!(
patch_verge(IVerge {
webdav_url: webdav_url,
webdav_username: webdav_username,
webdav_password: webdav_password,
..IVerge::default()
})
.await
);
// 最后删除临时文件
fs::remove_file(backup_storage_path)?;
Ok(())

View File

@@ -4,7 +4,6 @@ use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::{Mapping, Value};
use std::{fs, path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
/// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
@@ -99,12 +98,24 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
}
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
app.shell().open(path.to_string_lossy(), None).unwrap();
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
open::that_detached(&path.as_os_str())?;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
let img = image::load_from_memory(data)?;
let rgb_img = img.to_rgb8();
for pixel in rgb_img.pixels() {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return Ok(false);
}
}
Ok(true)
}
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> String {
use std::process::Command;

View File

@@ -193,12 +193,7 @@ pub fn init_resources() -> Result<()> {
let _ = fs::create_dir_all(&res_dir);
}
#[cfg(target_os = "windows")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(target_os = "macos")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(target_os = "linux")]
let file_list: [&str; 0] = [];
// copy the resource file
// if the source file is newer than the destination file, copy it over

View File

@@ -8,7 +8,6 @@ use percent_encoding::percent_decode_str;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::{App, Manager};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use url::Url;
//#[cfg(not(target_os = "linux"))]
@@ -128,63 +127,54 @@ pub fn create_window() {
return;
}
let builder = tauri::WebviewWindowBuilder::new(
#[cfg(target_os = "windows")]
let _ = {
let app_handle = app_handle.clone();
std::thread::spawn(move || {
tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.visible(false)
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.build()
}).join().unwrap()
}.unwrap();
#[cfg(target_os = "macos")]
let _ = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.build()
.unwrap();
#[cfg(target_os = "linux")]
let _ = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.visible(false)
.fullscreen(false)
.min_inner_size(600.0, 520.0);
#[cfg(target_os = "windows")]
let window = builder
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.visible(false)
.build().unwrap();
#[cfg(target_os = "macos")]
let window = builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build()
.unwrap();
#[cfg(target_os = "linux")]
let window = builder
.decorations(false)
.transparent(true)
.build()
.unwrap();
match window.restore_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state restored successfully");
}
Err(e) => {
log::error!(target: "app", "failed to restore window state: {}", e);
#[cfg(target_os = "windows")]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 636,
}))
.unwrap();
#[cfg(not(target_os = "windows"))]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 642,
}))
.unwrap();
}
};
.decorations(false)
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.transparent(true)
.build()
.unwrap();
}
pub async fn resolve_scheme(param: String) -> Result<()> {

View File

@@ -25,7 +25,7 @@
"devUrl": "http://localhost:3000/"
},
"productName": "Clash Verge",
"version": "2.0.0",
"version": "2.0.2",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": {
"updater": {

View File

@@ -49,7 +49,7 @@ export const Switch = styled((props: SwitchProps) => (
},
"& .MuiSwitch-track": {
borderRadius: 26 / 2,
backgroundColor: theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
backgroundColor: theme.palette.mode === "light" ? "#BBBBBB" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,

View File

@@ -38,7 +38,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
}
/>
);
}
},
);
interface InnerProps {

View File

@@ -59,7 +59,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { up: 0, down: 0 });
},
}
},
);
return () => {
@@ -69,7 +69,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
}
},
);
/* --------- meta memory information --------- */
@@ -96,7 +96,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { inuse: 0 });
},
}
},
);
return () => {
@@ -106,7 +106,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
}
},
);
const [up, upUnit] = parseTraffic(traffic.up);

View File

@@ -1,12 +1,13 @@
import { IconButton, Fade } from "@mui/material";
import { IconButton, Fade, SxProps, Theme } from "@mui/material";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
interface Props {
onClick: () => void;
show: boolean;
sx?: SxProps<Theme>;
}
export const ScrollTopButton = ({ onClick, show }: Props) => {
export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
return (
<Fade in={show}>
<IconButton
@@ -26,6 +27,7 @@ export const ScrollTopButton = ({ onClick, show }: Props) => {
: "rgba(0,0,0,0.2)",
},
visibility: show ? "visible" : "hidden",
...sx,
}}
>
<KeyboardArrowUpIcon />

View File

@@ -90,7 +90,7 @@ export const useCustomTheme = () => {
}
// css
const backgroundColor = mode === "light" ? "#f0f0f0" : "#2e303d";
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#54545480";
const dividerColor =
@@ -104,7 +104,7 @@ export const useCustomTheme = () => {
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(theme.palette.primary.main, 0.1)
alpha(theme.palette.primary.main, 0.1),
);
// inject css

View File

@@ -34,7 +34,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {
@@ -107,7 +107,7 @@ export const ProviderButton = () => {
const expire = sub?.Expire || 0;
const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
100,
);
return (
<>
@@ -213,7 +213,7 @@ const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
})
}),
);
const boxStyle = {

View File

@@ -25,7 +25,7 @@ export const useRenderList = (mode: string) => {
const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
{ refreshInterval: 45000 }
{ refreshInterval: 45000 },
);
const { verge } = useVerge();
@@ -78,7 +78,7 @@ export const useRenderList = (mode: string) => {
group.all,
group.name,
headState.filterText,
headState.sortType
headState.sortType,
);
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
@@ -97,7 +97,7 @@ export const useRenderList = (mode: string) => {
headState,
col,
proxyCol,
}))
})),
);
}
@@ -108,7 +108,7 @@ export const useRenderList = (mode: string) => {
group,
proxy,
headState,
}))
})),
);
}
return ret;

View File

@@ -32,7 +32,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {

View File

@@ -38,7 +38,7 @@ export interface BackupTableViewerProps {
page: number;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number
page: number,
) => void;
total: number;
onRefresh: () => Promise<void>;
@@ -109,7 +109,7 @@ export const BackupTableViewer = memo(
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to delete this backup file?")
t("Confirm to delete this backup file?"),
);
if (confirmed) {
await handleDelete(file.filename);
@@ -132,7 +132,7 @@ export const BackupTableViewer = memo(
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to restore this backup file?")
t("Confirm to restore this backup file?"),
);
if (confirmed) {
await handleRestore(file.filename);
@@ -181,7 +181,7 @@ export const BackupTableViewer = memo(
/>
</TableContainer>
);
}
},
);
function LinuxIcon(props: SVGProps<SVGSVGElement>) {

View File

@@ -48,7 +48,7 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
setPage(page);
},
[]
[],
);
const fetchAndSetBackupFiles = async () => {
@@ -95,8 +95,8 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
setDataSource(
backupFiles.slice(
page * DEFAULT_ROWS_PER_PAGE,
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE
)
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
),
);
}, [page, backupFiles]);
@@ -104,14 +104,14 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog
open={open}
title={t("Backup Setting")}
contentSx={{ width: 600, maxHeight: 800 }}
// contentSx={{ width: 600, maxHeight: 800 }}
okBtn={t("")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
disableOk
>
<Box sx={{ maxWidth: 800 }}>
<Box>
<BaseLoadingOverlay isLoading={isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<BackupConfigViewer

View File

@@ -103,7 +103,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Auto Close Connections")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Auto Close Connections Info")} />
<TooltipIcon
title={t("Auto Close Connections Info")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
checked={values.autoCloseConnection}
@@ -130,7 +133,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Enable Builtin Enhanced")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Enable Builtin Enhanced Info")} />
<TooltipIcon
title={t("Enable Builtin Enhanced Info")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
checked={values.enableBuiltinEnhanced}
@@ -196,7 +202,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Default Latency Test")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Default Latency Test Info")} />
<TooltipIcon
title={t("Default Latency Test Info")}
sx={{ opacity: "0.7" }}
/>
<TextField
autoComplete="new-password"
size="small"

View File

@@ -166,8 +166,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
? t("Enabled")
: t("Disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
? t("Enabled")
: t("Disabled")}
</Typography>
</FlexBox>
{!value.pac && (
@@ -202,7 +202,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Proxy Guard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} />
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
<Switch
edge="end"
disabled={!enabled}

View File

@@ -22,7 +22,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gvisor",
stack: "mixed",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
@@ -35,7 +35,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
open: () => {
setOpen(true);
setValues({
stack: clash?.tun.stack ?? "gvisor",
stack: clash?.tun.stack ?? "mixed",
device: clash?.tun.device ?? "Mihomo",
autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
@@ -64,7 +64,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
...(old! || {}),
tun,
}),
false
false,
);
try {
await enhanceProfiles();
@@ -89,7 +89,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
onClick={async () => {
let tun = {
stack: "gvisor",
stack: "mixed",
device: "Mihomo",
"auto-route": true,
"auto-detect-interface": true,
@@ -98,7 +98,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
mtu: 1500,
};
setValues({
stack: "gvisor",
stack: "mixed",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
@@ -112,7 +112,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
...(old! || {}),
tun,
}),
false
false,
);
}}
>

View File

@@ -76,10 +76,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
setDownloaded((a) => {
return a + e.payload.chunkLength;
});
}
},
);
try {
await updateInfo.install();
await updateInfo.downloadAndInstall();
await relaunch();
} catch (err: any) {
Notice.error(err?.message || err.toString());
@@ -100,7 +100,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
onClick={() => {
openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
);
}}
>

View File

@@ -139,7 +139,7 @@ const SettingClash = ({ onError }: Props) => {
>
<GuardState
// clash premium 2022.08.26 值为warn
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
value={logLevel === "warn" ? "warning" : (logLevel ?? "info")}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ "log-level": e })}
@@ -165,7 +165,7 @@ const SettingClash = ({ onError }: Props) => {
onClick={() => {
Notice.success(
t("Restart Application to Apply Modifications"),
1000
1000,
);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
@@ -209,7 +209,12 @@ const SettingClash = ({ onError }: Props) => {
<SettingItem
onClick={invoke_uwp_tool}
label={t("Open UWP tool")}
extra={<TooltipIcon title={t("Open UWP tool Info")} />}
extra={
<TooltipIcon
title={t("Open UWP tool Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
)}

View File

@@ -257,7 +257,16 @@ const SettingVerge = ({ onError }: Props) => {
label={t("Runtime Config")}
/>
<SettingItem onClick={openAppDir} label={t("Open Conf Dir")} />
<SettingItem
onClick={openAppDir}
label={t("Open Conf Dir")}
extra={
<TooltipIcon
title={t("Open Conf Dir Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />

View File

@@ -64,13 +64,18 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
try {
if (!form.name) throw new Error("`Name` should not be null");
if (!form.url) throw new Error("`Url` should not be null");
let newList;
let uid;
if (form.icon && form.icon.startsWith("<svg")) {
// 移除 icon 中的注释
if (form.icon) {
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
}
const doc = new DOMParser().parseFromString(
form.icon,
"image/svg+xml"
"image/svg+xml",
);
if (doc.querySelector("parsererror")) {
throw new Error("`Icon`svg format error");
@@ -97,7 +102,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
Notice.error(err.message || err.toString());
setLoading(false);
}
})
}),
);
const handleClose = () => {

View File

@@ -10,12 +10,12 @@ import {
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
"getRuntimeConfig",
getRuntimeConfig
getRuntimeConfig,
);
const { data: versionData, mutate: mutateVersion } = useSWR(
"getVersion",
getVersion
getVersion,
);
const patchClash = useLockFn(async (patch: Partial<IConfigData>) => {
@@ -26,8 +26,8 @@ export const useClash = () => {
const version = versionData?.premium
? `${versionData.version} Premium`
: versionData?.meta
? `${versionData.version} Mihomo`
: versionData?.version || "-";
? `${versionData.version} Mihomo`
: versionData?.version || "-";
return {
clash,
@@ -41,7 +41,7 @@ export const useClash = () => {
export const useClashInfo = () => {
const { data: clashInfo, mutate: mutateInfo } = useSWR(
"getClashInfo",
getClashInfo
getClashInfo,
);
const patchInfo = async (
@@ -56,7 +56,7 @@ export const useClashInfo = () => {
| "external-controller"
| "secret"
>
>
>,
) => {
const hasInfo =
patch["redir-port"] != null ||

View File

@@ -7,7 +7,7 @@ export const useListen = () => {
const addListener = async <T>(
eventName: string,
handler: EventCallback<T>
handler: EventCallback<T>,
) => {
const unlisten = await listen(eventName, handler);
unlistenFns.current.push(unlisten);

View File

@@ -21,7 +21,7 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
const params = new URLSearchParams();
if (secret) {
params.append("token", encodeURIComponent(secret));
params.append("token", secret);
}
if (logLevel === "all") {
params.append("level", "debug");

View File

@@ -335,6 +335,7 @@
"Backup Setting": "Backup Setting",
"Runtime Config": "Runtime Config",
"Open Conf Dir": "Open Conf Dir",
"Open Conf Dir Info": "If the software runs abnormally, BACKUP and delete all files in this folder than restart the software",
"Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
@@ -352,7 +353,6 @@
"Match Whole Word": "Match Whole Word",
"Use Regular Expression": "Use Regular Expression",
"Profile Imported Successfully": "Profile Imported Successfully",
"Clash Config Updated": "Clash Config Updated",
"Profile Switched": "Profile Switched",
"Profile Reactivated": "Profile Reactivated",
"Only YAML Files Supported": "Only YAML Files Supported",

View File

@@ -337,6 +337,7 @@
"Backup Setting Info": "از فایل های پیکربندی پشتیبان WebDAV پشتیبانی می کند",
"Runtime Config": "پیکربندی زمان اجرا",
"Open Conf Dir": "باز کردن پوشه برنامه",
"Open Conf Dir Info": "اگر نرم‌افزار به‌طور غیرعادی اجرا می‌شود، از تمام فایل‌های موجود در این پوشه نسخه پشتیبان تهیه و پاک کنید تا نرم‌افزار را مجدداً راه‌اندازی کنید",
"Open Core Dir": "باز کردن پوشه هسته",
"Open Logs Dir": "باز کردن پوشه لاگ‌ها",
"Check for Updates": "بررسی برای به‌روزرسانی‌ها",
@@ -354,7 +355,6 @@
"Match Whole Word": "تطبیق کل کلمه",
"Use Regular Expression": "استفاده از عبارت منظم",
"Profile Imported Successfully": "پروفایل با موفقیت وارد شد",
"Clash Config Updated": "پیکربندی Clash به‌روزرسانی شد",
"Profile Switched": "پروفایل تغییر یافت",
"Profile Reactivated": "پروفایل مجدداً فعال شد",
"Only YAML Files Supported": "فقط فایل‌های YAML پشتیبانی می‌شوند",

View File

@@ -337,6 +337,7 @@
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
"Runtime Config": "Используемый конфиг",
"Open Conf Dir": "Открыть папку приложения",
"Open Conf Dir Info": "Если программное обеспечение работает ненормально, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите программное обеспечение",
"Open Core Dir": "Открыть папку ядра",
"Open Logs Dir": "Открыть папку логов",
"Check for Updates": "Проверить обновления",
@@ -354,7 +355,6 @@
"Match Whole Word": "Полное совпадение слова",
"Use Regular Expression": "Использовать регулярные выражения",
"Profile Imported Successfully": "Профиль успешно импортирован",
"Clash Config Updated": "Clash конфигурация Обновлена",
"Profile Switched": "Профиль изменен",
"Profile Reactivated": "Профиль повторно активирован",
"Only YAML Files Supported": "Поддерживаются только файлы YAML",

View File

@@ -336,6 +336,7 @@
"Backup Setting": "备份设置",
"Backup Setting Info": "支持WebDAV备份配置文件",
"Runtime Config": "当前配置",
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
"Open Conf Dir": "配置目录",
"Open Core Dir": "内核目录",
"Open Logs Dir": "日志目录",
@@ -354,7 +355,6 @@
"Match Whole Word": "全字匹配",
"Use Regular Expression": "使用正则表达式",
"Profile Imported Successfully": "导入订阅成功",
"Clash Config Updated": "Clash 配置已更新",
"Profile Switched": "订阅已切换",
"Profile Reactivated": "订阅已激活",
"Only YAML Files Supported": "仅支持 YAML 文件",

View File

@@ -77,9 +77,6 @@ const Layout = () => {
navigate("/profile");
Notice.error(msg);
break;
case "set_config::ok":
Notice.success(t("Clash Config Updated"));
break;
case "set_config::error":
Notice.error(msg);
break;

View File

@@ -47,7 +47,7 @@ const ConnectionsPage = () => {
list.sort(
(a, b) =>
new Date(b.start || "0").getTime()! -
new Date(a.start || "0").getTime()!
new Date(a.start || "0").getTime()!,
),
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
"Download Speed": (list) =>
@@ -103,7 +103,7 @@ const ConnectionsPage = () => {
next(event);
},
},
3
3,
);
return () => {
@@ -114,7 +114,7 @@ const ConnectionsPage = () => {
const [filterConn, download, upload] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt];
let connections = connData.connections.filter((conn) =>
match(conn.metadata.host || conn.metadata.destinationIP || "")
match(conn.metadata.host || conn.metadata.destinationIP || ""),
);
if (orderFunc) connections = orderFunc(connections);
@@ -152,7 +152,7 @@ const ConnectionsPage = () => {
setSetting((o) =>
o?.layout !== "table"
? { ...o, layout: "table" }
: { ...o, layout: "list" }
: { ...o, layout: "list" },
)
}
>

View File

@@ -1,7 +1,7 @@
import useSWR, { mutate } from "swr";
import useSWR from "swr";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material";
import { Box, Button, IconButton, Stack, Divider, Grid2 } from "@mui/material";
import {
DndContext,
closestCenter,
@@ -25,7 +25,6 @@ import {
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import {
getProfiles,
importProfile,
enhanceProfiles,
getRuntimeLogs,
@@ -45,12 +44,14 @@ import { ProfileMore } from "@/components/profile/profile-more";
import { ProfileItem } from "@/components/profile/profile-item";
import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
import { add, throttle } from "lodash-es";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { useLocation } from "react-router-dom";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { TauriEvent } from "@tauri-apps/api/event";
const ProfilePage = () => {
const { t } = useTranslation();
@@ -64,35 +65,46 @@ const ProfilePage = () => {
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const { current } = location.state || {};
useEffect(() => {
const unlisten = addListener("tauri://file-drop", async (event) => {
const fileList = event.payload as string[];
for (let file of fileList) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error(t("Only YAML Files Supported"));
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
});
const handleFileDrop = async () => {
const unlisten = await addListener(
TauriEvent.DRAG_DROP,
async (event: any) => {
const paths = event.payload.paths;
for (let file of paths) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error(t("Only YAML Files Supported"));
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
},
);
return unlisten;
};
const unsubscribe = handleFileDrop();
return () => {
unlisten.then((fn) => fn());
unsubscribe.then((cleanup) => cleanup());
};
}, []);
@@ -105,7 +117,7 @@ const ProfilePage = () => {
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
"getRuntimeLogs",
getRuntimeLogs
getRuntimeLogs,
);
const viewerRef = useRef<ProfileViewerRef>(null);
@@ -117,9 +129,7 @@ const ProfilePage = () => {
const type1 = ["local", "remote"];
const profileItems = items.filter((i) => i && type1.includes(i.type!));
return profileItems;
return items.filter((i) => i && type1.includes(i.type!));
}, [profiles]);
const currentActivatings = () => {
@@ -240,7 +250,7 @@ const ProfilePage = () => {
setLoadingCache((cache) => {
// 获取没有正在更新的订阅
const items = profileItems.filter(
(e) => e.type === "remote" && !cache[e.uid]
(e) => e.type === "remote" && !cache[e.uid],
);
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
@@ -372,14 +382,14 @@ const ProfilePage = () => {
onDragEnd={onDragEnd}
>
<Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={profileItems.map((x) => {
return x.uid;
})}
>
{profileItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<Grid2 size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={item.file}>
<ProfileItem
id={item.uid}
selected={profiles.current === item.uid}
@@ -394,10 +404,10 @@ const ProfilePage = () => {
}}
onDelete={() => onDelete(item.uid)}
/>
</Grid>
</Grid2>
))}
</SortableContext>
</Grid>
</Grid2>
</Box>
</DndContext>
<Divider
@@ -406,8 +416,8 @@ const ProfilePage = () => {
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
<Box sx={{ mt: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid item xs={12} sm={6} md={6} lg={6}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<ProfileMore
id="Merge"
onSave={async (prev, curr) => {
@@ -416,8 +426,8 @@ const ProfilePage = () => {
}
}}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={6}>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<ProfileMore
id="Script"
logInfo={chainLogs["Script"]}
@@ -427,8 +437,8 @@ const ProfilePage = () => {
}
}}
/>
</Grid>
</Grid>
</Grid2>
</Grid2>
</Box>
</Box>

View File

@@ -15,7 +15,7 @@ const ProxyPage = () => {
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig
getClashConfig,
);
const { verge } = useVerge();

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { useVerge } from "@/hooks/use-verge";
import { Box, Button, Grid } from "@mui/material";
import { Box, Button } from "@mui/material";
import Grid2 from "@mui/material/Grid2";
import {
DndContext,
closestCenter,
@@ -21,6 +22,7 @@ import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { TestItem } from "@/components/test/test-item";
import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid";
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
// test icons
import apple from "@/assets/image/test/apple.svg?raw";
@@ -34,7 +36,7 @@ const TestPage = () => {
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const { verge, mutateVerge, patchVerge } = useVerge();
@@ -68,7 +70,7 @@ const TestPage = () => {
const onTestListItemChange = (
uid: string,
patch?: Partial<IVergeTestItem>
patch?: Partial<IVergeTestItem>,
) => {
if (patch) {
const newList = testList.map((x) => {
@@ -120,6 +122,19 @@ const TestPage = () => {
}, [verge]);
const viewerRef = useRef<TestViewerRef>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => {
containerRef.current?.scrollTo({
top: 0,
behavior: "smooth",
});
};
const handleScroll = (e: any) => {
setShowScrollTop(e.target.scrollTop > 100);
};
return (
<BasePage
@@ -145,10 +160,15 @@ const TestPage = () => {
}
>
<Box
ref={containerRef}
onScroll={handleScroll}
sx={{
pt: 1.25,
mb: 0.5,
px: "10px",
height: "calc(100vh - 100px)",
overflow: "auto",
position: "relative",
}}
>
<DndContext
@@ -157,26 +177,41 @@ const TestPage = () => {
onDragEnd={onDragEnd}
>
<Box sx={{ mb: 4.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={testList.map((x) => {
return x.uid;
})}
>
{testList.map((item) => (
<Grid item xs={6} sm={4} md={3} lg={2} key={item.uid}>
<Grid2
component={"div"}
size={{ xs: 6, lg: 2, sm: 4, md: 3 }}
key={item.uid}
>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
</Grid2>
))}
</SortableContext>
</Grid>
</Grid2>
</Box>
</DndContext>
<ScrollTopButton
onClick={scrollToTop}
show={showScrollTop}
sx={{
position: "absolute",
bottom: "20px",
left: "20px",
zIndex: 1000,
}}
/>
</Box>
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
</BasePage>

View File

@@ -20,7 +20,7 @@ export const useConnectionSetting = () =>
{
serializer: JSON.stringify,
deserializer: JSON.parse,
}
},
);
// save the state of each profile item loading

View File

@@ -6,7 +6,7 @@ import Sockette, { type SocketteOptions } from "sockette";
export const createSockette = (
url: string,
opt: SocketteOptions,
maxError = 10
maxError = 10,
) => {
let remainRetryCount = maxError;

View File

@@ -7,7 +7,7 @@ import monacoEditorPlugin, {
type IMonacoEditorOpts,
} from "vite-plugin-monaco-editor";
const monacoEditorPluginDefault = (monacoEditorPlugin as any).default as (
options: IMonacoEditorOpts
options: IMonacoEditorOpts,
) => any;
export default defineConfig({