Compare commits
31 Commits
@@ -104,8 +104,8 @@ jobs:
|
||||
|
||||
# Generate autobuild version using autobuild-latest format
|
||||
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||
MONTH=$(date +%m)
|
||||
DAY=$(date +%d)
|
||||
MONTH=$(TZ=Asia/Shanghai date +%m)
|
||||
DAY=$(TZ=Asia/Shanghai date +%d)
|
||||
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||
|
||||
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
|
||||
|
||||
4
.github/workflows/clean-old-assets.yml
vendored
4
.github/workflows/clean-old-assets.yml
vendored
@@ -88,8 +88,8 @@ jobs:
|
||||
|
||||
# Generate autobuild version for consistency
|
||||
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||
MONTH=$(date +%m)
|
||||
DAY=$(date +%d)
|
||||
MONTH=$(TZ=Asia/Shanghai date +%m)
|
||||
DAY=$(TZ=Asia/Shanghai date +%d)
|
||||
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||
|
||||
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -122,10 +122,10 @@ jobs:
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -455,7 +455,7 @@ jobs:
|
||||
release-update:
|
||||
name: Release Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release-for-linux-arm]
|
||||
needs: [update_tag]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -480,7 +480,7 @@ jobs:
|
||||
|
||||
release-update-for-fixed-webview2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-for-fixed-webview2]
|
||||
needs: [update_tag]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -605,10 +605,10 @@ jobs:
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
29
UPDATELOG.md
29
UPDATELOG.md
@@ -1,3 +1,32 @@
|
||||
## v2.4.2
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 增加托盘节点选择
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化前端首页加载速度
|
||||
- 优化前端未使用 i18n 文件缓存呢
|
||||
- 优化后端内存占用
|
||||
- 优化后端启动速度
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页节点切换失效的问题
|
||||
- 修复和优化服务检查流程
|
||||
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||
- 修复 rpm/deb 包名称问题
|
||||
- 修复托盘轻量模式状态检测异常
|
||||
- 修复通过 scheme 导入订阅崩溃
|
||||
- 修复单例检测实效
|
||||
- 修复启动阶段可能导致的无法连接内核
|
||||
- 修复导入订阅无法 Auth Basic
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 简化和改进代理设置样式
|
||||
|
||||
## v2.4.1
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -35,26 +35,26 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/lab": "7.0.0-beta.16",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@mui/x-data-grid": "^8.11.0",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/x-data-grid": "^8.11.1",
|
||||
"@tauri-apps/api": "2.8.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-notification": "^2.3.1",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.4",
|
||||
"ahooks": "^3.9.5",
|
||||
"axios": "^1.11.0",
|
||||
"cli-color": "^2.0.4",
|
||||
"dayjs": "1.11.16",
|
||||
"dayjs": "1.11.18",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next": "^25.5.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -78,8 +78,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@tauri-apps/cli": "2.8.3",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@tauri-apps/cli": "2.8.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.12",
|
||||
@@ -89,19 +89,21 @@
|
||||
"adm-zip": "^0.5.16",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"jiti": "^2.5.1",
|
||||
"meta-json-schema": "^1.19.12",
|
||||
"meta-json-schema": "^1.19.13",
|
||||
"node-fetch": "^3.3.2",
|
||||
"path": "^0.12.7",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.91.0",
|
||||
"terser": "^5.43.1",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.92.1",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.41.0",
|
||||
"vite": "^7.1.3",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"vite": "^7.1.4",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
|
||||
2482
pnpm-lock.yaml
generated
2482
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -68,14 +68,24 @@ function getLatestTauriCommit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
||||
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||
* 使用 Asia/Shanghai 时区
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp(withCommit = false) {
|
||||
const now = new Date();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(now);
|
||||
const month = parts.find((part) => part.type === "month").value;
|
||||
const day = parts.find((part) => part.type === "day").value;
|
||||
|
||||
if (withCommit) {
|
||||
const gitShort = getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
|
||||
@@ -75,7 +75,7 @@ async function sendTelegramNotification() {
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
const encodedVersion = encodeURIComponent(version);
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||
|
||||
// 发送到 Telegram
|
||||
try {
|
||||
|
||||
154
src-tauri/Cargo.lock
generated
154
src-tauri/Cargo.lock
generated
@@ -1089,7 +1089,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -1152,7 +1152,7 @@ dependencies = [
|
||||
"warp",
|
||||
"winapi",
|
||||
"winreg 0.55.0",
|
||||
"zip",
|
||||
"zip 5.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1391,6 +1391,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -1688,17 +1703,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -1723,6 +1727,27 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "destructure_traitobject"
|
||||
version = "0.2.0"
|
||||
@@ -3819,26 +3844,6 @@ dependencies = [
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d"
|
||||
dependencies = [
|
||||
"liblzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma-sys"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
@@ -3931,9 +3936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -3946,29 +3951,30 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
|
||||
|
||||
[[package]]
|
||||
name = "log4rs"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6"
|
||||
checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"chrono",
|
||||
"derivative",
|
||||
"derive_more 2.0.1",
|
||||
"fnv",
|
||||
"humantime",
|
||||
"libc",
|
||||
"log",
|
||||
"log-mdc",
|
||||
"once_cell",
|
||||
"mock_instant",
|
||||
"parking_lot 0.12.4",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.16",
|
||||
"thread-id",
|
||||
"typemap-ors",
|
||||
"unicode-segmentation",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -3987,6 +3993,16 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -4149,6 +4165,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mock_instant"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6"
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.1"
|
||||
@@ -6241,7 +6263,7 @@ checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"derive_more 0.99.20",
|
||||
"fxhash",
|
||||
"log",
|
||||
"phf 0.8.0",
|
||||
@@ -7009,9 +7031,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.8.4"
|
||||
version = "2.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d545ccf7b60dcd44e07c6fb5aeb09140966f0aabd5d2aa14a6821df7bc99348"
|
||||
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7064,9 +7086,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b"
|
||||
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -7173,9 +7195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.2"
|
||||
version = "2.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d430110d4ee102a9b673d3c03ff48098c80fe8ca71ba1ff52d8a5919538a1a6"
|
||||
checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
@@ -7221,9 +7243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.3.3"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee5a3c416dc59d7d9aa0de5490a82d6e201c67ffe97388979d77b69b08cda40"
|
||||
checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -7353,7 +7375,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
"zip 4.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7610,12 +7632,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "4.2.2"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea"
|
||||
checksum = "99043e46c5a15af379c06add30d9c93a6c0e8849de00d244c4a2c417da128d80"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8295,6 +8317,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -9772,9 +9800,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.5.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835eb39822904d39cb19465de1159e05d371973f0c6df3a365ad50565ddc8b9"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.11.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9fdfa5f34b5980f2c21b3a2c68c09ade4debddc7be52c51056695effc73a08c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
@@ -9786,7 +9826,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"hmac",
|
||||
"indexmap 2.11.0",
|
||||
"liblzma",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -13,16 +13,16 @@ build = "build.rs"
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.4.0", features = [] }
|
||||
tauri-build = { version = "2.4.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.99"
|
||||
dirs = "6.0"
|
||||
open = "5.3.2"
|
||||
log = "0.4.27"
|
||||
log = "0.4.28"
|
||||
dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
log4rs = "1.4.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = { version = "0.37.0", features = ["network", "system"] }
|
||||
@@ -44,7 +44,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
regex = "1.11.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.8.4", features = [
|
||||
tauri = { version = "2.8.5", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -53,14 +53,14 @@ tauri = { version = "2.8.4", features = [
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.1"
|
||||
tauri-plugin-dialog = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
tauri-plugin-fs = "2.4.2"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.2"
|
||||
tauri-plugin-deep-link = "2.4.3"
|
||||
tauri-plugin-devtools = "2.0.1"
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
zip = "4.5.0"
|
||||
zip = "5.0.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use tauri::Emitter;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type};
|
||||
use crate::{
|
||||
core::{handle::Handle, tray::Tray},
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
state::proxy::ProxyRequestCache,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
@@ -7,11 +15,11 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = IpcManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||
let manager = IpcManager::global();
|
||||
manager.get_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
@@ -32,11 +40,11 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = IpcManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("providers", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||
let manager = IpcManager::global();
|
||||
manager.get_providers_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
@@ -45,3 +53,69 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
/// 同步托盘和GUI的代理选择状态
|
||||
#[tauri::command]
|
||||
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
use crate::core::tray::Tray;
|
||||
|
||||
match Tray::global().update_menu().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "Tray proxy selection synced successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新代理选择并同步托盘和GUI状态
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
|
||||
match IpcManager::global().update_proxy(&group, &proxy).await {
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"Proxy updated successfully: {} -> {}",
|
||||
group,
|
||||
proxy
|
||||
);
|
||||
|
||||
let cache = crate::state::proxy::ProxyRequestCache::global();
|
||||
let key = crate::state::proxy::ProxyRequestCache::make_key("proxies", "default");
|
||||
cache.map.remove(&key);
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e);
|
||||
}
|
||||
|
||||
if let Some(app_handle) = Handle::global().app_handle() {
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
let _ = app_handle.emit("verge://refresh-proxy-config", ());
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"Proxy and sync completed successfully: {} -> {}",
|
||||
group,
|
||||
proxy
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to update proxy: {} -> {}, error: {}",
|
||||
group,
|
||||
proxy,
|
||||
e
|
||||
);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,21 +1071,22 @@ impl CoreManager {
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
self.start_core_by_service().await?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// 服务不可用,检查用户偏好
|
||||
let service_state = service::ServiceState::get().await;
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
// 服务不可用,检查用户偏好
|
||||
let service_state = service::ServiceState::get().await;
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1102,7 +1103,6 @@ impl CoreManager {
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
self.stop_core().await?;
|
||||
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::{
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.1.1"; // 定义所需的服务版本号
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.1.2"; // 定义所需的服务版本号
|
||||
|
||||
// 限制重装时间和次数的常量
|
||||
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::Emitter;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::ipc::Rate;
|
||||
@@ -7,7 +8,9 @@ use crate::process::AsyncHandler;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
feat,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
singleton_lazy,
|
||||
utils::{dirs::find_target_icons, i18n::t},
|
||||
@@ -281,6 +284,16 @@ impl Tray {
|
||||
.unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
|
||||
// 获取代理节点
|
||||
let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to fetch proxies for tray menu: {e}"
|
||||
);
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
});
|
||||
|
||||
match app_handle.tray_by_id("main") {
|
||||
Some(tray) => {
|
||||
let _ = tray.set_menu(Some(
|
||||
@@ -291,6 +304,7 @@ impl Tray {
|
||||
*tun_mode,
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
proxy_nodes_data,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
@@ -557,9 +571,21 @@ async fn create_tray_menu(
|
||||
tun_mode_enabled: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
is_lightweight_mode: bool,
|
||||
proxy_nodes_data: serde_json::Value,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
|
||||
// 获取当前配置文件的选中代理组信息
|
||||
let current_profile_selected = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_ref = profiles_config.latest_ref();
|
||||
profiles_ref
|
||||
.get_current()
|
||||
.and_then(|uid| profiles_ref.get_item(&uid).ok())
|
||||
.and_then(|profile| profile.selected.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let hotkeys = Config::verge()
|
||||
@@ -606,12 +632,117 @@ async fn create_tray_menu(
|
||||
results.into_iter().collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
|
||||
// 代理组子菜单
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus = Vec::new();
|
||||
|
||||
if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) {
|
||||
for (group_name, group_data) in proxies.iter() {
|
||||
// Filter groups based on mode
|
||||
let should_show = match mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
};
|
||||
|
||||
if !should_show {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
// Create proxy items
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_name| proxy_name.as_str())
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
// Get delay for display
|
||||
let delay_text = proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|p| p.get("history"))
|
||||
.and_then(|h| h.as_array())
|
||||
.and_then(|h| h.last())
|
||||
.and_then(|r| r.get("delay"))
|
||||
.and_then(|d| d.as_i64())
|
||||
.map(|delay| match delay {
|
||||
-1 => "-ms".to_string(),
|
||||
delay if delay >= 10000 => "-ms".to_string(),
|
||||
_ => format!("{}ms", delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".to_string());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
item_id,
|
||||
display_text,
|
||||
true,
|
||||
is_selected,
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if group is active
|
||||
let is_group_active = match mode {
|
||||
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
|
||||
"direct" => false,
|
||||
_ => {
|
||||
current_profile_selected
|
||||
.iter()
|
||||
.any(|s| s.name.as_deref() == Some(group_name))
|
||||
&& !now_proxy.is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
let group_display_name = if is_group_active {
|
||||
format!("✓ {}", group_name)
|
||||
} else {
|
||||
group_name.to_string()
|
||||
};
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> = group_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
submenus.push(submenu);
|
||||
} else {
|
||||
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submenus
|
||||
};
|
||||
|
||||
// Pre-fetch all localized strings
|
||||
let dashboard_text = t("Dashboard").await;
|
||||
let rule_mode_text = t("Rule Mode").await;
|
||||
let global_mode_text = t("Global Mode").await;
|
||||
let direct_mode_text = t("Direct Mode").await;
|
||||
let profiles_text = t("Profiles").await;
|
||||
let proxies_text = t("Proxies").await;
|
||||
let system_proxy_text = t("System Proxy").await;
|
||||
let tun_mode_text = t("TUN Mode").await;
|
||||
let lightweight_mode_text = t("LightWeight Mode").await;
|
||||
@@ -675,6 +806,24 @@ async fn create_tray_menu(
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
// 创建代理主菜单
|
||||
let proxies_submenu = if !proxy_submenus.is_empty() {
|
||||
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
|
||||
.iter()
|
||||
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
Some(Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"proxies",
|
||||
proxies_text,
|
||||
true,
|
||||
&proxy_submenu_refs,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
@@ -772,26 +921,37 @@ async fn create_tray_menu(
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
// 动态构建菜单项
|
||||
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![
|
||||
open_window,
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
];
|
||||
|
||||
// 如果有代理节点,添加代理节点菜单
|
||||
if let Some(ref proxies_menu) = proxies_submenu {
|
||||
menu_items.push(proxies_menu);
|
||||
}
|
||||
|
||||
menu_items.extend_from_slice(&[
|
||||
separator,
|
||||
system_proxy as &dyn IsMenuItem<Wry>,
|
||||
tun_mode as &dyn IsMenuItem<Wry>,
|
||||
separator,
|
||||
lighteweight_mode as &dyn IsMenuItem<Wry>,
|
||||
copy_env as &dyn IsMenuItem<Wry>,
|
||||
open_dir as &dyn IsMenuItem<Wry>,
|
||||
more as &dyn IsMenuItem<Wry>,
|
||||
separator,
|
||||
quit as &dyn IsMenuItem<Wry>,
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle)
|
||||
.items(&[
|
||||
open_window,
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
separator,
|
||||
quit,
|
||||
])
|
||||
.items(&menu_items)
|
||||
.build()?;
|
||||
Ok(menu)
|
||||
}
|
||||
@@ -819,11 +979,11 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
logging!(info, Type::Lightweight, true, "Exiting Lightweight Mode");
|
||||
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
|
||||
}
|
||||
let result = WindowManager::show_main_window().await; // Await async function
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
logging!(info, Type::Window, true, "Show Main Window: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy().await; // Await async function
|
||||
@@ -853,7 +1013,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window().await; // Await async function
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
logging!(info, Type::Window, true, "Show Main Window: {result:?}");
|
||||
} else {
|
||||
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async
|
||||
}
|
||||
@@ -865,6 +1025,42 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into()).await; // Await async function
|
||||
}
|
||||
id if id.starts_with("proxy_") => {
|
||||
// proxy_{group_name}_{proxy_name}
|
||||
let parts: Vec<&str> = id.splitn(3, '_').collect();
|
||||
|
||||
if parts.len() == 3 && parts[0] == "proxy" {
|
||||
let group_name = parts[1];
|
||||
let proxy_name = parts[2];
|
||||
|
||||
match cmd::proxy::update_proxy_and_sync(
|
||||
group_name.to_string(),
|
||||
proxy_name.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
|
||||
|
||||
// Fallback to IPC update
|
||||
if (IpcManager::global()
|
||||
.update_proxy(group_name, proxy_name)
|
||||
.await)
|
||||
.is_ok()
|
||||
{
|
||||
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@ use std::time::Duration;
|
||||
|
||||
use kode_bridge::{
|
||||
errors::{AnyError, AnyResult},
|
||||
pool::PoolConfig,
|
||||
ClientConfig, IpcHttpClient, LegacyResponse,
|
||||
};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
use crate::{
|
||||
logging, singleton_with_logging,
|
||||
utils::{dirs::ipc_path, logging::Type},
|
||||
};
|
||||
|
||||
// 定义用于URL路径的编码集合,只编码真正必要的字符
|
||||
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||
.add(b' ') // 空格
|
||||
@@ -16,51 +20,34 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||
.add(b'&') // 和号
|
||||
.add(b'%'); // 百分号
|
||||
|
||||
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
|
||||
|
||||
// Helper function to create AnyError from string
|
||||
fn create_error(msg: impl Into<String>) -> AnyError {
|
||||
Box::new(std::io::Error::other(msg.into()))
|
||||
}
|
||||
|
||||
pub struct IpcManager {
|
||||
ipc_path: String,
|
||||
config: ClientConfig,
|
||||
client: IpcHttpClient,
|
||||
}
|
||||
|
||||
impl IpcManager {
|
||||
fn new() -> Self {
|
||||
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
|
||||
logging!(
|
||||
error,
|
||||
crate::utils::logging::Type::Ipc,
|
||||
true,
|
||||
"Failed to get IPC path: {}",
|
||||
e
|
||||
);
|
||||
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
|
||||
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
|
||||
});
|
||||
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
|
||||
Self {
|
||||
ipc_path: ipc_path.to_string(),
|
||||
config: ClientConfig {
|
||||
default_timeout: Duration::from_secs(5),
|
||||
enable_pooling: true,
|
||||
max_retries: 3,
|
||||
max_concurrent_requests: 32,
|
||||
max_requests_per_second: Some(5.0),
|
||||
pool_config: PoolConfig {
|
||||
max_size: 32,
|
||||
min_idle: 2,
|
||||
max_idle_time_ms: 10_000,
|
||||
max_retries: 3,
|
||||
max_concurrent_requests: 32,
|
||||
max_requests_per_second: Some(5.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
let config = ClientConfig {
|
||||
default_timeout: Duration::from_secs(5),
|
||||
enable_pooling: false,
|
||||
max_retries: 4,
|
||||
retry_delay: Duration::from_millis(125),
|
||||
max_concurrent_requests: 16,
|
||||
max_requests_per_second: Some(64.0),
|
||||
..Default::default()
|
||||
};
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +61,7 @@ impl IpcManager {
|
||||
path: &str,
|
||||
body: Option<&serde_json::Value>,
|
||||
) -> AnyResult<LegacyResponse> {
|
||||
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?;
|
||||
client.request(method, path, body).await
|
||||
self.client.request(method, path, body).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::{
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use config::Config;
|
||||
use parking_lot::Mutex;
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
@@ -36,7 +35,7 @@ mod app_init {
|
||||
|
||||
/// Initialize singleton monitoring for other instances
|
||||
pub fn init_singleton_check() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
AsyncHandler::spawn_blocking(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_millis(500), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
@@ -81,8 +80,7 @@ mod app_init {
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_devtools::init());
|
||||
}
|
||||
|
||||
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
|
||||
builder
|
||||
}
|
||||
|
||||
/// Setup deep link handling
|
||||
@@ -96,7 +94,7 @@ mod app_init {
|
||||
app.deep_link().on_open_url(|event| {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
if let Some(url) = url {
|
||||
tokio::task::spawn_local(async move {
|
||||
AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = resolve::resolve_scheme(url).await {
|
||||
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
@@ -185,6 +183,8 @@ mod app_init {
|
||||
cmd::get_proxies,
|
||||
cmd::force_refresh_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
cmd::update_proxy_and_sync,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::check_dns_config_exists,
|
||||
@@ -280,17 +280,16 @@ pub fn run() {
|
||||
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_uppercase();
|
||||
let session_env = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
||||
let is_kde_desktop = desktop_env.contains("KDE");
|
||||
let is_wayland_session = session_env.contains("wayland");
|
||||
let is_plasma_desktop = desktop_env.contains("PLASMA");
|
||||
|
||||
if is_kde_desktop && is_wayland_session {
|
||||
std::env::set_var("GDK_BACKEND", "x11");
|
||||
if is_kde_desktop || is_plasma_desktop {
|
||||
std::env::set_var("GTK_CSD", "0");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"KDE Wayland detected: Switched to X11 backend for better titlebar stability."
|
||||
"KDE detected: Disabled GTK CSD for better titlebar stability."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -331,9 +330,9 @@ pub fn run() {
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
resolve::resolve_setup_sync(app_handle);
|
||||
resolve::resolve_setup_handle(app_handle);
|
||||
resolve::resolve_setup_async();
|
||||
resolve::resolve_setup_sync();
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
|
||||
@@ -3,16 +3,14 @@ use crate::{
|
||||
core::{handle, timer::Timer, tray::Tray},
|
||||
log_err, logging,
|
||||
process::AsyncHandler,
|
||||
state::lightweight::LightWeightState,
|
||||
utils::logging::Type,
|
||||
state::proxy::ProxyRequestCache,
|
||||
utils::{logging::Type, window_manager::WindowManager},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::logging_error;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
@@ -21,23 +19,20 @@ const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
// 添加退出轻量模式的锁,防止并发调用
|
||||
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn with_lightweight_status<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut LightWeightState) -> R,
|
||||
{
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
// Try to get state, but don't panic if it's not managed yet
|
||||
if let Some(state) = app_handle.try_state::<Mutex<LightWeightState>>() {
|
||||
let mut guard = state.lock();
|
||||
Some(f(&mut guard))
|
||||
} else {
|
||||
// State not managed yet, return None
|
||||
None
|
||||
}
|
||||
static IS_IN_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn inner_set_lightweight_mode(value: bool) -> bool {
|
||||
if value {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
} else {
|
||||
// App handle not available yet
|
||||
None
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||
}
|
||||
IS_IN_LIGHTWEIGHT.store(value, Ordering::SeqCst);
|
||||
value
|
||||
}
|
||||
|
||||
fn inner_get_lightweight_mode() -> bool {
|
||||
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst) || !WindowManager::is_main_window_exists()
|
||||
}
|
||||
|
||||
pub async fn run_once_auto_lightweight() {
|
||||
@@ -68,55 +63,33 @@ pub async fn run_once_auto_lightweight() {
|
||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||
);
|
||||
|
||||
if with_lightweight_status(|_| ()).is_some() {
|
||||
set_lightweight_mode(false).await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auto_lightweight_mode_init() -> Result<()> {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
// Check if state is available before accessing it
|
||||
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
|
||||
);
|
||||
return Err(anyhow::anyhow!("LightWeightState has not been initialized"));
|
||||
}
|
||||
let is_silent_start =
|
||||
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_auto_light_weight_mode
|
||||
}
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_silent_start =
|
||||
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_auto_light_weight_mode
|
||||
}
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_lightweight_mode(true).await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_lightweight_mode(true).await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -124,16 +97,13 @@ pub async fn auto_lightweight_mode_init() -> Result<()> {
|
||||
|
||||
// 检查是否处于轻量模式
|
||||
pub fn is_in_lightweight_mode() -> bool {
|
||||
with_lightweight_status(|state| state.is_lightweight).unwrap_or(false)
|
||||
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
pub async fn set_lightweight_mode(value: bool) {
|
||||
if with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
if inner_get_lightweight_mode() != value {
|
||||
inner_set_lightweight_mode(value);
|
||||
// 只有在状态可用时才触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
@@ -178,9 +148,10 @@ pub async fn entry_lightweight_mode() {
|
||||
}
|
||||
set_lightweight_mode(true).await;
|
||||
let _ = cancel_light_weight_timer();
|
||||
ProxyRequestCache::global().clean_default_keys();
|
||||
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
logging_error!(Type::Lightweight, true, Tray::global().update_part().await);
|
||||
}
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
|
||||
@@ -34,6 +34,7 @@ impl AsyncHandler {
|
||||
async_runtime::spawn_blocking(f)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
pub fn block_on<Fut>(fut: Fut) -> Fut::Output
|
||||
where
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::sync::{Arc, Once, OnceLock};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LightWeightState {
|
||||
#[allow(unused)]
|
||||
once: Arc<Once>,
|
||||
pub is_lightweight: bool,
|
||||
}
|
||||
|
||||
impl LightWeightState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
once: Arc::new(Once::new()),
|
||||
is_lightweight: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
||||
self.is_lightweight = value;
|
||||
if value {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
} else {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LightWeightState {
|
||||
fn default() -> Self {
|
||||
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
|
||||
INSTANCE.get_or_init(LightWeightState::new).clone()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
|
||||
// https://tauri.app/develop/state-management/#do-you-need-arc
|
||||
|
||||
pub mod lightweight;
|
||||
pub mod proxy;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// use crate::utils::logging::Type;
|
||||
// use crate::{logging, singleton};
|
||||
use crate::singleton;
|
||||
use dashmap::DashMap;
|
||||
use serde_json::Value;
|
||||
@@ -11,7 +13,7 @@ pub struct CacheEntry {
|
||||
}
|
||||
|
||||
pub struct ProxyRequestCache {
|
||||
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
|
||||
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry>>>>,
|
||||
}
|
||||
|
||||
impl ProxyRequestCache {
|
||||
@@ -54,10 +56,10 @@ impl ProxyRequestCache {
|
||||
|
||||
// Try to set a new value
|
||||
let value = fetch_fn().await;
|
||||
let entry = CacheEntry {
|
||||
let entry = Box::new(CacheEntry {
|
||||
value: Arc::new(value),
|
||||
expires_at: Instant::now() + ttl,
|
||||
};
|
||||
});
|
||||
|
||||
match cell.set(entry) {
|
||||
Ok(_) => {
|
||||
@@ -78,6 +80,22 @@ impl ProxyRequestCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn clean_default_keys(&self) {
|
||||
// logging!(info, Type::Cache, "Cleaning proxies keys");
|
||||
// let proxies_key = Self::make_key("proxies", "default");
|
||||
// self.map.remove(&proxies_key);
|
||||
|
||||
// logging!(info, Type::Cache, "Cleaning providers keys");
|
||||
// let providers_key = Self::make_key("providers", "default");
|
||||
// self.map.remove(&providers_key);
|
||||
|
||||
// !The frontend goes crash if we clean the clash_config cache
|
||||
// logging!(info, Type::Cache, "Cleaning clash config keys");
|
||||
// let clash_config_key = Self::make_key("clash_config", "default");
|
||||
// self.map.remove(&clash_config_key);
|
||||
}
|
||||
}
|
||||
|
||||
// Use singleton macro
|
||||
|
||||
@@ -405,14 +405,6 @@ pub async fn init_resources() -> Result<()> {
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let dest_path = app_dir.join(file);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Setup,
|
||||
true,
|
||||
"src_path: {:?}, dest_path: {:?}",
|
||||
src_path,
|
||||
dest_path
|
||||
);
|
||||
|
||||
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
|
||||
match fs::copy(&src, &dest).await {
|
||||
@@ -445,14 +437,6 @@ pub async fn init_resources() -> Result<()> {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Setup,
|
||||
true,
|
||||
"skipping resource copy '{}'",
|
||||
file
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum Type {
|
||||
Network,
|
||||
ProxyMode,
|
||||
Ipc,
|
||||
// Cache,
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
@@ -39,6 +40,7 @@ impl fmt::Display for Type {
|
||||
Type::Network => write!(f, "[Network]"),
|
||||
Type::ProxyMode => write!(f, "[ProxMode]"),
|
||||
Type::Ipc => write!(f, "[IPC]"),
|
||||
// Type::Cache => write!(f, "[Cache]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use isahc::http::{
|
||||
header::{HeaderMap, HeaderValue, USER_AGENT},
|
||||
StatusCode, Uri,
|
||||
};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use isahc::prelude::*;
|
||||
use isahc::{
|
||||
config::RedirectPolicy,
|
||||
http::{
|
||||
header::{HeaderMap, HeaderValue, USER_AGENT},
|
||||
StatusCode, Uri,
|
||||
},
|
||||
};
|
||||
use isahc::{config::SslOption, HttpClient};
|
||||
use std::sync::Once;
|
||||
use std::time::{Duration, Instant};
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::Url;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -53,7 +57,6 @@ pub struct NetworkManager {
|
||||
self_proxy_client: Mutex<Option<HttpClient>>,
|
||||
system_proxy_client: Mutex<Option<HttpClient>>,
|
||||
no_proxy_client: Mutex<Option<HttpClient>>,
|
||||
init: Once,
|
||||
last_connection_error: Mutex<Option<(Instant, String)>>,
|
||||
connection_error_count: Mutex<usize>,
|
||||
}
|
||||
@@ -64,16 +67,11 @@ impl NetworkManager {
|
||||
self_proxy_client: Mutex::new(None),
|
||||
system_proxy_client: Mutex::new(None),
|
||||
no_proxy_client: Mutex::new(None),
|
||||
init: Once::new(),
|
||||
last_connection_error: Mutex::new(None),
|
||||
connection_error_count: Mutex::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self) {
|
||||
self.init.call_once(|| {});
|
||||
}
|
||||
|
||||
async fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock().await;
|
||||
*last_error = Some((Instant::now(), error.to_string()));
|
||||
@@ -135,6 +133,8 @@ impl NetworkManager {
|
||||
builder = builder.timeout(Duration::from_secs(secs));
|
||||
}
|
||||
|
||||
builder = builder.redirect_policy(RedirectPolicy::Follow);
|
||||
|
||||
Ok(builder.build()?)
|
||||
};
|
||||
|
||||
@@ -197,15 +197,40 @@ impl NetworkManager {
|
||||
self.reset_clients().await;
|
||||
}
|
||||
|
||||
let parsed = Url::parse(url)?;
|
||||
let mut extra_headers = HeaderMap::new();
|
||||
|
||||
if !parsed.username().is_empty() {
|
||||
if let Some(pass) = parsed.password() {
|
||||
let auth_str = format!("{}:{}", parsed.username(), pass);
|
||||
let encoded = general_purpose::STANDARD.encode(auth_str);
|
||||
extra_headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Basic {}", encoded))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let clean_url = {
|
||||
let mut no_auth = parsed.clone();
|
||||
no_auth.set_username("").ok();
|
||||
no_auth.set_password(None).ok();
|
||||
no_auth.to_string()
|
||||
};
|
||||
|
||||
let client = self
|
||||
.create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs)
|
||||
.await?;
|
||||
|
||||
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20));
|
||||
let url_owned = url.to_string();
|
||||
|
||||
let response = match timeout(timeout_duration, async {
|
||||
let mut response = client.get_async(&url_owned).await?;
|
||||
let mut req = isahc::Request::get(&clean_url);
|
||||
|
||||
for (k, v) in extra_headers.iter() {
|
||||
req = req.header(k, v);
|
||||
}
|
||||
|
||||
let mut response = client.send_async(req.body(())?).await?;
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body = response.text().await?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
logging, logging_error,
|
||||
module::lightweight::auto_lightweight_mode_init,
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, network::NetworkManager, resolve::window::create_window, server},
|
||||
utils::{init, logging::Type, resolve::window::create_window, server},
|
||||
};
|
||||
|
||||
pub mod dns;
|
||||
@@ -16,11 +16,15 @@ pub mod ui;
|
||||
pub mod window;
|
||||
pub mod window_script;
|
||||
|
||||
pub fn resolve_setup_sync(app_handle: AppHandle) {
|
||||
pub fn resolve_setup_handle(app_handle: AppHandle) {
|
||||
init_handle(app_handle);
|
||||
init_scheme();
|
||||
init_embed_server();
|
||||
NetworkManager::new().init();
|
||||
}
|
||||
|
||||
pub fn resolve_setup_sync() {
|
||||
AsyncHandler::spawn(|| async {
|
||||
AsyncHandler::spawn_blocking(init_scheme);
|
||||
AsyncHandler::spawn_blocking(init_embed_server);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn resolve_setup_async() {
|
||||
@@ -33,28 +37,30 @@ pub fn resolve_setup_async() {
|
||||
std::thread::current().id()
|
||||
);
|
||||
|
||||
// AsyncHandler::spawn_blocking(|| AsyncHandler::block_on(init_work_config()));
|
||||
|
||||
AsyncHandler::spawn(|| async {
|
||||
init_work_config().await;
|
||||
init_resources().await;
|
||||
init_startup_script().await;
|
||||
futures::join!(
|
||||
init_work_config(),
|
||||
init_resources(),
|
||||
init_startup_script(),
|
||||
init_hotkey(),
|
||||
);
|
||||
|
||||
init_timer().await;
|
||||
init_hotkey().await;
|
||||
init_auto_lightweight_mode().await;
|
||||
|
||||
init_verge_config().await;
|
||||
init_core_manager().await;
|
||||
init_system_proxy().await;
|
||||
|
||||
init_system_proxy().await;
|
||||
AsyncHandler::spawn_blocking(|| {
|
||||
init_system_proxy_guard();
|
||||
});
|
||||
|
||||
init_window().await;
|
||||
init_tray().await;
|
||||
refresh_tray_menu().await
|
||||
let tray_and_refresh = async {
|
||||
init_tray().await;
|
||||
refresh_tray_menu().await;
|
||||
};
|
||||
futures::join!(init_window(), tray_and_refresh,);
|
||||
});
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
@@ -44,7 +44,7 @@ pub async fn check_singleton() -> Result<()> {
|
||||
pub fn embed_server() {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
AsyncHandler::spawn_blocking(move || async move {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let visible = warp::path!("commands" / "visible").and_then(|| async {
|
||||
Ok::<_, warp::Rejection>(warp::reply::with_status(
|
||||
"ok".to_string(),
|
||||
|
||||
@@ -338,6 +338,11 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查窗口是否存在
|
||||
pub fn is_main_window_exists() -> bool {
|
||||
Self::get_main_window().is_some()
|
||||
}
|
||||
|
||||
/// 检查窗口是否可见
|
||||
pub fn is_main_window_visible() -> bool {
|
||||
Self::get_main_window()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -29,10 +29,9 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||
import { updateProxy, deleteConnection } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
|
||||
// 本地存储的键名
|
||||
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||
@@ -94,8 +93,18 @@ export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { verge } = useVerge();
|
||||
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
|
||||
const { proxies, clashConfig, refreshProxy } = useAppData();
|
||||
|
||||
// 统一代理选择器
|
||||
const { handleSelectChange } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
refreshProxy();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("代理切换失败", error);
|
||||
refreshProxy();
|
||||
},
|
||||
});
|
||||
|
||||
// 判断模式
|
||||
const mode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||
@@ -113,8 +122,6 @@ export const CurrentProxyCard = () => {
|
||||
proxyData: {
|
||||
groups: { name: string; now: string; all: string[] }[];
|
||||
records: Record<string, any>;
|
||||
globalProxy: string;
|
||||
directProxy: any;
|
||||
};
|
||||
selection: {
|
||||
group: string;
|
||||
@@ -127,8 +134,6 @@ export const CurrentProxyCard = () => {
|
||||
proxyData: {
|
||||
groups: [],
|
||||
records: {},
|
||||
globalProxy: "",
|
||||
directProxy: { name: "DIRECT" }, // 默认值避免 undefined
|
||||
},
|
||||
selection: {
|
||||
group: "",
|
||||
@@ -253,8 +258,6 @@ export const CurrentProxyCard = () => {
|
||||
proxyData: {
|
||||
groups: filteredGroups,
|
||||
records: proxies.records || {},
|
||||
globalProxy: proxies.global?.now || "",
|
||||
directProxy: proxies.records?.DIRECT || { name: "DIRECT" },
|
||||
},
|
||||
selection: {
|
||||
group: newGroup,
|
||||
@@ -310,7 +313,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 处理代理节点变更
|
||||
const handleProxyChange = useCallback(
|
||||
async (event: SelectChangeEvent) => {
|
||||
(event: SelectChangeEvent) => {
|
||||
if (isDirectMode) return;
|
||||
|
||||
const newProxy = event.target.value;
|
||||
@@ -330,35 +333,15 @@ export const CurrentProxyCard = () => {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
|
||||
try {
|
||||
await updateProxy(currentGroup, newProxy);
|
||||
|
||||
// 自动关闭连接设置
|
||||
if (verge?.auto_close_connection && previousProxy) {
|
||||
connections.data.forEach((conn: any) => {
|
||||
if (conn.chains.includes(previousProxy)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 延长刷新延迟时间
|
||||
setTimeout(() => {
|
||||
refreshProxy();
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error("更新代理失败", error);
|
||||
}
|
||||
const skipConfigSave = isGlobalMode || isDirectMode;
|
||||
handleSelectChange(currentGroup, previousProxy, skipConfigSave)(event);
|
||||
},
|
||||
[
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
state.proxyData.records,
|
||||
state.selection,
|
||||
verge?.auto_close_connection,
|
||||
refreshProxy,
|
||||
debouncedSetState,
|
||||
connections.data,
|
||||
handleSelectChange,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
import {
|
||||
getConnections,
|
||||
providerHealthCheck,
|
||||
updateProxy,
|
||||
deleteConnection,
|
||||
getGroupProxyDelays,
|
||||
} from "@/services/cmds";
|
||||
import { forceRefreshProxies } from "@/services/cmds";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { providerHealthCheck, getGroupProxyDelays } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { BaseEmpty } from "../base";
|
||||
import { useRenderList } from "./use-render-list";
|
||||
import { ProxyRender } from "./proxy-render";
|
||||
@@ -203,7 +196,17 @@ export const ProxyGroups = (props: Props) => {
|
||||
const { renderList, onProxies, onHeadState } = useRenderList(mode);
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { current, patchCurrent } = useProfiles();
|
||||
|
||||
// 统代理选择
|
||||
const { handleProxyGroupChange } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
onProxies();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("代理切换失败", error);
|
||||
onProxies();
|
||||
},
|
||||
});
|
||||
|
||||
// 获取自动滚动开关状态,默认为 true
|
||||
const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true;
|
||||
@@ -335,44 +338,13 @@ export const ProxyGroups = (props: Props) => {
|
||||
[letterIndexMap],
|
||||
);
|
||||
|
||||
// 切换分组的节点代理
|
||||
const handleChangeProxy = useLockFn(
|
||||
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||
const handleChangeProxy = useCallback(
|
||||
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
|
||||
|
||||
const { name, now } = group;
|
||||
await updateProxy(name, proxy.name);
|
||||
|
||||
await forceRefreshProxies();
|
||||
|
||||
onProxies();
|
||||
|
||||
// 断开连接
|
||||
if (verge?.auto_close_connection) {
|
||||
getConnections().then(({ connections }) => {
|
||||
connections.forEach((conn) => {
|
||||
if (conn.chains.includes(now!)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 保存到selected中
|
||||
if (!current) return;
|
||||
if (!current.selected) current.selected = [];
|
||||
|
||||
const index = current.selected.findIndex(
|
||||
(item) => item.name === group.name,
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
current.selected.push({ name, now: proxy.name });
|
||||
} else {
|
||||
current.selected[index] = { name, now: proxy.name };
|
||||
}
|
||||
await patchCurrent({ selected: current.selected });
|
||||
handleProxyGroupChange(group, proxy);
|
||||
},
|
||||
[handleProxyGroupChange],
|
||||
);
|
||||
|
||||
// 测全部延迟
|
||||
|
||||
@@ -19,7 +19,7 @@ import getSystem from "@/utils/get-system";
|
||||
import { routers } from "@/pages/_routers";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { ContentCopyRounded } from "@mui/icons-material";
|
||||
import { languages } from "@/services/i18n";
|
||||
import { supportedLanguages } from "@/services/i18n";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
interface Props {
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const languageOptions = Object.entries(languages).map(([code, _]) => {
|
||||
const languageOptions = supportedLanguages.map((code) => {
|
||||
const labels: { [key: string]: string } = {
|
||||
en: "English",
|
||||
ru: "Русский",
|
||||
@@ -39,8 +39,13 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
|
||||
ar: "العربية",
|
||||
ko: "한국어",
|
||||
tr: "Türkçe",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
jp: "日本語",
|
||||
zhtw: "繁體中文",
|
||||
};
|
||||
return { code, label: labels[code] };
|
||||
const label = labels[code] || code;
|
||||
return { code, label };
|
||||
});
|
||||
|
||||
const SettingVergeBasic = ({ onError }: Props) => {
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
DeleteForeverRounded,
|
||||
WarningRounded,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Box, Typography, alpha, useTheme } from "@mui/material";
|
||||
import { DialogRef, Switch } from "@/components/base";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { GuardState } from "@/components/setting/mods/guard-state";
|
||||
import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer";
|
||||
import { TunViewer } from "@/components/setting/mods/tun-viewer";
|
||||
@@ -100,19 +94,6 @@ const ProxyControlSwitches = ({
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{label && (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "15px",
|
||||
fontWeight: "500",
|
||||
mb: 0.5,
|
||||
display: "none",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 仅显示当前选中的开关 */}
|
||||
{isSystemProxyMode && (
|
||||
<Box
|
||||
@@ -131,50 +112,36 @@ const ProxyControlSwitches = ({
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{systemProxyActualState ? (
|
||||
<PlayCircleOutlineRounded
|
||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
||||
/>
|
||||
<PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
|
||||
) : (
|
||||
<PauseCircleOutlineRounded
|
||||
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }}
|
||||
sx={{ color: "text.disabled", mr: 1 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 500, fontSize: "15px" }}
|
||||
>
|
||||
{t("System Proxy")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Tooltip title={t("System Proxy Info")} arrow>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
color: "text.secondary",
|
||||
"&:hover": { color: "primary.main" },
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => sysproxyRef.current?.open()}
|
||||
>
|
||||
<SettingsRounded fontSize="small" />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<GuardState
|
||||
value={systemProxyActualState}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onGuard={(e) => toggleSystemProxy(e)}
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 500, fontSize: "15px" }}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
{t("System Proxy")}
|
||||
</Typography>
|
||||
<TooltipIcon
|
||||
title={t("System Proxy Info")}
|
||||
icon={SettingsRounded}
|
||||
onClick={() => sysproxyRef.current?.open()}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<GuardState
|
||||
value={systemProxyActualState}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onGuard={(e) => toggleSystemProxy(e)}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -196,109 +163,87 @@ const ProxyControlSwitches = ({
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{enable_tun_mode ? (
|
||||
<PlayCircleOutlineRounded
|
||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
||||
/>
|
||||
<PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
|
||||
) : (
|
||||
<PauseCircleOutlineRounded
|
||||
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }}
|
||||
sx={{ color: "text.disabled", mr: 1 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 500, fontSize: "15px" }}
|
||||
>
|
||||
{t("Tun Mode")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 500, fontSize: "15px" }}
|
||||
>
|
||||
{t("Tun Mode")}
|
||||
</Typography>
|
||||
<TooltipIcon
|
||||
title={t("Tun Mode Info")}
|
||||
icon={SettingsRounded}
|
||||
onClick={() => tunRef.current?.open()}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
|
||||
{!isTunAvailable && (
|
||||
<Tooltip
|
||||
<TooltipIcon
|
||||
title={t("TUN requires Service Mode or Admin Mode")}
|
||||
arrow
|
||||
>
|
||||
<WarningRounded sx={{ color: "warning.main", ml: 1 }} />
|
||||
</Tooltip>
|
||||
icon={WarningRounded}
|
||||
sx={{ color: "warning.main", ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{!isTunAvailable && (
|
||||
<Tooltip title={t("Install Service")} arrow>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={onInstallService}
|
||||
sx={{ mr: 1, minWidth: "32px", p: "4px" }}
|
||||
>
|
||||
<BuildRounded fontSize="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<TooltipIcon
|
||||
title={t("Install Service")}
|
||||
icon={BuildRounded}
|
||||
color="primary"
|
||||
onClick={onInstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isServiceMode && (
|
||||
<Tooltip title={t("Uninstall Service")} arrow>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={onUninstallService}
|
||||
sx={{ mr: 1, minWidth: "32px", p: "4px" }}
|
||||
>
|
||||
<DeleteForeverRounded fontSize="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<TooltipIcon
|
||||
title={t("Uninstall Service")}
|
||||
icon={DeleteForeverRounded}
|
||||
color="secondary"
|
||||
onClick={onUninstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title={t("Tun Mode Info")} arrow>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
color: "text.secondary",
|
||||
"&:hover": { color: "primary.main" },
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => tunRef.current?.open()}
|
||||
>
|
||||
<SettingsRounded fontSize="small" />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<GuardState
|
||||
value={enable_tun_mode ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => {
|
||||
if (!isTunAvailable) {
|
||||
showNotice(
|
||||
"error",
|
||||
t("TUN requires Service Mode or Admin Mode"),
|
||||
);
|
||||
return Promise.reject(
|
||||
new Error(t("TUN requires Service Mode or Admin Mode")),
|
||||
);
|
||||
}
|
||||
onChangeData({ enable_tun_mode: e });
|
||||
}}
|
||||
onGuard={(e) => {
|
||||
if (!isTunAvailable) {
|
||||
showNotice(
|
||||
"error",
|
||||
t("TUN requires Service Mode or Admin Mode"),
|
||||
);
|
||||
return Promise.reject(
|
||||
new Error(t("TUN requires Service Mode or Admin Mode")),
|
||||
);
|
||||
}
|
||||
return patchVerge({ enable_tun_mode: e });
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" disabled={!isTunAvailable} />
|
||||
</GuardState>
|
||||
</Box>
|
||||
|
||||
<GuardState
|
||||
value={enable_tun_mode ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => {
|
||||
if (!isTunAvailable) {
|
||||
showNotice(
|
||||
"error",
|
||||
t("TUN requires Service Mode or Admin Mode"),
|
||||
);
|
||||
return Promise.reject(
|
||||
new Error(t("TUN requires Service Mode or Admin Mode")),
|
||||
);
|
||||
}
|
||||
onChangeData({ enable_tun_mode: e });
|
||||
}}
|
||||
onGuard={(e) => {
|
||||
if (!isTunAvailable) {
|
||||
showNotice(
|
||||
"error",
|
||||
t("TUN requires Service Mode or Admin Mode"),
|
||||
);
|
||||
return Promise.reject(
|
||||
new Error(t("TUN requires Service Mode or Admin Mode")),
|
||||
);
|
||||
}
|
||||
return patchVerge({ enable_tun_mode: e });
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" disabled={!isTunAvailable} />
|
||||
</GuardState>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
45
src/hooks/use-i18n.ts
Normal file
45
src/hooks/use-i18n.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { changeLanguage, supportedLanguages } from "@/services/i18n";
|
||||
import { useVerge } from "./use-verge";
|
||||
|
||||
export const useI18n = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const { patchVerge } = useVerge();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const switchLanguage = useCallback(
|
||||
async (language: string) => {
|
||||
if (!supportedLanguages.includes(language)) {
|
||||
console.warn(`Unsupported language: ${language}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (i18n.language === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await changeLanguage(language);
|
||||
|
||||
if (patchVerge) {
|
||||
await patchVerge({ language });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change language:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[i18n.language, patchVerge],
|
||||
);
|
||||
|
||||
return {
|
||||
currentLanguage: i18n.language,
|
||||
supportedLanguages,
|
||||
switchLanguage,
|
||||
isLoading,
|
||||
t,
|
||||
};
|
||||
};
|
||||
143
src/hooks/use-proxy-selection.ts
Normal file
143
src/hooks/use-proxy-selection.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
updateProxy,
|
||||
updateProxyAndSync,
|
||||
forceRefreshProxies,
|
||||
syncTrayProxySelection,
|
||||
getConnections,
|
||||
deleteConnection,
|
||||
} from "@/services/cmds";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
|
||||
// 缓存连接清理
|
||||
const cleanupConnections = async (previousProxy: string) => {
|
||||
try {
|
||||
const { connections } = await getConnections();
|
||||
const cleanupPromises = connections
|
||||
.filter((conn) => conn.chains.includes(previousProxy))
|
||||
.map((conn) => deleteConnection(conn.id));
|
||||
|
||||
if (cleanupPromises.length > 0) {
|
||||
await Promise.allSettled(cleanupPromises);
|
||||
console.log(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[ProxySelection] 连接清理失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProxySelectionOptions {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: any) => void;
|
||||
enableConnectionCleanup?: boolean;
|
||||
}
|
||||
|
||||
// 代理选择 Hook
|
||||
export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
const { current, patchCurrent } = useProfiles();
|
||||
const { verge } = useVerge();
|
||||
|
||||
const { onSuccess, onError, enableConnectionCleanup = true } = options;
|
||||
|
||||
// 缓存
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
autoCloseConnection: verge?.auto_close_connection ?? false,
|
||||
enableConnectionCleanup,
|
||||
}),
|
||||
[verge?.auto_close_connection, enableConnectionCleanup],
|
||||
);
|
||||
|
||||
// 切换节点
|
||||
const changeProxy = useLockFn(
|
||||
async (
|
||||
groupName: string,
|
||||
proxyName: string,
|
||||
previousProxy?: string,
|
||||
skipConfigSave: boolean = false,
|
||||
) => {
|
||||
console.log(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`);
|
||||
|
||||
try {
|
||||
if (current && !skipConfigSave) {
|
||||
if (!current.selected) current.selected = [];
|
||||
|
||||
const index = current.selected.findIndex(
|
||||
(item) => item.name === groupName,
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
current.selected.push({ name: groupName, now: proxyName });
|
||||
} else {
|
||||
current.selected[index] = { name: groupName, now: proxyName };
|
||||
}
|
||||
await patchCurrent({ selected: current.selected });
|
||||
}
|
||||
|
||||
await updateProxyAndSync(groupName, proxyName);
|
||||
console.log(
|
||||
`[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`,
|
||||
);
|
||||
|
||||
onSuccess?.();
|
||||
|
||||
if (
|
||||
config.enableConnectionCleanup &&
|
||||
config.autoCloseConnection &&
|
||||
previousProxy
|
||||
) {
|
||||
setTimeout(() => cleanupConnections(previousProxy), 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ProxySelection] 代理切换失败: ${groupName} -> ${proxyName}`,
|
||||
error,
|
||||
);
|
||||
|
||||
try {
|
||||
await updateProxy(groupName, proxyName);
|
||||
await forceRefreshProxies();
|
||||
await syncTrayProxySelection();
|
||||
onSuccess?.();
|
||||
console.log(
|
||||
`[ProxySelection] 代理切换回退成功: ${groupName} -> ${proxyName}`,
|
||||
);
|
||||
} catch (fallbackError) {
|
||||
console.error(
|
||||
`[ProxySelection] 代理切换回退也失败: ${groupName} -> ${proxyName}`,
|
||||
fallbackError,
|
||||
);
|
||||
onError?.(fallbackError);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(
|
||||
groupName: string,
|
||||
previousProxy?: string,
|
||||
skipConfigSave: boolean = false,
|
||||
) =>
|
||||
(event: { target: { value: string } }) => {
|
||||
const newProxy = event.target.value;
|
||||
changeProxy(groupName, newProxy, previousProxy, skipConfigSave);
|
||||
},
|
||||
[changeProxy],
|
||||
);
|
||||
|
||||
const handleProxyGroupChange = useCallback(
|
||||
(group: { name: string; now?: string }, proxy: { name: string }) => {
|
||||
changeProxy(group.name, proxy.name, group.now);
|
||||
},
|
||||
[changeProxy],
|
||||
);
|
||||
|
||||
return {
|
||||
changeProxy,
|
||||
handleSelectChange,
|
||||
handleProxyGroupChange,
|
||||
};
|
||||
};
|
||||
60
src/main.tsx
60
src/main.tsx
@@ -13,7 +13,7 @@ import { ComposeContextProvider } from "foxact/compose-context-provider";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import Layout from "./pages/_layout";
|
||||
import "./services/i18n";
|
||||
import { initializeLanguage } from "./services/i18n";
|
||||
import {
|
||||
LoadingCacheProvider,
|
||||
ThemeModeProvider,
|
||||
@@ -39,29 +39,47 @@ document.addEventListener("keydown", (event) => {
|
||||
["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
|
||||
event.key.toUpperCase(),
|
||||
));
|
||||
disabledShortcuts && event.preventDefault();
|
||||
if (disabledShortcuts) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const contexts = [
|
||||
<ThemeModeProvider />,
|
||||
<LoadingCacheProvider />,
|
||||
<UpdateStateProvider />,
|
||||
];
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
await initializeLanguage("zh");
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
const contexts = [
|
||||
<ThemeModeProvider key="theme" />,
|
||||
<LoadingCacheProvider key="loading" />,
|
||||
<UpdateStateProvider key="update" />,
|
||||
];
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[main.tsx] 应用初始化失败:", error);
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div style={{ padding: "20px", color: "red" }}>
|
||||
应用初始化失败: {error instanceof Error ? error.message : String(error)}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
|
||||
// 错误处理
|
||||
window.addEventListener("error", (event) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import i18next from "i18next";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { SWRConfig, mutate } from "swr";
|
||||
import { useEffect, useCallback, useState, useRef } from "react";
|
||||
@@ -11,6 +10,7 @@ import { routers } from "./_routers";
|
||||
import { getAxios } from "@/services/api";
|
||||
import { forceRefreshClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||
import iconLight from "@/assets/image/icon_light.svg?react";
|
||||
import iconDark from "@/assets/image/icon_dark.svg?react";
|
||||
@@ -158,6 +158,7 @@ const Layout = () => {
|
||||
const [enableLog] = useEnableLog();
|
||||
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
||||
const { language, start_page } = verge ?? {};
|
||||
const { switchLanguage } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routersEles = useRoutes(routers);
|
||||
@@ -439,9 +440,9 @@ const Layout = () => {
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||
i18next.changeLanguage(language);
|
||||
switchLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
}, [language, switchLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (start_page) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
@@ -26,17 +27,34 @@ import {
|
||||
import { ProxyTunCard } from "@/components/home/proxy-tun-card";
|
||||
import { ClashModeCard } from "@/components/home/clash-mode-card";
|
||||
import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats";
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo, Suspense, lazy, useCallback } from "react";
|
||||
import { HomeProfileCard } from "@/components/home/home-profile-card";
|
||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||
import { CurrentProxyCard } from "@/components/home/current-proxy-card";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { ClashInfoCard } from "@/components/home/clash-info-card";
|
||||
import { SystemInfoCard } from "@/components/home/system-info-card";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { entry_lightweight_mode, openWebUrl } from "@/services/cmds";
|
||||
import { TestCard } from "@/components/home/test-card";
|
||||
import { IpInfoCard } from "@/components/home/ip-info-card";
|
||||
|
||||
const LazyTestCard = lazy(() =>
|
||||
import("@/components/home/test-card").then((module) => ({
|
||||
default: module.TestCard,
|
||||
})),
|
||||
);
|
||||
const LazyIpInfoCard = lazy(() =>
|
||||
import("@/components/home/ip-info-card").then((module) => ({
|
||||
default: module.IpInfoCard,
|
||||
})),
|
||||
);
|
||||
const LazyClashInfoCard = lazy(() =>
|
||||
import("@/components/home/clash-info-card").then((module) => ({
|
||||
default: module.ClashInfoCard,
|
||||
})),
|
||||
);
|
||||
const LazySystemInfoCard = lazy(() =>
|
||||
import("@/components/home/system-info-card").then((module) => ({
|
||||
default: module.SystemInfoCard,
|
||||
})),
|
||||
);
|
||||
|
||||
// 定义首页卡片设置接口
|
||||
interface HomeCardsSettings {
|
||||
@@ -190,9 +208,11 @@ export const HomePage = () => {
|
||||
|
||||
// 设置弹窗的状态
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
// 卡片显示状态
|
||||
const [homeCards, setHomeCards] = useState<HomeCardsSettings>(
|
||||
(verge?.home_cards as HomeCardsSettings) || {
|
||||
const defaultCards = useMemo<HomeCardsSettings>(
|
||||
() => ({
|
||||
info: false,
|
||||
profile: true,
|
||||
proxy: true,
|
||||
network: true,
|
||||
@@ -202,18 +222,49 @@ export const HomePage = () => {
|
||||
systeminfo: true,
|
||||
test: true,
|
||||
ip: true,
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [homeCards, setHomeCards] = useState<HomeCardsSettings>(() => {
|
||||
return (verge?.home_cards as HomeCardsSettings) || defaultCards;
|
||||
});
|
||||
|
||||
// 文档链接函数
|
||||
const toGithubDoc = useLockFn(() => {
|
||||
return openWebUrl("https://clash-verge-rev.github.io/index.html");
|
||||
});
|
||||
|
||||
// 新增:打开设置弹窗
|
||||
const openSettings = () => {
|
||||
const openSettings = useCallback(() => {
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderCard = useCallback(
|
||||
(cardKey: string, component: React.ReactNode, size: number = 6) => {
|
||||
if (!homeCards[cardKey]) return null;
|
||||
|
||||
return (
|
||||
<Grid size={size} key={cardKey}>
|
||||
{component}
|
||||
</Grid>
|
||||
);
|
||||
},
|
||||
[homeCards],
|
||||
);
|
||||
|
||||
const criticalCards = useMemo(
|
||||
() => [
|
||||
renderCard(
|
||||
"profile",
|
||||
<HomeProfileCard current={current} onProfileUpdated={mutateProfiles} />,
|
||||
),
|
||||
renderCard("proxy", <CurrentProxyCard />),
|
||||
renderCard("network", <NetworkSettingsCard />),
|
||||
renderCard("mode", <ClashModeEnhancedCard />),
|
||||
],
|
||||
[homeCards, current, mutateProfiles, renderCard],
|
||||
);
|
||||
|
||||
// 新增:保存设置时用requestIdleCallback/setTimeout
|
||||
const handleSaveSettings = (newCards: HomeCardsSettings) => {
|
||||
@@ -224,6 +275,47 @@ export const HomePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const nonCriticalCards = useMemo(
|
||||
() => [
|
||||
renderCard(
|
||||
"traffic",
|
||||
<EnhancedCard
|
||||
title={t("Traffic Stats")}
|
||||
icon={<SpeedOutlined />}
|
||||
iconColor="secondary"
|
||||
>
|
||||
<EnhancedTrafficStats />
|
||||
</EnhancedCard>,
|
||||
12,
|
||||
),
|
||||
renderCard(
|
||||
"test",
|
||||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||||
<LazyTestCard />
|
||||
</Suspense>,
|
||||
),
|
||||
renderCard(
|
||||
"ip",
|
||||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||||
<LazyIpInfoCard />
|
||||
</Suspense>,
|
||||
),
|
||||
renderCard(
|
||||
"clashinfo",
|
||||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||||
<LazyClashInfoCard />
|
||||
</Suspense>,
|
||||
),
|
||||
renderCard(
|
||||
"systeminfo",
|
||||
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
|
||||
<LazySystemInfoCard />
|
||||
</Suspense>,
|
||||
),
|
||||
],
|
||||
[homeCards, t, renderCard],
|
||||
);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Label-Home")}
|
||||
@@ -253,71 +345,9 @@ export const HomePage = () => {
|
||||
}
|
||||
>
|
||||
<Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}>
|
||||
{/* 订阅和当前节点部分 */}
|
||||
{homeCards.profile && (
|
||||
<Grid size={6}>
|
||||
<HomeProfileCard
|
||||
current={current}
|
||||
onProfileUpdated={mutateProfiles}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{criticalCards}
|
||||
|
||||
{homeCards.proxy && (
|
||||
<Grid size={6}>
|
||||
<CurrentProxyCard />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 代理和网络设置区域 */}
|
||||
{homeCards.network && (
|
||||
<Grid size={6}>
|
||||
<NetworkSettingsCard />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{homeCards.mode && (
|
||||
<Grid size={6}>
|
||||
<ClashModeEnhancedCard />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 增强的流量统计区域 */}
|
||||
{homeCards.traffic && (
|
||||
<Grid size={12}>
|
||||
<EnhancedCard
|
||||
title={t("Traffic Stats")}
|
||||
icon={<SpeedOutlined />}
|
||||
iconColor="secondary"
|
||||
>
|
||||
<EnhancedTrafficStats />
|
||||
</EnhancedCard>
|
||||
</Grid>
|
||||
)}
|
||||
{/* 测试网站部分 */}
|
||||
{homeCards.test && (
|
||||
<Grid size={6}>
|
||||
<TestCard />
|
||||
</Grid>
|
||||
)}
|
||||
{/* IP信息卡片 */}
|
||||
{homeCards.ip && (
|
||||
<Grid size={6}>
|
||||
<IpInfoCard />
|
||||
</Grid>
|
||||
)}
|
||||
{/* Clash信息 */}
|
||||
{homeCards.clashinfo && (
|
||||
<Grid size={6}>
|
||||
<ClashInfoCard />
|
||||
</Grid>
|
||||
)}
|
||||
{/* 系统信息 */}
|
||||
{homeCards.systeminfo && (
|
||||
<Grid size={6}>
|
||||
<SystemInfoCard />
|
||||
</Grid>
|
||||
)}
|
||||
{nonCriticalCards}
|
||||
</Grid>
|
||||
|
||||
{/* 首页设置弹窗 */}
|
||||
|
||||
@@ -221,16 +221,105 @@ export const AppDataProvider = ({
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
// 监听代理配置刷新事件(托盘代理切换等)
|
||||
const handleRefreshProxy = () => {
|
||||
const now = Date.now();
|
||||
console.log("[AppDataProvider] 代理配置刷新事件");
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
if (now - lastUpdateTime > refreshThrottle) {
|
||||
lastUpdateTime = now;
|
||||
|
||||
setTimeout(() => {
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] 代理刷新失败:", e),
|
||||
);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听强制代理刷新事件(托盘代理切换立即刷新)
|
||||
const handleForceRefreshProxies = () => {
|
||||
console.log("[AppDataProvider] 强制代理刷新事件");
|
||||
|
||||
// 立即刷新,无延迟,无防抖
|
||||
forceRefreshProxies()
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 强制刷新代理缓存完成");
|
||||
// 强制刷新完成后,立即刷新前端显示
|
||||
return refreshProxy();
|
||||
})
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 前端代理数据刷新完成");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("[AppDataProvider] 强制代理刷新失败:", e);
|
||||
// 如果强制刷新失败,尝试普通刷新
|
||||
refreshProxy().catch((e2) =>
|
||||
console.warn("[AppDataProvider] 普通代理刷新也失败:", e2),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 使用 Tauri 事件监听器替代 window 事件监听器
|
||||
const setupTauriListeners = async () => {
|
||||
try {
|
||||
const unlistenClash = await listen(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
const unlistenProxy = await listen(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
const unlistenForceRefresh = await listen(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenClash();
|
||||
unlistenProxy();
|
||||
unlistenForceRefresh();
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||||
|
||||
// 降级到 window 事件监听器
|
||||
window.addEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
window.addEventListener(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.addEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupTauriListeners = setupTauriListeners();
|
||||
|
||||
return async () => {
|
||||
const cleanup = await cleanupTauriListeners;
|
||||
cleanup();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[AppDataProvider] 事件监听器设置失败:", error);
|
||||
|
||||
@@ -143,6 +143,14 @@ export async function updateProxy(group: string, proxy: string) {
|
||||
// console.log(`[API] updateProxy 耗时: ${duration}ms`);
|
||||
}
|
||||
|
||||
export async function syncTrayProxySelection() {
|
||||
return invoke<void>("sync_tray_proxy_selection");
|
||||
}
|
||||
|
||||
export async function updateProxyAndSync(group: string, proxy: string) {
|
||||
return invoke<void>("update_proxy_and_sync", { group, proxy });
|
||||
}
|
||||
|
||||
export async function getProxies(): Promise<{
|
||||
global: IProxyGroupItem;
|
||||
direct: IProxyItem;
|
||||
|
||||
@@ -74,7 +74,8 @@ class DelayManager {
|
||||
if (delay >= 0 || delay === -2) return delay;
|
||||
}
|
||||
|
||||
if (proxy.history.length > 0) {
|
||||
// 添加 history 属性的安全检查
|
||||
if (proxy.history && proxy.history.length > 0) {
|
||||
// 0ms以error显示
|
||||
return proxy.history[proxy.history.length - 1].delay || 1e6;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,59 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en from "@/locales/en.json";
|
||||
import ru from "@/locales/ru.json";
|
||||
import zh from "@/locales/zh.json";
|
||||
import fa from "@/locales/fa.json";
|
||||
import tt from "@/locales/tt.json";
|
||||
import id from "@/locales/id.json";
|
||||
import ar from "@/locales/ar.json";
|
||||
import ko from "@/locales/ko.json";
|
||||
import tr from "@/locales/tr.json";
|
||||
|
||||
export const languages = { en, ru, zh, fa, tt, id, ar, ko, tr };
|
||||
export const supportedLanguages = [
|
||||
"en",
|
||||
"ru",
|
||||
"zh",
|
||||
"fa",
|
||||
"tt",
|
||||
"id",
|
||||
"ar",
|
||||
"ko",
|
||||
"tr",
|
||||
"de",
|
||||
"es",
|
||||
"jp",
|
||||
"zhtw",
|
||||
];
|
||||
|
||||
const resources = Object.fromEntries(
|
||||
Object.entries(languages).map(([key, value]) => [
|
||||
key,
|
||||
{ translation: value },
|
||||
]),
|
||||
export const languages: Record<string, any> = supportedLanguages.reduce(
|
||||
(acc, lang) => {
|
||||
acc[lang] = {};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
export const loadLanguage = async (language: string) => {
|
||||
try {
|
||||
const module = await import(`@/locales/${language}.json`);
|
||||
return module.default;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load language ${language}, fallback to zh`);
|
||||
const fallback = await import("@/locales/zh.json");
|
||||
return fallback.default;
|
||||
}
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
resources: {},
|
||||
lng: "zh",
|
||||
fallbackLng: "zh",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const changeLanguage = async (language: string) => {
|
||||
if (!i18n.hasResourceBundle(language, "translation")) {
|
||||
const resources = await loadLanguage(language);
|
||||
i18n.addResourceBundle(language, "translation", resources);
|
||||
}
|
||||
|
||||
await i18n.changeLanguage(language);
|
||||
};
|
||||
|
||||
export const initializeLanguage = async (initialLanguage: string = "zh") => {
|
||||
await changeLanguage(initialLanguage);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ export default defineConfig({
|
||||
svgr(),
|
||||
react(),
|
||||
legacy({
|
||||
targets: ["edge>=109", "safari>=13"],
|
||||
renderLegacyChunks: false,
|
||||
modernTargets: ["edge>=109", "safari>=13"],
|
||||
modernPolyfills: true,
|
||||
additionalModernPolyfills: [
|
||||
"core-js/modules/es.object.has-own.js",
|
||||
@@ -42,13 +42,24 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
emptyOutDir: true,
|
||||
target: "es2020",
|
||||
minify: "terser",
|
||||
chunkSizeWarningLimit: 4000,
|
||||
reportCompressedSize: false,
|
||||
sourcemap: false,
|
||||
cssCodeSplit: true,
|
||||
cssMinify: true,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: false,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ["console.debug", "console.trace"],
|
||||
dead_code: true,
|
||||
unused: true,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
treeshake: {
|
||||
preset: "recommended",
|
||||
@@ -57,38 +68,41 @@ export default defineConfig({
|
||||
},
|
||||
output: {
|
||||
compact: true,
|
||||
experimentalMinChunkSize: 30000,
|
||||
experimentalMinChunkSize: 100000,
|
||||
dynamicImportInCjs: true,
|
||||
manualChunks(id) {
|
||||
if (id.includes("node_modules")) {
|
||||
// Monaco Editor should be a separate chunk
|
||||
if (id.includes("monaco-editor")) return "monaco-editor";
|
||||
|
||||
// React-related libraries (react, react-dom, react-router-dom, etc.)
|
||||
// React core libraries
|
||||
if (
|
||||
id.includes("react") ||
|
||||
id.includes("react-dom") ||
|
||||
id.includes("react-router-dom") ||
|
||||
id.includes("react-router-dom")
|
||||
) {
|
||||
return "react-core";
|
||||
}
|
||||
|
||||
// React UI libraries
|
||||
if (
|
||||
id.includes("react-transition-group") ||
|
||||
id.includes("react-error-boundary") ||
|
||||
id.includes("react-hook-form") ||
|
||||
id.includes("react-markdown") ||
|
||||
id.includes("react-virtuoso")
|
||||
) {
|
||||
return "react";
|
||||
return "react-ui";
|
||||
}
|
||||
|
||||
// Utilities chunk: group commonly used utility libraries
|
||||
// Material UI libraries (grouped together)
|
||||
if (
|
||||
id.includes("axios") ||
|
||||
id.includes("lodash-es") ||
|
||||
id.includes("dayjs") ||
|
||||
id.includes("js-base64") ||
|
||||
id.includes("js-yaml") ||
|
||||
id.includes("cli-color") ||
|
||||
id.includes("nanoid")
|
||||
id.includes("@mui/material") ||
|
||||
id.includes("@mui/icons-material") ||
|
||||
id.includes("@mui/lab") ||
|
||||
id.includes("@mui/x-data-grid")
|
||||
) {
|
||||
return "utils";
|
||||
return "mui";
|
||||
}
|
||||
|
||||
// Tauri-related plugins: grouping together Tauri plugins
|
||||
@@ -106,22 +120,35 @@ export default defineConfig({
|
||||
return "tauri-plugins";
|
||||
}
|
||||
|
||||
// Material UI libraries (grouped together)
|
||||
// Utilities chunk: group commonly used utility libraries
|
||||
if (
|
||||
id.includes("@mui/material") ||
|
||||
id.includes("@mui/icons-material") ||
|
||||
id.includes("@mui/lab") ||
|
||||
id.includes("@mui/x-data-grid")
|
||||
id.includes("axios") ||
|
||||
id.includes("lodash-es") ||
|
||||
id.includes("dayjs") ||
|
||||
id.includes("js-base64") ||
|
||||
id.includes("js-yaml") ||
|
||||
id.includes("cli-color") ||
|
||||
id.includes("nanoid")
|
||||
) {
|
||||
return "mui";
|
||||
return "utils";
|
||||
}
|
||||
|
||||
// Small vendor packages
|
||||
// Group other vendor packages together to reduce small chunks
|
||||
const pkg = id.match(/node_modules\/([^/]+)/)?.[1];
|
||||
if (pkg && pkg.length < 8) return "small-vendors";
|
||||
if (pkg) {
|
||||
// Large packages get their own chunks
|
||||
if (
|
||||
pkg.includes("monaco") ||
|
||||
pkg.includes("lodash") ||
|
||||
pkg.includes("antd") ||
|
||||
pkg.includes("emotion")
|
||||
) {
|
||||
return `vendor-${pkg}`;
|
||||
}
|
||||
|
||||
// Large vendor packages
|
||||
return "large-vendor";
|
||||
// Group all other packages together
|
||||
return "vendor";
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -133,14 +160,8 @@ export default defineConfig({
|
||||
"@root": path.resolve("."),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
define: {
|
||||
OS_PLATFORM: `"${process.platform}"`,
|
||||
OS_PLATFORM: '"unknown"',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user