Compare commits

...

31 Commits

42 changed files with 1644 additions and 2888 deletions

View File

@@ -104,8 +104,8 @@ jobs:
# Generate autobuild version using autobuild-latest format # 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') 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) MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(date +%d) DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION" echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"

View File

@@ -88,8 +88,8 @@ jobs:
# Generate autobuild version for consistency # 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') 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) MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(date +%d) DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION" echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"

View File

@@ -122,10 +122,10 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### 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 ./路径 安装 #### 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 ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -455,7 +455,7 @@ jobs:
release-update: release-update:
name: Release Update name: Release Update
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release, release-for-linux-arm] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -480,7 +480,7 @@ jobs:
release-update-for-fixed-webview2: release-update-for-fixed-webview2:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release-for-fixed-webview2] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -605,10 +605,10 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### 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 ./路径 安装 #### 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 ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

View File

@@ -1,3 +1,32 @@
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存呢
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1 ## v2.4.1
### 🏆 重大改进 ### 🏆 重大改进

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.4.1", "version": "2.4.2",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -35,26 +35,26 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.1", "@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.16", "@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.1", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.0", "@mui/x-data-grid": "^8.11.1",
"@tauri-apps/api": "2.8.0", "@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.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-fs": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.3.1", "@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1", "@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0", "@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.9.4", "ahooks": "^3.9.5",
"axios": "^1.11.0", "axios": "^1.11.0",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"dayjs": "1.11.16", "dayjs": "1.11.18",
"foxact": "^0.2.49", "foxact": "^0.2.49",
"glob": "^11.0.3", "glob": "^11.0.3",
"i18next": "^25.4.2", "i18next": "^25.5.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -78,8 +78,8 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@eslint/js": "^9.34.0", "@eslint/js": "^9.35.0",
"@tauri-apps/cli": "2.8.3", "@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.12", "@types/react": "19.1.12",
@@ -89,19 +89,21 @@
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "commander": "^14.0.0",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"eslint": "^9.34.0", "eslint": "^9.35.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0", "globals": "^16.3.0",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"jiti": "^2.5.1", "jiti": "^2.5.1",
"meta-json-schema": "^1.19.12", "meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"path": "^0.12.7",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.91.0", "process": "^0.11.10",
"terser": "^5.43.1", "sass": "^1.92.1",
"terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.41.0", "typescript-eslint": "^8.42.0",
"vite": "^7.1.3", "vite": "^7.1.4",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0" "vite-plugin-svgr": "^4.5.0"
}, },

