Compare commits
31 Commits
@@ -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"
|
||||||
|
|||||||
4
.github/workflows/clean-old-assets.yml
vendored
4
.github/workflows/clean-old-assets.yml
vendored
@@ -88,8 +88,8 @@ jobs:
|
|||||||
|
|
||||||
# Generate autobuild version for consistency
|
# 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"
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -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)
|
||||||
|
|||||||
29
UPDATELOG.md
29
UPDATELOG.md
@@ -1,3 +1,32 @@
|
|||||||
|
## v2.4.2
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- 增加托盘节点选择
|
||||||
|
|
||||||
|
### 🚀 性能优化
|
||||||
|
|
||||||
|
- 优化前端首页加载速度
|
||||||
|
- 优化前端未使用 i18n 文件缓存呢
|
||||||
|
- 优化后端内存占用
|
||||||
|
- 优化后端启动速度
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 修复首页节点切换失效的问题
|
||||||
|
- 修复和优化服务检查流程
|
||||||
|
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||||
|
- 修复 rpm/deb 包名称问题
|
||||||
|
- 修复托盘轻量模式状态检测异常
|
||||||
|
- 修复通过 scheme 导入订阅崩溃
|
||||||
|
- 修复单例检测实效
|
||||||
|
- 修复启动阶段可能导致的无法连接内核
|
||||||
|
- 修复导入订阅无法 Auth Basic
|
||||||
|
|
||||||
|
### 👙 界面样式
|
||||||
|
|
||||||
|
- 简化和改进代理设置样式
|
||||||
|
|
||||||
## v2.4.1
|
## v2.4.1
|
||||||
|
|
||||||
### 🏆 重大改进
|
### 🏆 重大改进
|
||||||
|
|||||||
36
package.json
36
package.json
@@ -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
2482
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -68,14 +68,24 @@ function getLatestTauriCommit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||||
|
* 使用 Asia/Shanghai 时区
|
||||||
* @param {boolean} withCommit 是否带 commit
|
* @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}`;
|
||||||
|
|||||||
@@ -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
154
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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分钟冷却期
|
||||||
|
|||||||
@@ -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", ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加从轻量模式恢复的函数
|
// 添加从轻量模式恢复的函数
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
use std::sync::{Arc, Once, OnceLock};
|
|
||||||
|
|
||||||
use crate::{logging, utils::logging::Type};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LightWeightState {
|
|
||||||
#[allow(unused)]
|
|
||||||
once: Arc<Once>,
|
|
||||||
pub is_lightweight: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LightWeightState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
once: Arc::new(Once::new()),
|
|
||||||
is_lightweight: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
|
||||||
self.is_lightweight = value;
|
|
||||||
if value {
|
|
||||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LightWeightState {
|
|
||||||
fn default() -> Self {
|
|
||||||
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
|
|
||||||
INSTANCE.get_or_init(LightWeightState::new).clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
|
// 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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -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]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 测全部延迟
|
// 测全部延迟
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
45
src/hooks/use-i18n.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { changeLanguage, supportedLanguages } from "@/services/i18n";
|
||||||
|
import { useVerge } from "./use-verge";
|
||||||
|
|
||||||
|
export const useI18n = () => {
|
||||||
|
const { i18n, t } = useTranslation();
|
||||||
|
const { patchVerge } = useVerge();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const switchLanguage = useCallback(
|
||||||
|
async (language: string) => {
|
||||||
|
if (!supportedLanguages.includes(language)) {
|
||||||
|
console.warn(`Unsupported language: ${language}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i18n.language === language) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await changeLanguage(language);
|
||||||
|
|
||||||
|
if (patchVerge) {
|
||||||
|
await patchVerge({ language });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to change language:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[i18n.language, patchVerge],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentLanguage: i18n.language,
|
||||||
|
supportedLanguages,
|
||||||
|
switchLanguage,
|
||||||
|
isLoading,
|
||||||
|
t,
|
||||||
|
};
|
||||||
|
};
|
||||||
143
src/hooks/use-proxy-selection.ts
Normal file
143
src/hooks/use-proxy-selection.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import {
|
||||||
|
updateProxy,
|
||||||
|
updateProxyAndSync,
|
||||||
|
forceRefreshProxies,
|
||||||
|
syncTrayProxySelection,
|
||||||
|
getConnections,
|
||||||
|
deleteConnection,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
|
// 缓存连接清理
|
||||||
|
const cleanupConnections = async (previousProxy: string) => {
|
||||||
|
try {
|
||||||
|
const { connections } = await getConnections();
|
||||||
|
const cleanupPromises = connections
|
||||||
|
.filter((conn) => conn.chains.includes(previousProxy))
|
||||||
|
.map((conn) => deleteConnection(conn.id));
|
||||||
|
|
||||||
|
if (cleanupPromises.length > 0) {
|
||||||
|
await Promise.allSettled(cleanupPromises);
|
||||||
|
console.log(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ProxySelection] 连接清理失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProxySelectionOptions {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: any) => void;
|
||||||
|
enableConnectionCleanup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理选择 Hook
|
||||||
|
export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||||
|
const { current, patchCurrent } = useProfiles();
|
||||||
|
const { verge } = useVerge();
|
||||||
|
|
||||||
|
const { onSuccess, onError, enableConnectionCleanup = true } = options;
|
||||||
|
|
||||||
|
// 缓存
|
||||||
|
const config = useMemo(
|
||||||
|
() => ({
|
||||||
|
autoCloseConnection: verge?.auto_close_connection ?? false,
|
||||||
|
enableConnectionCleanup,
|
||||||
|
}),
|
||||||
|
[verge?.auto_close_connection, enableConnectionCleanup],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 切换节点
|
||||||
|
const changeProxy = useLockFn(
|
||||||
|
async (
|
||||||
|
groupName: string,
|
||||||
|
proxyName: string,
|
||||||
|
previousProxy?: string,
|
||||||
|
skipConfigSave: boolean = false,
|
||||||
|
) => {
|
||||||
|
console.log(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (current && !skipConfigSave) {
|
||||||
|
if (!current.selected) current.selected = [];
|
||||||
|
|
||||||
|
const index = current.selected.findIndex(
|
||||||
|
(item) => item.name === groupName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
current.selected.push({ name: groupName, now: proxyName });
|
||||||
|
} else {
|
||||||
|
current.selected[index] = { name: groupName, now: proxyName };
|
||||||
|
}
|
||||||
|
await patchCurrent({ selected: current.selected });
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProxyAndSync(groupName, proxyName);
|
||||||
|
console.log(
|
||||||
|
`[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
onSuccess?.();
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.enableConnectionCleanup &&
|
||||||
|
config.autoCloseConnection &&
|
||||||
|
previousProxy
|
||||||
|
) {
|
||||||
|
setTimeout(() => cleanupConnections(previousProxy), 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[ProxySelection] 代理切换失败: ${groupName} -> ${proxyName}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateProxy(groupName, proxyName);
|
||||||
|
await forceRefreshProxies();
|
||||||
|
await syncTrayProxySelection();
|
||||||
|
onSuccess?.();
|
||||||
|
console.log(
|
||||||
|
`[ProxySelection] 代理切换回退成功: ${groupName} -> ${proxyName}`,
|
||||||
|
);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error(
|
||||||
|
`[ProxySelection] 代理切换回退也失败: ${groupName} -> ${proxyName}`,
|
||||||
|
fallbackError,
|
||||||
|
);
|
||||||
|
onError?.(fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectChange = useCallback(
|
||||||
|
(
|
||||||
|
groupName: string,
|
||||||
|
previousProxy?: string,
|
||||||
|
skipConfigSave: boolean = false,
|
||||||
|
) =>
|
||||||
|
(event: { target: { value: string } }) => {
|
||||||
|
const newProxy = event.target.value;
|
||||||
|
changeProxy(groupName, newProxy, previousProxy, skipConfigSave);
|
||||||
|
},
|
||||||
|
[changeProxy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProxyGroupChange = useCallback(
|
||||||
|
(group: { name: string; now?: string }, proxy: { name: string }) => {
|
||||||
|
changeProxy(group.name, proxy.name, group.now);
|
||||||
|
},
|
||||||
|
[changeProxy],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeProxy,
|
||||||
|
handleSelectChange,
|
||||||
|
handleProxyGroupChange,
|
||||||
|
};
|
||||||
|
};
|
||||||
60
src/main.tsx
60
src/main.tsx
@@ -13,7 +13,7 @@ import { ComposeContextProvider } from "foxact/compose-context-provider";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { 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) => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{/* 首页设置弹窗 */}
|
{/* 首页设置弹窗 */}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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"',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user