2482
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,14 +68,24 @@ function getLatestTauriCommit() {
} }
/** /**
* 生成短时间戳(格式:YYMMDD或带 commit格式YYMMDD.cc39b27 * 生成短时间戳格式MMDD或带 commit格式MMDD.cc39b27
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit * @param {boolean} withCommit 是否带 commit
* @returns {string} * @returns {string}
*/ */
function generateShortTimestamp(withCommit = false) { function generateShortTimestamp(withCommit = false) {
const now = new Date(); 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) { if (withCommit) {
const gitShort = getGitShortCommit(); const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`; return `${month}${day}.${gitShort}`;

View File

@@ -75,7 +75,7 @@ async function sendTelegramNotification() {
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布"; const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const encodedVersion = encodeURIComponent(version); 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 // 发送到 Telegram
try { try {

154
src-tauri/Cargo.lock generated
View File

@@ -1089,7 +1089,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "2.4.1" version = "2.4.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
@@ -1152,7 +1152,7 @@ dependencies = [
"warp", "warp",
"winapi", "winapi",
"winreg 0.55.0", "winreg 0.55.0",
"zip", "zip 5.0.0",
] ]
[[package]] [[package]]
@@ -1391,6 +1391,21 @@ dependencies = [
"libc", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -1688,17 +1703,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.4.2" version = "1.4.2"
@@ -1723,6 +1727,27 @@ dependencies = [
"syn 2.0.106", "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]] [[package]]
name = "destructure_traitobject" name = "destructure_traitobject"
version = "0.2.0" version = "0.2.0"
@@ -3819,26 +3844,6 @@ dependencies = [
"windows-targets 0.53.3", "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]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.9" version = "0.1.9"
@@ -3931,9 +3936,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -3946,29 +3951,30 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
[[package]] [[package]]
name = "log4rs" name = "log4rs"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
"chrono", "chrono",
"derivative", "derive_more 2.0.1",
"fnv", "fnv",
"humantime", "humantime",
"libc", "libc",
"log", "log",
"log-mdc", "log-mdc",
"once_cell", "mock_instant",
"parking_lot 0.12.4", "parking_lot 0.12.4",
"rand 0.8.5", "rand 0.9.2",
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"thiserror 1.0.69", "thiserror 2.0.16",
"thread-id", "thread-id",
"typemap-ors", "typemap-ors",
"unicode-segmentation",
"winapi", "winapi",
] ]
@@ -3987,6 +3993,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 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]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@@ -4149,6 +4165,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "muda" name = "muda"
version = "0.17.1" version = "0.17.1"
@@ -6241,7 +6263,7 @@ checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cssparser", "cssparser",
"derive_more", "derive_more 0.99.20",
"fxhash", "fxhash",
"log", "log",
"phf 0.8.0", "phf 0.8.0",
@@ -7009,9 +7031,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.8.4" version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d545ccf7b60dcd44e07c6fb5aeb09140966f0aabd5d2aa14a6821df7bc99348" checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -7064,9 +7086,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.4.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b" checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -7173,9 +7195,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.4.2" version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d430110d4ee102a9b673d3c03ff48098c80fe8ca71ba1ff52d8a5919538a1a6" checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb"
dependencies = [ dependencies = [
"dunce", "dunce",
"plist", "plist",
@@ -7221,9 +7243,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.3.3" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee5a3c416dc59d7d9aa0de5490a82d6e201c67ffe97388979d77b69b08cda40" checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7353,7 +7375,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"zip", "zip 4.6.1",
] ]
[[package]] [[package]]
@@ -7610,12 +7632,12 @@ dependencies = [
[[package]] [[package]]
name = "thread-id" name = "thread-id"
version = "4.2.2" version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" checksum = "99043e46c5a15af379c06add30d9c93a6c0e8849de00d244c4a2c417da128d80"
dependencies = [ dependencies = [
"libc", "libc",
"winapi", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -8295,6 +8317,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"
@@ -9772,9 +9800,21 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "4.5.0" version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"aes", "aes",
"arbitrary", "arbitrary",
@@ -9786,7 +9826,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"hmac", "hmac",
"indexmap 2.11.0", "indexmap 2.11.0",
"liblzma", "lzma-rust2",
"memchr", "memchr",
"pbkdf2", "pbkdf2",
"ppmd-rust", "ppmd-rust",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.4.1" version = "2.4.2"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -13,16 +13,16 @@ build = "build.rs"
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.0", features = [] } tauri-build = { version = "2.4.1", features = [] }
[dependencies] [dependencies]
warp = { version = "0.4.2", features = ["server"] } warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99" anyhow = "1.0.99"
dirs = "6.0" dirs = "6.0"
open = "5.3.2" open = "5.3.2"
log = "0.4.27" log = "0.4.28"
dunce = "1.0.5" dunce = "1.0.5"
log4rs = "1.3.0" log4rs = "1.4.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = { version = "0.37.0", features = ["network", "system"] } 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"] } reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2" regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.4", features = [ tauri = { version = "2.8.5", features = [
"protocol-asset", "protocol-asset",
"devtools", "devtools",
"tray-icon", "tray-icon",
@@ -53,14 +53,14 @@ tauri = { version = "2.8.4", features = [
] } ] }
network-interface = { version = "2.0.3", features = ["serde"] } network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.1" 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-fs = "2.4.2"
tauri-plugin-process = "2.3.0" tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "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-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0" tauri-plugin-window-state = "2.4.0"
zip = "4.5.0" zip = "5.0.0"
reqwest_dav = "0.2.2" reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"

View File

@@ -1,5 +1,13 @@
use tauri::Emitter;
use super::CmdResult; 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; use std::time::Duration;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
@@ -7,11 +15,11 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global(); let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default"); let key = ProxyRequestCache::make_key("proxies", "default");
let value = cache let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_proxies().await.unwrap_or_else(|e| { manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}"); logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new()) serde_json::Value::Object(serde_json::Map::new())
@@ -32,11 +40,11 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global(); let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default"); let key = ProxyRequestCache::make_key("providers", "default");
let value = cache let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_providers_proxies().await.unwrap_or_else(|e| { manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}"); logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new()) serde_json::Value::Object(serde_json::Map::new())
@@ -45,3 +53,69 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
.await; .await;
Ok((*value).clone()) 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())
}
}
}

View File

@@ -1071,21 +1071,22 @@ impl CoreManager {
} }
logging!(info, Type::Core, true, "服务可用,使用服务模式启动"); logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?; 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 { } else {
// 服务不可用,检查用户偏好 logging!(info, Type::Core, true, "服务不可用使用Sidecar模式");
let service_state = service::ServiceState::get().await; self.start_core_by_sidecar().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?;
}
} }
Ok(()) Ok(())
} }
@@ -1102,7 +1103,6 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub async fn restart_core(&self) -> Result<()> { pub async fn restart_core(&self) -> Result<()> {
self.stop_core().await?; self.stop_core().await?;
self.start_core().await?; self.start_core().await?;
Ok(()) Ok(())
} }

View File

@@ -13,7 +13,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, 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分钟冷却期 const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期

View File

@@ -1,5 +1,6 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use tauri::tray::TrayIconBuilder; use tauri::tray::TrayIconBuilder;
use tauri::Emitter;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod speed_rate; pub mod speed_rate;
use crate::ipc::Rate; use crate::ipc::Rate;
@@ -7,7 +8,9 @@ use crate::process::AsyncHandler;
use crate::{ use crate::{
cmd, cmd,
config::Config, config::Config,
feat, logging, feat,
ipc::IpcManager,
logging,
module::lightweight::is_in_lightweight_mode, module::lightweight::is_in_lightweight_mode,
singleton_lazy, singleton_lazy,
utils::{dirs::find_target_icons, i18n::t}, utils::{dirs::find_target_icons, i18n::t},
@@ -281,6 +284,16 @@ impl Tray {
.unwrap_or_default(); .unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode(); 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") { match app_handle.tray_by_id("main") {
Some(tray) => { Some(tray) => {
let _ = tray.set_menu(Some( let _ = tray.set_menu(Some(
@@ -291,6 +304,7 @@ impl Tray {
*tun_mode, *tun_mode,
profile_uid_and_name, profile_uid_and_name,
is_lightweight_mode, is_lightweight_mode,
proxy_nodes_data,
) )
.await?, .await?,
)); ));
@@ -557,9 +571,21 @@ async fn create_tray_menu(
tun_mode_enabled: bool, tun_mode_enabled: bool,
profile_uid_and_name: Vec<(String, String)>, profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool, is_lightweight_mode: bool,
proxy_nodes_data: serde_json::Value,
) -> Result<tauri::menu::Menu<Wry>> { ) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or(""); 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 version = env!("CARGO_PKG_VERSION");
let hotkeys = Config::verge() let hotkeys = Config::verge()
@@ -606,12 +632,117 @@ async fn create_tray_menu(
results.into_iter().collect::<Result<Vec<_>, _>>()? 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 // Pre-fetch all localized strings
let dashboard_text = t("Dashboard").await; let dashboard_text = t("Dashboard").await;
let rule_mode_text = t("Rule Mode").await; let rule_mode_text = t("Rule Mode").await;
let global_mode_text = t("Global Mode").await; let global_mode_text = t("Global Mode").await;
let direct_mode_text = t("Direct Mode").await; let direct_mode_text = t("Direct Mode").await;
let profiles_text = t("Profiles").await; let profiles_text = t("Profiles").await;
let proxies_text = t("Proxies").await;
let system_proxy_text = t("System Proxy").await; let system_proxy_text = t("System Proxy").await;
let tun_mode_text = t("TUN Mode").await; let tun_mode_text = t("TUN Mode").await;
let lightweight_mode_text = t("LightWeight Mode").await; let lightweight_mode_text = t("LightWeight Mode").await;
@@ -675,6 +806,24 @@ async fn create_tray_menu(
&profile_menu_items_refs, &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( let system_proxy = &CheckMenuItem::with_id(
app_handle, app_handle,
"system_proxy", "system_proxy",
@@ -772,26 +921,37 @@ async fn create_tray_menu(
let separator = &PredefinedMenuItem::separator(app_handle)?; 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) let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[ .items(&menu_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,
])
.build()?; .build()?;
Ok(menu) Ok(menu)
} }
@@ -819,11 +979,11 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
if crate::module::lightweight::is_in_lightweight_mode() { 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 crate::module::lightweight::exit_lightweight_mode().await; // Await async function
} }
let result = WindowManager::show_main_window().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" => { "system_proxy" => {
feat::toggle_system_proxy().await; // Await async function 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 crate::module::lightweight::exit_lightweight_mode().await; // Await async function
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window().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:?}");
} else { } else {
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async 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()..]; let profile_index = &id["profiles_".len()..];
feat::toggle_proxy_profile(profile_index.into()).await; // Await async function 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", ());
}
}
}
}
}
}
_ => {} _ => {}
} }

View File

@@ -2,11 +2,15 @@ use std::time::Duration;
use kode_bridge::{ use kode_bridge::{
errors::{AnyError, AnyResult}, errors::{AnyError, AnyResult},
pool::PoolConfig,
ClientConfig, IpcHttpClient, LegacyResponse, ClientConfig, IpcHttpClient, LegacyResponse,
}; };
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use crate::{
logging, singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
// 定义用于URL路径的编码集合只编码真正必要的字符 // 定义用于URL路径的编码集合只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') // 空格 .add(b' ') // 空格
@@ -16,51 +20,34 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'&') // 和号 .add(b'&') // 和号
.add(b'%'); // 百分号 .add(b'%'); // 百分号
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
// Helper function to create AnyError from string // Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError { fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into())) Box::new(std::io::Error::other(msg.into()))
} }
pub struct IpcManager { pub struct IpcManager {
ipc_path: String, client: IpcHttpClient,
config: ClientConfig,
} }
impl IpcManager { impl IpcManager {
fn new() -> Self { fn new() -> Self {
let ipc_path_buf = ipc_path().unwrap_or_else(|e| { let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!( logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
error,
crate::utils::logging::Type::Ipc,
true,
"Failed to get IPC path: {}",
e
);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
}); });
let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
Self { let config = ClientConfig {
ipc_path: ipc_path.to_string(), default_timeout: Duration::from_secs(5),
config: ClientConfig { enable_pooling: false,
default_timeout: Duration::from_secs(5), max_retries: 4,
enable_pooling: true, retry_delay: Duration::from_millis(125),
max_retries: 3, max_concurrent_requests: 16,
max_concurrent_requests: 32, max_requests_per_second: Some(64.0),
max_requests_per_second: Some(5.0), ..Default::default()
pool_config: PoolConfig { };
max_size: 32, #[allow(clippy::unwrap_used)]
min_idle: 2, let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
max_idle_time_ms: 10_000, Self { client }
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
..Default::default()
},
..Default::default()
},
}
} }
} }
@@ -74,8 +61,7 @@ impl IpcManager {
path: &str, path: &str,
body: Option<&serde_json::Value>, body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> { ) -> AnyResult<LegacyResponse> {
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?; self.client.request(method, path, body).await
client.request(method, path, body).await
} }
} }

View File

@@ -20,7 +20,6 @@ use crate::{
utils::{resolve, server}, utils::{resolve, server},
}; };
use config::Config; use config::Config;
use parking_lot::Mutex;
use tauri::AppHandle; use tauri::AppHandle;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::Manager; use tauri::Manager;
@@ -36,7 +35,7 @@ mod app_init {
/// Initialize singleton monitoring for other instances /// Initialize singleton monitoring for other instances
pub fn init_singleton_check() { pub fn init_singleton_check() {
AsyncHandler::spawn(move || async move { AsyncHandler::spawn_blocking(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例..."); logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_millis(500), server::check_singleton()).await { match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => { Ok(result) => {
@@ -81,8 +80,7 @@ mod app_init {
{ {
builder = builder.plugin(tauri_plugin_devtools::init()); builder = builder.plugin(tauri_plugin_devtools::init());
} }
builder
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
} }
/// Setup deep link handling /// Setup deep link handling
@@ -96,7 +94,7 @@ mod app_init {
app.deep_link().on_open_url(|event| { app.deep_link().on_open_url(|event| {
let url = event.urls().first().map(|u| u.to_string()); let url = event.urls().first().map(|u| u.to_string());
if let Some(url) = url { if let Some(url) = url {
tokio::task::spawn_local(async move { AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url).await { if let Err(e) = resolve::resolve_scheme(url).await {
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e); logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
} }
@@ -185,6 +183,8 @@ mod app_init {
cmd::get_proxies, cmd::get_proxies,
cmd::force_refresh_proxies, cmd::force_refresh_proxies,
cmd::get_providers_proxies, cmd::get_providers_proxies,
cmd::sync_tray_proxy_selection,
cmd::update_proxy_and_sync,
cmd::save_dns_config, cmd::save_dns_config,
cmd::apply_dns_config, cmd::apply_dns_config,
cmd::check_dns_config_exists, cmd::check_dns_config_exists,
@@ -280,17 +280,16 @@ pub fn run() {
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP") let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default() .unwrap_or_default()
.to_uppercase(); .to_uppercase();
let session_env = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
let is_kde_desktop = desktop_env.contains("KDE"); 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 { if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GDK_BACKEND", "x11"); std::env::set_var("GTK_CSD", "0");
logging!( logging!(
info, info,
Type::Setup, Type::Setup,
true, 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, "执行主要设置操作...");
logging!(info, Type::Setup, true, "异步执行应用设置..."); resolve::resolve_setup_handle(app_handle);
resolve::resolve_setup_sync(app_handle);
resolve::resolve_setup_async(); resolve::resolve_setup_async();
resolve::resolve_setup_sync();
logging!(info, Type::Setup, true, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(()) Ok(())

View File

@@ -3,16 +3,14 @@ use crate::{
core::{handle, timer::Timer, tray::Tray}, core::{handle, timer::Timer, tray::Tray},
log_err, logging, log_err, logging,
process::AsyncHandler, process::AsyncHandler,
state::lightweight::LightWeightState, state::proxy::ProxyRequestCache,
utils::logging::Type, utils::{logging::Type, window_manager::WindowManager},
}; };
#[cfg(target_os = "macos")]
use crate::logging_error; use crate::logging_error;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder; use delay_timer::prelude::TaskBuilder;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Listener, Manager}; use tauri::{Listener, Manager};
@@ -21,23 +19,20 @@ const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用 // 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
fn with_lightweight_status<F, R>(f: F) -> Option<R> static IS_IN_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
where
F: FnOnce(&mut LightWeightState) -> R, fn inner_set_lightweight_mode(value: bool) -> bool {
{ if value {
if let Some(app_handle) = handle::Handle::global().app_handle() { logging!(info, Type::Lightweight, true, "轻量模式已开启");
// 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
}
} else { } else {
// App handle not available yet logging!(info, Type::Lightweight, true, "轻量模式已关闭");
None
} }
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() { pub async fn run_once_auto_lightweight() {
@@ -68,55 +63,33 @@ pub async fn run_once_auto_lightweight() {
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器" "在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
); );
if with_lightweight_status(|_| ()).is_some() { enable_auto_light_weight_mode().await;
set_lightweight_mode(false).await;
enable_auto_light_weight_mode().await;
if let Err(e) = Tray::global().update_part().await { if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}"); log::warn!("Failed to update tray: {e}");
}
} }
} }
pub async fn auto_lightweight_mode_init() -> Result<()> { pub async fn auto_lightweight_mode_init() -> Result<()> {
if let Some(app_handle) = handle::Handle::global().app_handle() { let is_silent_start =
// Check if state is available before accessing it { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() { let enable_auto = {
logging!( Config::verge()
warn, .await
Type::Lightweight, .latest_ref()
true, .enable_auto_light_weight_mode
"LightWeightState 尚未初始化,跳过自动轻量模式初始化" }
); .unwrap_or(false);
return Err(anyhow::anyhow!("LightWeightState has not been initialized"));
}
let is_silent_start = if enable_auto && !is_silent_start {
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); logging!(
let enable_auto = { info,
Config::verge() Type::Lightweight,
.await true,
.latest_ref() "非静默启动直接挂载自动进入轻量模式监听器!"
.enable_auto_light_weight_mode );
} set_lightweight_mode(true).await;
.unwrap_or(false); enable_auto_light_weight_mode().await;
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);
}
}
} }
Ok(()) Ok(())
@@ -124,16 +97,13 @@ pub async fn auto_lightweight_mode_init() -> Result<()> {
// 检查是否处于轻量模式 // 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool { 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) { pub async fn set_lightweight_mode(value: bool) {
if with_lightweight_status(|state| { if inner_get_lightweight_mode() != value {
state.set_lightweight_mode(value); inner_set_lightweight_mode(value);
})
.is_some()
{
// 只有在状态可用时才触发托盘更新 // 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await { if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}"); log::warn!("Failed to update tray: {e}");
@@ -178,9 +148,10 @@ pub async fn entry_lightweight_mode() {
} }
set_lightweight_mode(true).await; set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer(); 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);
} }
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数

View File

@@ -34,6 +34,7 @@ impl AsyncHandler {
async_runtime::spawn_blocking(f) async_runtime::spawn_blocking(f)
} }
#[allow(dead_code)]
#[track_caller] #[track_caller]
pub fn block_on<Fut>(fut: Fut) -> Fut::Output pub fn block_on<Fut>(fut: Fut) -> Fut::Output
where where

View File

@@ -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()
}
}

View File

@@ -1,5 +1,4 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc // Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc // https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy; pub mod proxy;

View File

@@ -1,3 +1,5 @@
// use crate::utils::logging::Type;
// use crate::{logging, singleton};
use crate::singleton; use crate::singleton;
use dashmap::DashMap; use dashmap::DashMap;
use serde_json::Value; use serde_json::Value;
@@ -11,7 +13,7 @@ pub struct CacheEntry {
} }
pub struct ProxyRequestCache { pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>, pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry>>>>,
} }
impl ProxyRequestCache { impl ProxyRequestCache {
@@ -54,10 +56,10 @@ impl ProxyRequestCache {
// Try to set a new value // Try to set a new value
let value = fetch_fn().await; let value = fetch_fn().await;
let entry = CacheEntry { let entry = Box::new(CacheEntry {
value: Arc::new(value), value: Arc::new(value),
expires_at: Instant::now() + ttl, expires_at: Instant::now() + ttl,
}; });
match cell.set(entry) { match cell.set(entry) {
Ok(_) => { 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 // Use singleton macro

View File

@@ -405,14 +405,6 @@ pub async fn init_resources() -> Result<()> {
for file in file_list.iter() { for file in file_list.iter() {
let src_path = res_dir.join(file); let src_path = res_dir.join(file);
let dest_path = app_dir.join(file); let dest_path = app_dir.join(file);
logging!(
debug,
Type::Setup,
true,
"src_path: {:?}, dest_path: {:?}",
src_path,
dest_path
);
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move { let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await { match fs::copy(&src, &dest).await {
@@ -445,14 +437,6 @@ pub async fn init_resources() -> Result<()> {
(Ok(src_modified), Ok(dest_modified)) => { (Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified { if src_modified > dest_modified {
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await; handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
} else {
logging!(
debug,
Type::Setup,
true,
"skipping resource copy '{}'",
file
);
} }
} }
_ => { _ => {

View File

@@ -18,6 +18,7 @@ pub enum Type {
Network, Network,
ProxyMode, ProxyMode,
Ipc, Ipc,
// Cache,
} }
impl fmt::Display for Type { impl fmt::Display for Type {
@@ -39,6 +40,7 @@ impl fmt::Display for Type {
Type::Network => write!(f, "[Network]"), Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"), Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Ipc => write!(f, "[IPC]"), Type::Ipc => write!(f, "[IPC]"),
// Type::Cache => write!(f, "[Cache]"),
} }
} }
} }

View File

@@ -1,13 +1,17 @@
use anyhow::Result; use anyhow::Result;
use isahc::http::{ use base64::{engine::general_purpose, Engine as _};
header::{HeaderMap, HeaderValue, USER_AGENT},
StatusCode, Uri,
};
use isahc::prelude::*; use isahc::prelude::*;
use isahc::{
config::RedirectPolicy,
http::{
header::{HeaderMap, HeaderValue, USER_AGENT},
StatusCode, Uri,
},
};
use isahc::{config::SslOption, HttpClient}; use isahc::{config::SslOption, HttpClient};
use std::sync::Once;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use sysproxy::Sysproxy; use sysproxy::Sysproxy;
use tauri::Url;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::timeout; use tokio::time::timeout;
@@ -53,7 +57,6 @@ pub struct NetworkManager {
self_proxy_client: Mutex<Option<HttpClient>>, self_proxy_client: Mutex<Option<HttpClient>>,
system_proxy_client: Mutex<Option<HttpClient>>, system_proxy_client: Mutex<Option<HttpClient>>,
no_proxy_client: Mutex<Option<HttpClient>>, no_proxy_client: Mutex<Option<HttpClient>>,
init: Once,
last_connection_error: Mutex<Option<(Instant, String)>>, last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: Mutex<usize>, connection_error_count: Mutex<usize>,
} }
@@ -64,16 +67,11 @@ impl NetworkManager {
self_proxy_client: Mutex::new(None), self_proxy_client: Mutex::new(None),
system_proxy_client: Mutex::new(None), system_proxy_client: Mutex::new(None),
no_proxy_client: Mutex::new(None), no_proxy_client: Mutex::new(None),
init: Once::new(),
last_connection_error: Mutex::new(None), last_connection_error: Mutex::new(None),
connection_error_count: Mutex::new(0), connection_error_count: Mutex::new(0),
} }
} }
pub fn init(&self) {
self.init.call_once(|| {});
}
async fn record_connection_error(&self, error: &str) { async fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock().await; let mut last_error = self.last_connection_error.lock().await;
*last_error = Some((Instant::now(), error.to_string())); *last_error = Some((Instant::now(), error.to_string()));
@@ -135,6 +133,8 @@ impl NetworkManager {
builder = builder.timeout(Duration::from_secs(secs)); builder = builder.timeout(Duration::from_secs(secs));
} }
builder = builder.redirect_policy(RedirectPolicy::Follow);
Ok(builder.build()?) Ok(builder.build()?)
}; };
@@ -197,15 +197,40 @@ impl NetworkManager {
self.reset_clients().await; 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 let client = self
.create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs) .create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs)
.await?; .await?;
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20)); 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 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 status = response.status();
let headers = response.headers().clone(); let headers = response.headers().clone();
let body = response.text().await?; let body = response.text().await?;

View File

@@ -7,7 +7,7 @@ use crate::{
logging, logging_error, logging, logging_error,
module::lightweight::auto_lightweight_mode_init, module::lightweight::auto_lightweight_mode_init,
process::AsyncHandler, 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; pub mod dns;
@@ -16,11 +16,15 @@ pub mod ui;
pub mod window; pub mod window;
pub mod window_script; 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_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() { pub fn resolve_setup_async() {
@@ -33,28 +37,30 @@ pub fn resolve_setup_async() {
std::thread::current().id() std::thread::current().id()
); );
// AsyncHandler::spawn_blocking(|| AsyncHandler::block_on(init_work_config()));
AsyncHandler::spawn(|| async { AsyncHandler::spawn(|| async {
init_work_config().await; futures::join!(
init_resources().await; init_work_config(),
init_startup_script().await; init_resources(),
init_startup_script(),
init_hotkey(),
);
init_timer().await; init_timer().await;
init_hotkey().await;
init_auto_lightweight_mode().await; init_auto_lightweight_mode().await;
init_verge_config().await; init_verge_config().await;
init_core_manager().await; init_core_manager().await;
init_system_proxy().await;
init_system_proxy().await;
AsyncHandler::spawn_blocking(|| { AsyncHandler::spawn_blocking(|| {
init_system_proxy_guard(); init_system_proxy_guard();
}); });
init_window().await; let tray_and_refresh = async {
init_tray().await; init_tray().await;
refresh_tray_menu().await refresh_tray_menu().await;
};
futures::join!(init_window(), tray_and_refresh,);
}); });
let elapsed = start_time.elapsed(); let elapsed = start_time.elapsed();

View File

@@ -44,7 +44,7 @@ pub async fn check_singleton() -> Result<()> {
pub fn embed_server() { pub fn embed_server() {
let port = IVerge::get_singleton_port(); 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 { let visible = warp::path!("commands" / "visible").and_then(|| async {
Ok::<_, warp::Rejection>(warp::reply::with_status( Ok::<_, warp::Rejection>(warp::reply::with_status(
"ok".to_string(), "ok".to_string(),

View File

@@ -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 { pub fn is_main_window_visible() -> bool {
Self::get_main_window() Self::get_main_window()

View File

@@ -1,5 +1,5 @@
{ {
"version": "2.4.1", "version": "2.4.2",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,

View File

@@ -29,10 +29,9 @@ import {
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card"; import { EnhancedCard } from "@/components/home/enhanced-card";
import { updateProxy, deleteConnection } from "@/services/cmds";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
import { useProxySelection } from "@/hooks/use-proxy-selection";
// 本地存储的键名 // 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@@ -94,8 +93,18 @@ export const CurrentProxyCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { verge } = useVerge(); const { proxies, clashConfig, refreshProxy } = useAppData();
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
// 统一代理选择器
const { handleSelectChange } = useProxySelection({
onSuccess: () => {
refreshProxy();
},
onError: (error) => {
console.error("代理切换失败", error);
refreshProxy();
},
});
// 判断模式 // 判断模式
const mode = clashConfig?.mode?.toLowerCase() || "rule"; const mode = clashConfig?.mode?.toLowerCase() || "rule";
@@ -113,8 +122,6 @@ export const CurrentProxyCard = () => {
proxyData: { proxyData: {
groups: { name: string; now: string; all: string[] }[]; groups: { name: string; now: string; all: string[] }[];
records: Record<string, any>; records: Record<string, any>;
globalProxy: string;
directProxy: any;
}; };
selection: { selection: {
group: string; group: string;
@@ -127,8 +134,6 @@ export const CurrentProxyCard = () => {
proxyData: { proxyData: {
groups: [], groups: [],
records: {}, records: {},
globalProxy: "",
directProxy: { name: "DIRECT" }, // 默认值避免 undefined
}, },
selection: { selection: {
group: "", group: "",
@@ -253,8 +258,6 @@ export const CurrentProxyCard = () => {
proxyData: { proxyData: {
groups: filteredGroups, groups: filteredGroups,
records: proxies.records || {}, records: proxies.records || {},
globalProxy: proxies.global?.now || "",
directProxy: proxies.records?.DIRECT || { name: "DIRECT" },
}, },
selection: { selection: {
group: newGroup, group: newGroup,
@@ -310,7 +313,7 @@ export const CurrentProxyCard = () => {
// 处理代理节点变更 // 处理代理节点变更
const handleProxyChange = useCallback( const handleProxyChange = useCallback(
async (event: SelectChangeEvent) => { (event: SelectChangeEvent) => {
if (isDirectMode) return; if (isDirectMode) return;
const newProxy = event.target.value; const newProxy = event.target.value;
@@ -330,35 +333,15 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy); localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
} }
try { const skipConfigSave = isGlobalMode || isDirectMode;
await updateProxy(currentGroup, newProxy); handleSelectChange(currentGroup, previousProxy, skipConfigSave)(event);
// 自动关闭连接设置
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);
}
}, },
[ [
isDirectMode, isDirectMode,
isGlobalMode, isGlobalMode,
state.proxyData.records,
state.selection, state.selection,
verge?.auto_close_connection,
refreshProxy,
debouncedSetState, debouncedSetState,
connections.data, handleSelectChange,
], ],
); );

View File

@@ -1,16 +1,9 @@
import { useRef, useState, useEffect, useCallback, useMemo } from "react"; import { useRef, useState, useEffect, useCallback, useMemo } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { import { providerHealthCheck, getGroupProxyDelays } from "@/services/cmds";
getConnections,
providerHealthCheck,
updateProxy,
deleteConnection,
getGroupProxyDelays,
} from "@/services/cmds";
import { forceRefreshProxies } from "@/services/cmds";
import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { BaseEmpty } from "../base"; import { BaseEmpty } from "../base";
import { useRenderList } from "./use-render-list"; import { useRenderList } from "./use-render-list";
import { ProxyRender } from "./proxy-render"; import { ProxyRender } from "./proxy-render";
@@ -203,7 +196,17 @@ export const ProxyGroups = (props: Props) => {
const { renderList, onProxies, onHeadState } = useRenderList(mode); const { renderList, onProxies, onHeadState } = useRenderList(mode);
const { verge } = useVerge(); const { verge } = useVerge();
const { current, patchCurrent } = useProfiles();
// 统代理选择
const { handleProxyGroupChange } = useProxySelection({
onSuccess: () => {
onProxies();
},
onError: (error) => {
console.error("代理切换失败", error);
onProxies();
},
});
// 获取自动滚动开关状态,默认为 true // 获取自动滚动开关状态,默认为 true
const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true; const enableAutoScroll = verge?.enable_hover_jump_navigator ?? true;
@@ -335,44 +338,13 @@ export const ProxyGroups = (props: Props) => {
[letterIndexMap], [letterIndexMap],
); );
// 切换分组的节点代理 const handleChangeProxy = useCallback(
const handleChangeProxy = useLockFn( (group: IProxyGroupItem, proxy: IProxyItem) => {
async (group: IProxyGroupItem, proxy: IProxyItem) => {
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return; if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
const { name, now } = group; handleProxyGroupChange(group, proxy);
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],
); );
// 测全部延迟 // 测全部延迟

View File

@@ -19,7 +19,7 @@ import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material"; import { ContentCopyRounded } from "@mui/icons-material";
import { languages } from "@/services/i18n"; import { supportedLanguages } from "@/services/i18n";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
interface Props { interface Props {
@@ -28,7 +28,7 @@ interface Props {
const OS = getSystem(); const OS = getSystem();
const languageOptions = Object.entries(languages).map(([code, _]) => { const languageOptions = supportedLanguages.map((code) => {
const labels: { [key: string]: string } = { const labels: { [key: string]: string } = {
en: "English", en: "English",
ru: "Русский", ru: "Русский",
@@ -39,8 +39,13 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
ar: "العربية", ar: "العربية",
ko: "한국어", ko: "한국어",
tr: "Türkçe", 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) => { const SettingVergeBasic = ({ onError }: Props) => {

View File

@@ -8,15 +8,9 @@ import {
DeleteForeverRounded, DeleteForeverRounded,
WarningRounded, WarningRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { import { Box, Typography, alpha, useTheme } from "@mui/material";
Box,
Button,
Tooltip,
Typography,
alpha,
useTheme,
} from "@mui/material";
import { DialogRef, Switch } from "@/components/base"; import { DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { GuardState } from "@/components/setting/mods/guard-state"; import { GuardState } from "@/components/setting/mods/guard-state";
import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer";
import { TunViewer } from "@/components/setting/mods/tun-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer";
@@ -100,19 +94,6 @@ const ProxyControlSwitches = ({
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
{label && (
<Box
sx={{
fontSize: "15px",
fontWeight: "500",
mb: 0.5,
display: "none",
}}
>
{label}
</Box>
)}
{/* 仅显示当前选中的开关 */} {/* 仅显示当前选中的开关 */}
{isSystemProxyMode && ( {isSystemProxyMode && (
<Box <Box
@@ -131,50 +112,36 @@ const ProxyControlSwitches = ({
> >
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
{systemProxyActualState ? ( {systemProxyActualState ? (
<PlayCircleOutlineRounded <PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
/>
) : ( ) : (
<PauseCircleOutlineRounded <PauseCircleOutlineRounded
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }} sx={{ color: "text.disabled", mr: 1 }}
/> />
)} )}
<Box> <Typography
<Typography variant="subtitle1"
variant="subtitle1" sx={{ fontWeight: 500, fontSize: "15px" }}
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)}
> >
<Switch edge="end" /> {t("System Proxy")}
</GuardState> </Typography>
<TooltipIcon
title={t("System Proxy Info")}
icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()}
sx={{ ml: 1 }}
/>
</Box> </Box>
<GuardState
value={systemProxyActualState}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onGuard={(e) => toggleSystemProxy(e)}
>
<Switch edge="end" />
</GuardState>
</Box> </Box>
)} )}
@@ -196,109 +163,87 @@ const ProxyControlSwitches = ({
> >
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
{enable_tun_mode ? ( {enable_tun_mode ? (
<PlayCircleOutlineRounded <PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
/>
) : ( ) : (
<PauseCircleOutlineRounded <PauseCircleOutlineRounded
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }} sx={{ color: "text.disabled", mr: 1 }}
/> />
)} )}
<Box> <Typography
<Typography variant="subtitle1"
variant="subtitle1" sx={{ fontWeight: 500, fontSize: "15px" }}
sx={{ fontWeight: 500, fontSize: "15px" }} >
> {t("Tun Mode")}
{t("Tun Mode")} </Typography>
</Typography> <TooltipIcon
</Box> title={t("Tun Mode Info")}
icon={SettingsRounded}
onClick={() => tunRef.current?.open()}
sx={{ ml: 1 }}
/>
{!isTunAvailable && ( {!isTunAvailable && (
<Tooltip <TooltipIcon
title={t("TUN requires Service Mode or Admin Mode")} title={t("TUN requires Service Mode or Admin Mode")}
arrow icon={WarningRounded}
> sx={{ color: "warning.main", ml: 1 }}
<WarningRounded sx={{ color: "warning.main", ml: 1 }} /> />
</Tooltip>
)} )}
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
{!isTunAvailable && ( {!isTunAvailable && (
<Tooltip title={t("Install Service")} arrow> <TooltipIcon
<Button title={t("Install Service")}
variant="outlined" icon={BuildRounded}
color="primary" color="primary"
size="small" onClick={onInstallService}
onClick={onInstallService} sx={{ ml: 1 }}
sx={{ mr: 1, minWidth: "32px", p: "4px" }} />
>
<BuildRounded fontSize="small" />
</Button>
</Tooltip>
)} )}
{isServiceMode && ( {isServiceMode && (
<Tooltip title={t("Uninstall Service")} arrow> <TooltipIcon
<Button title={t("Uninstall Service")}
color="secondary" icon={DeleteForeverRounded}
size="small" color="secondary"
onClick={onUninstallService} onClick={onUninstallService}
sx={{ mr: 1, minWidth: "32px", p: "4px" }} sx={{ ml: 1 }}
> />
<DeleteForeverRounded fontSize="small" />
</Button>
</Tooltip>
)} )}
<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> </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> </Box>
)} )}

45
src/hooks/use-i18n.ts Normal file
View 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,
};
};

View 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,
};
};

View File

@@ -13,7 +13,7 @@ import { ComposeContextProvider } from "foxact/compose-context-provider";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { BaseErrorBoundary } from "./components/base"; import { BaseErrorBoundary } from "./components/base";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
import "./services/i18n"; import { initializeLanguage } from "./services/i18n";
import { import {
LoadingCacheProvider, LoadingCacheProvider,
ThemeModeProvider, ThemeModeProvider,
@@ -39,29 +39,47 @@ document.addEventListener("keydown", (event) => {
["F", "G", "H", "J", "P", "Q", "R", "U"].includes( ["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
event.key.toUpperCase(), event.key.toUpperCase(),
)); ));
disabledShortcuts && event.preventDefault(); if (disabledShortcuts) {
event.preventDefault();
}
}); });
const contexts = [ const initializeApp = async () => {
<ThemeModeProvider />, try {
<LoadingCacheProvider />, await initializeLanguage("zh");
<UpdateStateProvider />,
];
const root = createRoot(container); const contexts = [
root.render( <ThemeModeProvider key="theme" />,
<React.StrictMode> <LoadingCacheProvider key="loading" />,
<ComposeContextProvider contexts={contexts}> <UpdateStateProvider key="update" />,
<BaseErrorBoundary> ];
<AppDataProvider>
<BrowserRouter> const root = createRoot(container);
<Layout /> root.render(
</BrowserRouter> <React.StrictMode>
</AppDataProvider> <ComposeContextProvider contexts={contexts}>
</BaseErrorBoundary> <BaseErrorBoundary>
</ComposeContextProvider> <AppDataProvider>
</React.StrictMode>, <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) => { window.addEventListener("error", (event) => {

View File

@@ -1,5 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr"; import { SWRConfig, mutate } from "swr";
import { useEffect, useCallback, useState, useRef } from "react"; import { useEffect, useCallback, useState, useRef } from "react";
@@ -11,6 +10,7 @@ import { routers } from "./_routers";
import { getAxios } from "@/services/api"; import { getAxios } from "@/services/api";
import { forceRefreshClashConfig } from "@/services/cmds"; import { forceRefreshClashConfig } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useI18n } from "@/hooks/use-i18n";
import LogoSvg from "@/assets/image/logo.svg?react"; import LogoSvg from "@/assets/image/logo.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react"; import iconLight from "@/assets/image/icon_light.svg?react";
import iconDark from "@/assets/image/icon_dark.svg?react"; import iconDark from "@/assets/image/icon_dark.svg?react";
@@ -158,6 +158,7 @@ const Layout = () => {
const [enableLog] = useEnableLog(); const [enableLog] = useEnableLog();
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info"); const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const { language, start_page } = verge ?? {}; const { language, start_page } = verge ?? {};
const { switchLanguage } = useI18n();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const routersEles = useRoutes(routers); const routersEles = useRoutes(routers);
@@ -439,9 +440,9 @@ const Layout = () => {
useEffect(() => { useEffect(() => {
if (language) { if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language); dayjs.locale(language === "zh" ? "zh-cn" : language);
i18next.changeLanguage(language); switchLanguage(language);
} }
}, [language]); }, [language, switchLanguage]);
useEffect(() => { useEffect(() => {
if (start_page) { if (start_page) {

View File

@@ -12,6 +12,7 @@ import {
Checkbox, Checkbox,
Tooltip, Tooltip,
Grid, Grid,
Skeleton,
} from "@mui/material"; } from "@mui/material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
@@ -26,17 +27,34 @@ import {
import { ProxyTunCard } from "@/components/home/proxy-tun-card"; import { ProxyTunCard } from "@/components/home/proxy-tun-card";
import { ClashModeCard } from "@/components/home/clash-mode-card"; import { ClashModeCard } from "@/components/home/clash-mode-card";
import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats"; 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 { HomeProfileCard } from "@/components/home/home-profile-card";
import { EnhancedCard } from "@/components/home/enhanced-card"; import { EnhancedCard } from "@/components/home/enhanced-card";
import { CurrentProxyCard } from "@/components/home/current-proxy-card"; import { CurrentProxyCard } from "@/components/home/current-proxy-card";
import { BasePage } from "@/components/base"; 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 { useLockFn } from "ahooks";
import { entry_lightweight_mode, openWebUrl } from "@/services/cmds"; 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 { interface HomeCardsSettings {
@@ -190,9 +208,11 @@ export const HomePage = () => {
// 设置弹窗的状态 // 设置弹窗的状态
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
// 卡片显示状态 // 卡片显示状态
const [homeCards, setHomeCards] = useState<HomeCardsSettings>( const defaultCards = useMemo<HomeCardsSettings>(
(verge?.home_cards as HomeCardsSettings) || { () => ({
info: false,
profile: true, profile: true,
proxy: true, proxy: true,
network: true, network: true,
@@ -202,18 +222,49 @@ export const HomePage = () => {
systeminfo: true, systeminfo: true,
test: true, test: true,
ip: true, ip: true,
}, }),
[],
); );
const [homeCards, setHomeCards] = useState<HomeCardsSettings>(() => {
return (verge?.home_cards as HomeCardsSettings) || defaultCards;
});
// 文档链接函数 // 文档链接函数
const toGithubDoc = useLockFn(() => { const toGithubDoc = useLockFn(() => {
return openWebUrl("https://clash-verge-rev.github.io/index.html"); return openWebUrl("https://clash-verge-rev.github.io/index.html");
}); });
// 新增:打开设置弹窗 // 新增:打开设置弹窗
const openSettings = () => { const openSettings = useCallback(() => {
setSettingsOpen(true); 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 // 新增保存设置时用requestIdleCallback/setTimeout
const handleSaveSettings = (newCards: HomeCardsSettings) => { 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 ( return (
<BasePage <BasePage
title={t("Label-Home")} title={t("Label-Home")}
@@ -253,71 +345,9 @@ export const HomePage = () => {
} }
> >
<Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}> <Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}>
{/* 订阅和当前节点部分 */} {criticalCards}
{homeCards.profile && (
<Grid size={6}>
<HomeProfileCard
current={current}
onProfileUpdated={mutateProfiles}
/>
</Grid>
)}
{homeCards.proxy && ( {nonCriticalCards}
<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>
)}
</Grid> </Grid>
{/* 首页设置弹窗 */} {/* 首页设置弹窗 */}

View File

@@ -221,16 +221,105 @@ export const AppDataProvider = ({
} }
}; };
window.addEventListener( // 监听代理配置刷新事件(托盘代理切换等)
"verge://refresh-clash-config", const handleRefreshProxy = () => {
handleRefreshClash, const now = Date.now();
); console.log("[AppDataProvider] 代理配置刷新事件");
return () => { if (now - lastUpdateTime > refreshThrottle) {
window.removeEventListener( lastUpdateTime = now;
"verge://refresh-clash-config",
handleRefreshClash, 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) { } catch (error) {
console.error("[AppDataProvider] 事件监听器设置失败:", error); console.error("[AppDataProvider] 事件监听器设置失败:", error);

View File

@@ -143,6 +143,14 @@ export async function updateProxy(group: string, proxy: string) {
// console.log(`[API] updateProxy 耗时: ${duration}ms`); // 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<{ export async function getProxies(): Promise<{
global: IProxyGroupItem; global: IProxyGroupItem;
direct: IProxyItem; direct: IProxyItem;

View File

@@ -74,7 +74,8 @@ class DelayManager {
if (delay >= 0 || delay === -2) return delay; if (delay >= 0 || delay === -2) return delay;
} }
if (proxy.history.length > 0) { // 添加 history 属性的安全检查
if (proxy.history && proxy.history.length > 0) {
// 0ms以error显示 // 0ms以error显示
return proxy.history[proxy.history.length - 1].delay || 1e6; return proxy.history[proxy.history.length - 1].delay || 1e6;
} }

View File

@@ -1,29 +1,59 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-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( export const languages: Record<string, any> = supportedLanguages.reduce(
Object.entries(languages).map(([key, value]) => [ (acc, lang) => {
key, acc[lang] = {};
{ translation: value }, 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({ i18n.use(initReactI18next).init({
resources, resources: {},
lng: "zh", lng: "zh",
fallbackLng: "zh", fallbackLng: "zh",
interpolation: { interpolation: {
escapeValue: false, 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);
};

View File

@@ -17,8 +17,8 @@ export default defineConfig({
svgr(), svgr(),
react(), react(),
legacy({ legacy({
targets: ["edge>=109", "safari>=13"],
renderLegacyChunks: false, renderLegacyChunks: false,
modernTargets: ["edge>=109", "safari>=13"],
modernPolyfills: true, modernPolyfills: true,
additionalModernPolyfills: [ additionalModernPolyfills: [
"core-js/modules/es.object.has-own.js", "core-js/modules/es.object.has-own.js",
@@ -42,13 +42,24 @@ export default defineConfig({
build: { build: {
outDir: "../dist", outDir: "../dist",
emptyOutDir: true, emptyOutDir: true,
target: "es2020",
minify: "terser", minify: "terser",
chunkSizeWarningLimit: 4000, chunkSizeWarningLimit: 4000,
reportCompressedSize: false, reportCompressedSize: false,
sourcemap: false, sourcemap: false,
cssCodeSplit: true, cssCodeSplit: true,
cssMinify: 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: { rollupOptions: {
treeshake: { treeshake: {
preset: "recommended", preset: "recommended",
@@ -57,38 +68,41 @@ export default defineConfig({
}, },
output: { output: {
compact: true, compact: true,
experimentalMinChunkSize: 30000, experimentalMinChunkSize: 100000,
dynamicImportInCjs: true, dynamicImportInCjs: true,
manualChunks(id) { manualChunks(id) {
if (id.includes("node_modules")) { if (id.includes("node_modules")) {
// Monaco Editor should be a separate chunk // Monaco Editor should be a separate chunk
if (id.includes("monaco-editor")) return "monaco-editor"; if (id.includes("monaco-editor")) return "monaco-editor";
// React-related libraries (react, react-dom, react-router-dom, etc.) // React core libraries
if ( if (
id.includes("react") || id.includes("react") ||
id.includes("react-dom") || 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-transition-group") ||
id.includes("react-error-boundary") || id.includes("react-error-boundary") ||
id.includes("react-hook-form") || id.includes("react-hook-form") ||
id.includes("react-markdown") || id.includes("react-markdown") ||
id.includes("react-virtuoso") id.includes("react-virtuoso")
) { ) {
return "react"; return "react-ui";
} }
// Utilities chunk: group commonly used utility libraries // Material UI libraries (grouped together)
if ( if (
id.includes("axios") || id.includes("@mui/material") ||
id.includes("lodash-es") || id.includes("@mui/icons-material") ||
id.includes("dayjs") || id.includes("@mui/lab") ||
id.includes("js-base64") || id.includes("@mui/x-data-grid")
id.includes("js-yaml") ||
id.includes("cli-color") ||
id.includes("nanoid")
) { ) {
return "utils"; return "mui";
} }
// Tauri-related plugins: grouping together Tauri plugins // Tauri-related plugins: grouping together Tauri plugins
@@ -106,22 +120,35 @@ export default defineConfig({
return "tauri-plugins"; return "tauri-plugins";
} }
// Material UI libraries (grouped together) // Utilities chunk: group commonly used utility libraries
if ( if (
id.includes("@mui/material") || id.includes("axios") ||
id.includes("@mui/icons-material") || id.includes("lodash-es") ||
id.includes("@mui/lab") || id.includes("dayjs") ||
id.includes("@mui/x-data-grid") 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]; 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 // Group all other packages together
return "large-vendor"; return "vendor";
}
} }
}, },
}, },
@@ -133,14 +160,8 @@ export default defineConfig({
"@root": path.resolve("."), "@root": path.resolve("."),
}, },
}, },
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
},
},
},
define: { define: {
OS_PLATFORM: `"${process.platform}"`, OS_PLATFORM: '"unknown"',
}, },
}); });