mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-04 14:22:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7743c42dd1 | ||
|
|
3f3dab9495 | ||
|
|
2dc9672b20 | ||
|
|
2c90d1ca69 | ||
|
|
1555e2910d | ||
|
|
19b6bd35f5 | ||
|
|
3d4d60edc8 | ||
|
|
db0a000ecb | ||
|
|
24ff48b41c | ||
|
|
18a67d0b2f | ||
|
|
e23af1ad58 | ||
|
|
70af392059 | ||
|
|
d255df23ee | ||
|
|
1317a8b2db | ||
|
|
4cac118442 | ||
|
|
2ce43ccd23 | ||
|
|
d23b2949d8 | ||
|
|
3471476ba2 | ||
|
|
cee61e5619 | ||
|
|
5026e2bade | ||
|
|
83f005c256 | ||
|
|
80843c5ee3 | ||
|
|
ee00defe43 | ||
|
|
7696504d97 | ||
|
|
7a0e38a1b4 | ||
|
|
bc5d577553 | ||
|
|
cfed2b7236 | ||
|
|
e34e6654bc | ||
|
|
d6b85f1a01 | ||
|
|
16af7b53cb | ||
|
|
befc856207 | ||
|
|
5ab8e7a7c7 | ||
|
|
443bfa5928 | ||
|
|
52627575ff | ||
|
|
39973f2d24 | ||
|
|
2b01857d15 | ||
|
|
2650aa845f | ||
|
|
8400a61bf0 | ||
|
|
763b6b9003 | ||
|
|
053f414f3c | ||
|
|
2defa2cc56 | ||
|
|
bb1dbfcfc3 | ||
|
|
1378068a30 | ||
|
|
5d9dce7d10 | ||
|
|
acb66a3012 | ||
|
|
054f902cc6 | ||
|
|
4333153a59 | ||
|
|
19e623d7c2 | ||
|
|
2dd646537e | ||
|
|
1ece079978 | ||
|
|
0d8f779634 | ||
|
|
a7ffc9fc38 | ||
|
|
932b29e66f | ||
|
|
6be7a3b94c | ||
|
|
8e8dd1ec03 | ||
|
|
772f3268ce | ||
|
|
a179591ac2 | ||
|
|
d071e5971f | ||
|
|
a9032f5f20 | ||
|
|
0190702616 | ||
|
|
c35bf38420 | ||
|
|
89d20e564a | ||
|
|
3c24ff3afc | ||
|
|
daf0398750 | ||
|
|
6b14c2b763 | ||
|
|
5bf2f9b8ed | ||
|
|
10a2655288 | ||
|
|
ac07397818 | ||
|
|
30d061d00f | ||
|
|
360d8a5143 | ||
|
|
70e0a5adc8 | ||
|
|
ef52f81494 | ||
|
|
e5c1de3ad3 | ||
|
|
576fd700ae | ||
|
|
476df65a1b | ||
|
|
bd0a4863a8 | ||
|
|
d25fbc05e2 | ||
|
|
b8a0b6f1f4 | ||
|
|
18c7ed1ccc | ||
|
|
b14db06955 | ||
|
|
215dcee3f1 | ||
|
|
37e5c22a5a | ||
|
|
bb0b6c3f77 | ||
|
|
a37df46ce7 | ||
|
|
73fbec2514 | ||
|
|
f66fa08b2c | ||
|
|
bae606bc9d | ||
|
|
1a2d5e988a | ||
|
|
d20bd62b90 | ||
|
|
204bf43b9a | ||
|
|
3ea0d20e2c |
+29
-15
@@ -2,9 +2,6 @@ name: Alpha Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
tags-ignore: [updater, alpha]
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -58,7 +55,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "22"
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -70,6 +67,9 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Alpha Version update
|
||||||
|
run: pnpm run fix-alpha-version
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "22"
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -248,7 +248,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "22"
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -281,9 +281,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Rename
|
- name: Rename
|
||||||
run: |
|
run: |
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
|
foreach ($file in $files) {
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
|
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
@@ -313,12 +327,12 @@ jobs:
|
|||||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Update Tag
|
# - name: Update Tag
|
||||||
uses: richardsimko/update-tag@v1
|
# uses: richardsimko/update-tag@v1
|
||||||
with:
|
# with:
|
||||||
tag_name: alpha
|
# tag_name: alpha
|
||||||
env:
|
# env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
cat > release.txt << 'EOF'
|
cat > release.txt << 'EOF'
|
||||||
|
|||||||
@@ -271,9 +271,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Rename
|
- name: Rename
|
||||||
run: |
|
run: |
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
|
foreach ($file in $files) {
|
||||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
|
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
|
|
||||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### Donation
|
||||||
|
|
||||||
|
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||||
|
|||||||
+103
@@ -1,3 +1,106 @@
|
|||||||
|
## v2.1.2
|
||||||
|
|
||||||
|
**发行代号:臻**
|
||||||
|
|
||||||
|
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
|
||||||
|
|
||||||
|
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||||
|
|
||||||
|
2.1.2相对2.1.1(已下架不在提供)更新了:
|
||||||
|
|
||||||
|
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
||||||
|
- 设置菜单区分Verge基本设置和高级设置
|
||||||
|
- 增加v2 Updater的更多功能和权限
|
||||||
|
- 退出Verge后Tun代理状态仍保留的问题
|
||||||
|
|
||||||
|
2.1.1相对2.1.0(已下架不在提供)更新了:
|
||||||
|
|
||||||
|
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
||||||
|
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
||||||
|
- 文件类型判断不准导致脚本检测报错的问题
|
||||||
|
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
|
||||||
|
- 边框去白边
|
||||||
|
- 修复Linux下编译问题
|
||||||
|
- 修复热键无法关闭面板的问题
|
||||||
|
|
||||||
|
## v2.1.0 - 发行代号:臻
|
||||||
|
|
||||||
|
### 功能新增
|
||||||
|
|
||||||
|
- 新增窗口状态实时监控与自动保存功能
|
||||||
|
- 增强核心配置变更时的验证与错误处理机制
|
||||||
|
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||||
|
- 添加连接表列宽持久化设置与进程过滤功能
|
||||||
|
- 新增代理组首字母导航与动态滚动定位功能
|
||||||
|
- 实现连接追踪暂停/恢复功能
|
||||||
|
- 支持从托盘菜单快速切换代理配置
|
||||||
|
- 添加轻量级模式开关选项
|
||||||
|
- 允许用户自定义TUN模式增强类型和FakeIP范围
|
||||||
|
- 新增系统代理状态指示器
|
||||||
|
- 增加Alpha版本自动重命名逻辑
|
||||||
|
- 优化字母导航工具提示与防抖交互机制
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
|
||||||
|
- 重构代理列表渲染逻辑,提升布局计算效率
|
||||||
|
- 优化代理数据更新机制,采用乐观UI策略
|
||||||
|
- 改进虚拟列表渲染性能(Virtuoso)
|
||||||
|
- 提升主窗口Clash模式切换速度(感谢Tunglies)
|
||||||
|
- 加速内核关闭流程并优化管理逻辑
|
||||||
|
- 优化节点延迟刷新速率
|
||||||
|
- 改进托盘网速显示更新逻辑
|
||||||
|
- 提升配置验证错误信息的可读性
|
||||||
|
- 重构服务架构,优化代码组织结构(感谢Tunglies)
|
||||||
|
- 优化内核启动时的配置验证流程
|
||||||
|
|
||||||
|
### 问题修复
|
||||||
|
|
||||||
|
- 修复删除节点时关联组信息残留问题
|
||||||
|
- 解决菜单切换异常与重复勾选问题
|
||||||
|
- 修正连接页流量计算错误
|
||||||
|
- 修复Windows圆角显示异常问题
|
||||||
|
- 解决控制台废弃API警告
|
||||||
|
- 修复全局热键空值导致的崩溃
|
||||||
|
- 修复Alpha版本Windows打包重命名问题
|
||||||
|
- 修复MacOS端口切换崩溃问题
|
||||||
|
- 解决Linux持续集成更新器问题
|
||||||
|
- 修复静默启动后热键失效问题
|
||||||
|
- 修正TypeScript代理组类型定义
|
||||||
|
- 修复Windows托盘图标空白问题
|
||||||
|
- 优化远程目标地址显示(替换旧版IP展示)
|
||||||
|
|
||||||
|
### 交互体验
|
||||||
|
|
||||||
|
- 统一多平台托盘图标点击行为
|
||||||
|
- 优化代理列表滚动流畅度
|
||||||
|
- 改进日志搜索功能与数据管理
|
||||||
|
- 重构热键管理逻辑,修复托盘冻结问题
|
||||||
|
- 优化托盘网速显示样式
|
||||||
|
- 增强字母导航工具提示的动态响应
|
||||||
|
|
||||||
|
### 国际化
|
||||||
|
|
||||||
|
- 新增配置检查多语言支持
|
||||||
|
- 添加轻量级模式多语言文本
|
||||||
|
- 完善多语言翻译内容
|
||||||
|
|
||||||
|
### 维护更新
|
||||||
|
|
||||||
|
- 将默认TUN协议栈改为gVisor
|
||||||
|
- 更新Node.js运行版本
|
||||||
|
- 移除自动生成更新器文件
|
||||||
|
- 清理废弃代码与未使用组件
|
||||||
|
- 禁用工作流自动Alpha标签更新
|
||||||
|
- 更新依赖库版本
|
||||||
|
- 添加MacOS格式转换函数专项测试
|
||||||
|
- 优化开发模式日志输出
|
||||||
|
|
||||||
|
### 安全增强
|
||||||
|
|
||||||
|
- 强化应用启动时的配置验证机制
|
||||||
|
- 改进脚本验证与异常处理流程
|
||||||
|
- 修复编译警告(移除无用导入)
|
||||||
|
|
||||||
## v2.0.3
|
## v2.0.3
|
||||||
|
|
||||||
### Notice
|
### Notice
|
||||||
|
|||||||
+24
-23
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.0.3",
|
"version": "2.1.2",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||||
@@ -15,25 +15,26 @@
|
|||||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||||
"portable": "node scripts/portable.mjs",
|
"portable": "node scripts/portable.mjs",
|
||||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
|
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^6.3.0",
|
"@mui/icons-material": "^6.4.2",
|
||||||
"@mui/lab": "5.0.0-alpha.149",
|
"@mui/lab": "6.0.0-beta.25",
|
||||||
"@mui/material": "^6.3.0",
|
"@mui/material": "^6.4.2",
|
||||||
"@mui/x-data-grid": "^7.23.5",
|
"@mui/x-data-grid": "^7.25.0",
|
||||||
"@tauri-apps/api": "2.1.1",
|
"@tauri-apps/api": "2.2.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||||
"@tauri-apps/plugin-notification": "^2.2.0",
|
"@tauri-apps/plugin-notification": "^2.2.1",
|
||||||
"@tauri-apps/plugin-process": "^2.2.0",
|
"@tauri-apps/plugin-process": "^2.2.0",
|
||||||
"@tauri-apps/plugin-shell": "2.2.0",
|
"@tauri-apps/plugin-shell": "2.2.0",
|
||||||
"@tauri-apps/plugin-updater": "2.3.0",
|
"@tauri-apps/plugin-updater": "2.3.0",
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"foxact": "^0.2.43",
|
"foxact": "^0.2.43",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.1",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^24.2.2",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@@ -54,31 +55,31 @@
|
|||||||
"peggy": "^4.2.0",
|
"peggy": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.0.12",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-monaco-editor": "^0.56.2",
|
"react-monaco-editor": "^0.56.0",
|
||||||
"react-router-dom": "^6.28.1",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-virtuoso": "^4.12.3",
|
"react-virtuoso": "^4.6.3",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"swr": "^2.3.0",
|
"swr": "^2.3.0",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.0",
|
||||||
"@tauri-apps/cli": "2.1.0",
|
"@tauri-apps/cli": "2.2.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@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": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@vitejs/plugin-legacy": "^5.4.3",
|
"@vitejs/plugin-legacy": "^6.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
@@ -87,10 +88,10 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"pretty-quick": "^4.0.0",
|
"pretty-quick": "^4.0.0",
|
||||||
"sass": "^1.83.0",
|
"sass": "^1.83.4",
|
||||||
"terser": "^5.37.0",
|
"terser": "^5.37.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.11",
|
"vite": "^6.0.11",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+1106
-1202
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为Alpha版本重命名版本号
|
||||||
|
*/
|
||||||
|
const execPromise = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准输出HEAD hash
|
||||||
|
*/
|
||||||
|
async function getLatestCommitHash() {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise("git rev-parse HEAD");
|
||||||
|
const commitHash = stdout.trim();
|
||||||
|
// 格式化,只截取前7位字符
|
||||||
|
const formathash = commitHash.substring(0, 7);
|
||||||
|
console.log(`Found the latest commit hash code: ${commitHash}`);
|
||||||
|
return formathash;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string 传入格式化后的hash
|
||||||
|
* 将新的版本号写入文件 package.json
|
||||||
|
*/
|
||||||
|
async function updatePackageVersion(newVersion) {
|
||||||
|
// 获取内容根目录
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
const initversion = packageJson.version;
|
||||||
|
// 将匹配到的第一个 "alpha" => 具体的hash
|
||||||
|
const fixversion = initversion.replace("alpha", newVersion);
|
||||||
|
packageJson.version = fixversion;
|
||||||
|
// 写入版本号
|
||||||
|
await fs.writeFile(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(`Alpha version update to: ${fixversion}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newVersion = await getLatestCommitHash();
|
||||||
|
updatePackageVersion(newVersion);
|
||||||
+4
-4
@@ -5,7 +5,7 @@ import { extract } from "tar";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import proxyAgent from "https-proxy-agent";
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
@@ -85,7 +85,7 @@ async function getLatestAlphaVersion() {
|
|||||||
process.env.https_proxy;
|
process.env.https_proxy;
|
||||||
|
|
||||||
if (httpProxy) {
|
if (httpProxy) {
|
||||||
options.agent = proxyAgent(httpProxy);
|
options.agent = new HttpsProxyAgent(httpProxy);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||||
@@ -132,7 +132,7 @@ async function getLatestReleaseVersion() {
|
|||||||
process.env.https_proxy;
|
process.env.https_proxy;
|
||||||
|
|
||||||
if (httpProxy) {
|
if (httpProxy) {
|
||||||
options.agent = proxyAgent(httpProxy);
|
options.agent = new HttpsProxyAgent(httpProxy);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(META_VERSION_URL, {
|
const response = await fetch(META_VERSION_URL, {
|
||||||
@@ -332,7 +332,7 @@ async function resolveResource(binInfo) {
|
|||||||
process.env.https_proxy;
|
process.env.https_proxy;
|
||||||
|
|
||||||
if (httpProxy) {
|
if (httpProxy) {
|
||||||
options.agent = proxyAgent(httpProxy);
|
options.agent = new HttpsProxyAgent(httpProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|||||||
+6
-6
@@ -60,35 +60,35 @@ async function resolveUpdater() {
|
|||||||
const { name, browser_download_url } = asset;
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
// win64 url
|
// win64 url
|
||||||
if (name.endsWith("x64-setup.nsis.zip")) {
|
if (name.endsWith("x64-setup.exe")) {
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// win64 signature
|
// win64 signature
|
||||||
if (name.endsWith("x64-setup.nsis.zip.sig")) {
|
if (name.endsWith("x64-setup.exe.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms.win64.signature = sig;
|
updateData.platforms.win64.signature = sig;
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// win32 url
|
// win32 url
|
||||||
if (name.endsWith("x86-setup.nsis.zip")) {
|
if (name.endsWith("x86-setup.exe")) {
|
||||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// win32 signature
|
// win32 signature
|
||||||
if (name.endsWith("x86-setup.nsis.zip.sig")) {
|
if (name.endsWith("x86-setup.exe.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms["windows-x86"].signature = sig;
|
updateData.platforms["windows-x86"].signature = sig;
|
||||||
updateData.platforms["windows-i686"].signature = sig;
|
updateData.platforms["windows-i686"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// win arm url
|
// win arm url
|
||||||
if (name.endsWith("arm64-setup.nsis.zip")) {
|
if (name.endsWith("arm64-setup.exe")) {
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// win arm signature
|
// win arm signature
|
||||||
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
|
if (name.endsWith("arm64-setup.exe.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms["windows-aarch64"].signature = sig;
|
updateData.platforms["windows-aarch64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+807
-752
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.0.3"
|
version = "2.1.2"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -13,36 +13,36 @@ 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.0.3", features = [] }
|
tauri-build = { version = "2.0.5", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "6.0"
|
||||||
open = "5.1"
|
open = "5.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
dunce = "1.0"
|
dunce = "1.0"
|
||||||
log4rs = "1"
|
log4rs = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
sysinfo = "0.33.0"
|
sysinfo = "0.33.1"
|
||||||
boa_engine = "0.20.0"
|
boa_engine = "0.20.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
window-shadows = { version = "0.2.2" }
|
window-shadows = { version = "0.2.2" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||||
image = "0.24"
|
image = "0.24"
|
||||||
imageproc = "0.23"
|
imageproc = "0.23"
|
||||||
rusttype = "0.9"
|
rusttype = "0.9"
|
||||||
tauri = { version = "2.1.1", features = [
|
tauri = { version = "2.2.5", features = [
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
"devtools",
|
"devtools",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
@@ -53,13 +53,13 @@ network-interface = { version = "2.0.0", features = ["serde"] }
|
|||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
tauri-plugin-dialog = "2.2.0"
|
tauri-plugin-dialog = "2.2.0"
|
||||||
tauri-plugin-fs = "2.2.0"
|
tauri-plugin-fs = "2.2.0"
|
||||||
tauri-plugin-notification = "2.2.0"
|
tauri-plugin-notification = "2.2.1"
|
||||||
tauri-plugin-process = "2.2.0"
|
tauri-plugin-process = "2.2.0"
|
||||||
tauri-plugin-clipboard-manager = "2.2.0"
|
tauri-plugin-clipboard-manager = "2.2.1"
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
tauri-plugin-deep-link = "2.2.0"
|
||||||
tauri-plugin-devtools = "2.0.0-rc"
|
tauri-plugin-devtools = "2.0.0-rc"
|
||||||
url = "2.5.2"
|
url = "2.5.4"
|
||||||
zip = "2.2.0"
|
zip = "2.2.2"
|
||||||
reqwest_dav = "0.1.14"
|
reqwest_dav = "0.1.14"
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
@@ -71,8 +71,8 @@ sys-locale = "0.3.1"
|
|||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
deelevate = "0.2.0"
|
deelevate = "0.2.0"
|
||||||
winreg = "0.52.0"
|
winreg = "0.55.0"
|
||||||
url = "2.5.2"
|
url = "2.5.4"
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
@@ -82,7 +82,7 @@ users = "0.11.0"
|
|||||||
tauri-plugin-autostart = "2.2.0"
|
tauri-plugin-autostart = "2.2.0"
|
||||||
tauri-plugin-global-shortcut = "2.2.0"
|
tauri-plugin-global-shortcut = "2.2.0"
|
||||||
tauri-plugin-updater = "2.3.0"
|
tauri-plugin-updater = "2.3.0"
|
||||||
tauri-plugin-window-state = "2.2.0"
|
tauri-plugin-window-state = "2.2.1"
|
||||||
#openssl
|
#openssl
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,13 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"global-shortcut:default",
|
"global-shortcut:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
|
"dialog:default",
|
||||||
|
"dialog:allow-ask",
|
||||||
|
"dialog:allow-message",
|
||||||
|
"updater:default",
|
||||||
|
"updater:allow-check",
|
||||||
|
"updater:allow-download-and-install",
|
||||||
|
"process:allow-restart",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"window-state:default",
|
"window-state:default",
|
||||||
"window-state:default",
|
"window-state:default",
|
||||||
|
|||||||
+292
-23
@@ -13,6 +13,7 @@ use sysproxy::{Autoproxy, Sysproxy};
|
|||||||
type CmdResult<T = ()> = Result<T, String>;
|
type CmdResult<T = ()> = Result<T, String>;
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_clash_env() -> CmdResult {
|
pub fn copy_clash_env() -> CmdResult {
|
||||||
@@ -22,15 +23,30 @@ pub fn copy_clash_env() -> CmdResult {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
|
let _ = tray::Tray::global().update_menu();
|
||||||
Ok(Config::profiles().data().clone())
|
Ok(Config::profiles().data().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn enhance_profiles() -> CmdResult {
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
match CoreManager::global().update_config().await {
|
||||||
log_err!(tray::Tray::global().update_tooltip());
|
Ok((true, _)) => {
|
||||||
handle::Handle::refresh_clash();
|
println!("[enhance_profiles] 配置更新成功");
|
||||||
Ok(())
|
log_err!(tray::Tray::global().update_tooltip());
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[enhance_profiles] 更新过程发生错误: {}", e);
|
||||||
|
handle::Handle::notice_message("config_validate::process_terminated", &e.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -62,36 +78,80 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改profiles的
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
pub async fn patch_profiles_config(
|
||||||
|
profiles: IProfiles
|
||||||
|
) -> CmdResult<bool> {
|
||||||
|
println!("[cmd配置patch] 开始修改配置文件");
|
||||||
|
|
||||||
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
|
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
||||||
|
|
||||||
|
// 更新profiles配置
|
||||||
|
println!("[cmd配置patch] 正在更新配置草稿");
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||||
|
|
||||||
|
// 更新配置并进行验证
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok((true, _)) => {
|
||||||
|
println!("[cmd配置patch] 配置更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
let _ = tray::Tray::global().update_tooltip();
|
let _ = tray::Tray::global().update_tooltip();
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
Ok(())
|
Ok(true)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
Err(format!("{err}"))
|
// 如果验证失败,恢复到之前的配置
|
||||||
|
if let Some(prev_profile) = current_profile {
|
||||||
|
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||||
|
let restore_profiles = IProfiles {
|
||||||
|
current: Some(prev_profile),
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
|
// 静默恢复,不触发验证
|
||||||
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证错误通知
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[cmd配置patch] 更新过程发生错误: {}", e);
|
||||||
|
Config::profiles().discard();
|
||||||
|
handle::Handle::notice_message("config_validate::boot_error", &e.to_string());
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据profile name修改profiles
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_profiles_config_by_profile_index(
|
||||||
|
_app_handle: tauri::AppHandle,
|
||||||
|
profile_index: String
|
||||||
|
) -> CmdResult<bool> {
|
||||||
|
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||||
|
patch_profiles_config(profiles).await
|
||||||
|
}
|
||||||
|
|
||||||
/// 修改某个profile item的
|
/// 修改某个profile item的
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
wrap_err!(timer::Timer::global().refresh())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -119,17 +179,107 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
let data = wrap_err!(item.read_file())?;
|
let data = wrap_err!(item.read_file())?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
/// 保存profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||||
if file_data.is_none() {
|
if file_data.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let profiles = Config::profiles();
|
// 在异步操作前完成所有文件操作
|
||||||
let profiles = profiles.latest();
|
let (file_path, original_content, is_merge_file) = {
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
let profiles = Config::profiles();
|
||||||
wrap_err!(item.save_file(file_data.unwrap()))
|
let profiles_guard = profiles.latest();
|
||||||
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
|
// 确定是否为merge类型文件
|
||||||
|
let is_merge = item.itype.as_ref().map_or(false, |t| t == "merge");
|
||||||
|
let content = wrap_err!(item.read_file())?;
|
||||||
|
let path = item.file.clone().ok_or("file field is null")?;
|
||||||
|
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||||
|
(profiles_dir.join(path), content, is_merge)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存新的配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||||
|
|
||||||
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
|
println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
|
||||||
|
|
||||||
|
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||||
|
if is_merge_file {
|
||||||
|
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||||
|
match CoreManager::global().validate_config_file(&file_path_str, Some(true)).await {
|
||||||
|
Ok((true, _)) => {
|
||||||
|
println!("[cmd配置save] merge文件语法验证通过");
|
||||||
|
// 成功后尝试更新整体配置
|
||||||
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
|
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||||
|
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
// 发送合并文件专用错误通知
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
handle_yaml_validation_notice(&result, "合并配置文件");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非merge文件使用完整验证流程
|
||||||
|
match CoreManager::global().validate_config_file(&file_path_str, None).await {
|
||||||
|
Ok((true, _)) => {
|
||||||
|
println!("[cmd配置save] 验证成功");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
|
||||||
|
// 智能判断错误类型
|
||||||
|
let is_script_error = file_path_str.ends_with(".js") ||
|
||||||
|
error_msg.contains("Script syntax error") ||
|
||||||
|
error_msg.contains("Script must contain a main function") ||
|
||||||
|
error_msg.contains("Failed to read script file");
|
||||||
|
|
||||||
|
if error_msg.contains("YAML syntax error") ||
|
||||||
|
error_msg.contains("Failed to read file:") ||
|
||||||
|
(!file_path_str.ends_with(".js") && !is_script_error) {
|
||||||
|
// 普通YAML错误使用YAML通知处理
|
||||||
|
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||||
|
} else if is_script_error {
|
||||||
|
// 脚本错误使用专门的通知处理
|
||||||
|
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
handle_script_validation_notice(&result, "脚本文件");
|
||||||
|
} else {
|
||||||
|
// 普通配置错误使用一般通知
|
||||||
|
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
Err(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -169,6 +319,12 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
|||||||
wrap_err!(feat::patch_clash(payload).await)
|
wrap_err!(feat::patch_clash(payload).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||||
|
Ok(feat::change_clash_mode(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
@@ -178,12 +334,27 @@ pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||||
wrap_err!(feat::patch_verge(payload).await)
|
wrap_err!(feat::patch_verge(payload, false).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||||
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
log::info!(target: "app", "changing core to {clash_core}");
|
||||||
|
|
||||||
|
match CoreManager::global().change_core(Some(clash_core.clone())).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "core changed to {clash_core}");
|
||||||
|
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = err.to_string();
|
||||||
|
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||||
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
|
Ok(Some(error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// restart the sidecar
|
/// restart the sidecar
|
||||||
@@ -427,3 +598,101 @@ pub mod uwp {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||||
|
handle::Handle::notice_message(&status, &msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理脚本验证相关的所有消息通知
|
||||||
|
/// 统一通知接口,保持消息类型一致性
|
||||||
|
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
|
if !result.0 {
|
||||||
|
let error_msg = &result.1;
|
||||||
|
|
||||||
|
// 根据错误消息内容判断错误类型
|
||||||
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
|
"config_validate::file_not_found"
|
||||||
|
} else if error_msg.starts_with("Failed to read script file:") {
|
||||||
|
"config_validate::script_error"
|
||||||
|
} else if error_msg.starts_with("Script syntax error:") {
|
||||||
|
"config_validate::script_syntax_error"
|
||||||
|
} else if error_msg == "Script must contain a main function" {
|
||||||
|
"config_validate::script_missing_main"
|
||||||
|
} else {
|
||||||
|
// 如果是其他类型错误,作为一般脚本错误处理
|
||||||
|
"config_validate::script_error"
|
||||||
|
};
|
||||||
|
|
||||||
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
|
handle::Handle::notice_message(status, error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证指定脚本文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||||
|
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||||
|
|
||||||
|
match CoreManager::global().validate_config_file(&file_path, None).await {
|
||||||
|
Ok(result) => {
|
||||||
|
handle_script_validation_notice(&result, "脚本文件");
|
||||||
|
Ok(result.0) // 返回验证结果布尔值
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||||
|
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理YAML验证相关的所有消息通知
|
||||||
|
/// 统一通知接口,保持消息类型一致性
|
||||||
|
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
|
if !result.0 {
|
||||||
|
let error_msg = &result.1;
|
||||||
|
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||||
|
|
||||||
|
// 检查是否为merge文件
|
||||||
|
let is_merge_file = file_type.contains("合并");
|
||||||
|
|
||||||
|
// 根据错误消息内容判断错误类型
|
||||||
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
|
"config_validate::file_not_found"
|
||||||
|
} else if error_msg.starts_with("Failed to read file:") {
|
||||||
|
"config_validate::yaml_read_error"
|
||||||
|
} else if error_msg.starts_with("YAML syntax error:") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_syntax_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_syntax_error"
|
||||||
|
}
|
||||||
|
} else if error_msg.contains("mapping values are not allowed") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_mapping_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_mapping_error"
|
||||||
|
}
|
||||||
|
} else if error_msg.contains("did not find expected key") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_key_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_key_error"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是其他类型错误,根据文件类型作为一般错误处理
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
|
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||||
|
handle::Handle::notice_message(status, error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ use crate::{
|
|||||||
config::PrfItem,
|
config::PrfItem,
|
||||||
enhance,
|
enhance,
|
||||||
utils::{dirs, help},
|
utils::{dirs, help},
|
||||||
|
core::{handle, CoreManager},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
@@ -64,20 +66,57 @@ impl Config {
|
|||||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||||
Self::profiles().data().append_item(script_item.clone())?;
|
Self::profiles().data().append_item(script_item.clone())?;
|
||||||
}
|
}
|
||||||
crate::log_err!(Self::generate().await);
|
|
||||||
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
|
|
||||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
// 生成运行时配置
|
||||||
// 如果不存在就将默认的clash文件拿过来
|
crate::log_err!(Self::generate().await);
|
||||||
if !runtime_path.exists() {
|
|
||||||
help::save_yaml(
|
// 生成运行时配置文件并验证
|
||||||
&runtime_path,
|
let config_result = Self::generate_file(ConfigType::Run);
|
||||||
&Config::clash().latest().0,
|
|
||||||
Some("# Clash Verge Runtime"),
|
let validation_result = if let Ok(_) = config_result {
|
||||||
)?;
|
// 验证配置文件
|
||||||
|
println!("[首次启动] 开始验证配置");
|
||||||
|
|
||||||
|
match CoreManager::global().validate_config().await {
|
||||||
|
Ok((is_valid, error_msg)) => {
|
||||||
|
if !is_valid {
|
||||||
|
println!("[首次启动] 配置验证失败,使用默认最小配置启动: {}", error_msg);
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config("config_validate::boot_error", &error_msg)
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::boot_error", error_msg))
|
||||||
|
} else {
|
||||||
|
println!("[首次启动] 配置验证成功");
|
||||||
|
Some(("config_validate::success", String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("[首次启动] 验证进程执行失败: {}", err);
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config("config_validate::process_terminated", "")
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::process_terminated", String::new()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config(
|
||||||
|
"config_validate::error",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::error", String::new()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在单独的任务中发送通知
|
||||||
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
handle::Handle::notice_message(msg_type, &msg_content);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ impl IProfiles {
|
|||||||
if let Some(current) = patch.current {
|
if let Some(current) = patch.current {
|
||||||
let items = self.items.as_ref().unwrap();
|
let items = self.items.as_ref().unwrap();
|
||||||
let some_uid = Some(current);
|
let some_uid = Some(current);
|
||||||
|
|
||||||
if items.iter().any(|e| e.uid == some_uid) {
|
if items.iter().any(|e| e.uid == some_uid) {
|
||||||
self.current = some_uid;
|
self.current = some_uid;
|
||||||
}
|
}
|
||||||
@@ -465,4 +464,23 @@ impl IProfiles {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 判断profile是否是current指向的
|
||||||
|
pub fn is_current_profile_index(&self, index: String) -> bool {
|
||||||
|
self.current == Some(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有的profiles(uid,名称)
|
||||||
|
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
match self.items.as_ref() {
|
||||||
|
Some(items) => Some(items.iter().filter_map(|e| {
|
||||||
|
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||||
|
Some((uid, name))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).collect()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ pub struct IVerge {
|
|||||||
/// hotkey map
|
/// hotkey map
|
||||||
/// format: {func},{key}
|
/// format: {func},{key}
|
||||||
pub hotkeys: Option<Vec<String>>,
|
pub hotkeys: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// enable global hotkey
|
||||||
|
pub enable_global_hotkey: Option<bool>,
|
||||||
|
|
||||||
/// 切换代理时自动关闭连接
|
/// 切换代理时自动关闭连接
|
||||||
pub auto_close_connection: Option<bool>,
|
pub auto_close_connection: Option<bool>,
|
||||||
@@ -178,6 +181,9 @@ pub struct IVerge {
|
|||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
|
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
|
||||||
|
/// 轻量模式 - 只保留内核运行
|
||||||
|
pub enable_lite_mode: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -279,6 +285,8 @@ impl IVerge {
|
|||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
enable_tray_speed: Some(true),
|
enable_tray_speed: Some(true),
|
||||||
|
enable_global_hotkey: Some(true),
|
||||||
|
enable_lite_mode: Some(false),
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,6 +353,7 @@ impl IVerge {
|
|||||||
patch!(web_ui_list);
|
patch!(web_ui_list);
|
||||||
patch!(clash_core);
|
patch!(clash_core);
|
||||||
patch!(hotkeys);
|
patch!(hotkeys);
|
||||||
|
patch!(enable_global_hotkey);
|
||||||
|
|
||||||
patch!(auto_close_connection);
|
patch!(auto_close_connection);
|
||||||
patch!(auto_check_update);
|
patch!(auto_check_update);
|
||||||
@@ -359,6 +368,7 @@ impl IVerge {
|
|||||||
patch!(webdav_username);
|
patch!(webdav_username);
|
||||||
patch!(webdav_password);
|
patch!(webdav_password);
|
||||||
patch!(enable_tray_speed);
|
patch!(enable_tray_speed);
|
||||||
|
patch!(enable_lite_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
@@ -411,6 +421,7 @@ pub struct IVergeResponse {
|
|||||||
pub enable_silent_start: Option<bool>,
|
pub enable_silent_start: Option<bool>,
|
||||||
pub enable_system_proxy: Option<bool>,
|
pub enable_system_proxy: Option<bool>,
|
||||||
pub enable_proxy_guard: Option<bool>,
|
pub enable_proxy_guard: Option<bool>,
|
||||||
|
pub enable_global_hotkey: Option<bool>,
|
||||||
pub use_default_bypass: Option<bool>,
|
pub use_default_bypass: Option<bool>,
|
||||||
pub system_proxy_bypass: Option<String>,
|
pub system_proxy_bypass: Option<String>,
|
||||||
pub proxy_guard_duration: Option<u64>,
|
pub proxy_guard_duration: Option<u64>,
|
||||||
@@ -446,6 +457,7 @@ pub struct IVergeResponse {
|
|||||||
pub webdav_username: Option<String>,
|
pub webdav_username: Option<String>,
|
||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
pub enable_lite_mode: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IVerge> for IVergeResponse {
|
impl From<IVerge> for IVergeResponse {
|
||||||
@@ -472,6 +484,7 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
enable_silent_start: verge.enable_silent_start,
|
enable_silent_start: verge.enable_silent_start,
|
||||||
enable_system_proxy: verge.enable_system_proxy,
|
enable_system_proxy: verge.enable_system_proxy,
|
||||||
enable_proxy_guard: verge.enable_proxy_guard,
|
enable_proxy_guard: verge.enable_proxy_guard,
|
||||||
|
enable_global_hotkey: verge.enable_global_hotkey,
|
||||||
use_default_bypass: verge.use_default_bypass,
|
use_default_bypass: verge.use_default_bypass,
|
||||||
system_proxy_bypass: verge.system_proxy_bypass,
|
system_proxy_bypass: verge.system_proxy_bypass,
|
||||||
proxy_guard_duration: verge.proxy_guard_duration,
|
proxy_guard_duration: verge.proxy_guard_duration,
|
||||||
@@ -507,6 +520,7 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
webdav_username: verge.webdav_username,
|
webdav_username: verge.webdav_username,
|
||||||
webdav_password: verge.webdav_password,
|
webdav_password: verge.webdav_password,
|
||||||
enable_tray_speed: verge.enable_tray_speed,
|
enable_tray_speed: verge.enable_tray_speed,
|
||||||
|
enable_lite_mode: verge.enable_lite_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+361
-59
@@ -1,8 +1,9 @@
|
|||||||
use super::tray::Tray;
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::core::{clash_api, handle, service};
|
use crate::core::{clash_api, handle, service};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::core::tray::Tray;
|
||||||
use crate::log_err;
|
use crate::log_err;
|
||||||
use crate::utils::dirs;
|
use crate::utils::{dirs, help};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
@@ -32,28 +33,6 @@ impl CoreManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查订阅是否正确
|
|
||||||
pub async fn check_config(&self) -> Result<()> {
|
|
||||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
|
||||||
let config_path = dirs::path_to_str(&config_path)?;
|
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
|
||||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
|
||||||
|
|
||||||
let test_dir = dirs::app_home_dir()?.join("test");
|
|
||||||
let test_dir = dirs::path_to_str(&test_dir)?;
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
|
|
||||||
let _ = app_handle
|
|
||||||
.shell()
|
|
||||||
.sidecar(clash_core)?
|
|
||||||
.args(["-t", "-d", test_dir, "-f", config_path])
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止核心运行
|
/// 停止核心运行
|
||||||
pub async fn stop_core(&self) -> Result<()> {
|
pub async fn stop_core(&self) -> Result<()> {
|
||||||
let mut running = self.running.lock().await;
|
let mut running = self.running.lock().await;
|
||||||
@@ -112,6 +91,23 @@ impl CoreManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 使用默认配置
|
||||||
|
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||||
|
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||||
|
*Config::runtime().draft() = IRuntime {
|
||||||
|
config: Some(Config::clash().latest().0.clone()),
|
||||||
|
exists_keys: vec![],
|
||||||
|
chain_logs: Default::default(),
|
||||||
|
};
|
||||||
|
help::save_yaml(
|
||||||
|
&runtime_path,
|
||||||
|
&Config::clash().latest().0,
|
||||||
|
Some("# Clash Verge Runtime"),
|
||||||
|
)?;
|
||||||
|
handle::Handle::notice_message(msg_type, msg_content);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 切换核心
|
/// 切换核心
|
||||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||||
@@ -122,57 +118,363 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log::info!(target: "app", "change core to `{clash_core}`");
|
log::info!(target: "app", "change core to `{clash_core}`");
|
||||||
|
|
||||||
|
// 1. 先更新内核配置(但不应用)
|
||||||
Config::verge().draft().clash_core = Some(clash_core);
|
Config::verge().draft().clash_core = Some(clash_core);
|
||||||
|
|
||||||
// 更新订阅
|
// 2. 使用新内核验证配置
|
||||||
Config::generate().await?;
|
println!("[切换内核] 使用新内核验证配置");
|
||||||
|
match self.validate_config().await {
|
||||||
self.check_config().await?;
|
Ok((true, _)) => {
|
||||||
|
println!("[切换内核] 配置验证通过,开始切换内核");
|
||||||
match self.restart_core().await {
|
// 3. 验证通过后,应用内核配置并重启
|
||||||
Ok(_) => {
|
|
||||||
Config::verge().apply();
|
Config::verge().apply();
|
||||||
Config::runtime().apply();
|
|
||||||
log_err!(Config::verge().latest().save_file());
|
log_err!(Config::verge().latest().save_file());
|
||||||
Ok(())
|
|
||||||
|
match self.restart_core().await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("[切换内核] 内核切换成功");
|
||||||
|
Config::runtime().apply();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("[切换内核] 内核切换失败: {}", err);
|
||||||
|
Config::verge().discard();
|
||||||
|
Config::runtime().discard();
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[切换内核] 配置验证失败: {}", error_msg);
|
||||||
|
// 使用默认配置并继续切换内核
|
||||||
|
self.use_default_config("config_validate::core_change", &error_msg).await?;
|
||||||
|
Config::verge().apply();
|
||||||
|
log_err!(Config::verge().latest().save_file());
|
||||||
|
|
||||||
|
match self.restart_core().await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("[切换内核] 内核切换成功(使用默认配置)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("[切换内核] 内核切换失败: {}", err);
|
||||||
|
Config::verge().discard();
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
println!("[切换内核] 验证过程发生错误: {}", err);
|
||||||
Config::verge().discard();
|
Config::verge().discard();
|
||||||
Config::runtime().discard();
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新proxies那些
|
/// 内部验证配置文件的实现
|
||||||
/// 如果涉及端口和外部控制则需要重启
|
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
|
||||||
pub async fn update_config(&self) -> Result<()> {
|
println!("[core配置验证] 开始验证配置文件: {}", config_path);
|
||||||
log::debug!(target: "app", "try to update clash config");
|
|
||||||
// 更新订阅
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
Config::generate().await?;
|
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||||
|
println!("[core配置验证] 使用内核: {}", clash_core);
|
||||||
|
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let test_dir = dirs::app_home_dir()?.join("test");
|
||||||
|
let test_dir = dirs::path_to_str(&test_dir)?;
|
||||||
|
println!("[core配置验证] 测试目录: {}", test_dir);
|
||||||
|
|
||||||
// 检查订阅是否正常
|
// 使用子进程运行clash验证配置
|
||||||
self.check_config().await?;
|
println!("[core配置验证] 运行子进程验证配置");
|
||||||
|
let output = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar(clash_core)?
|
||||||
|
.args(["-t", "-d", test_dir, "-f", config_path])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
// 更新运行时订阅
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let path = Config::generate_file(ConfigType::Run)?;
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let path = dirs::path_to_str(&path)?;
|
|
||||||
|
// 检查进程退出状态和错误输出
|
||||||
|
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||||
|
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||||
|
|
||||||
|
println!("\n[core配置验证] -------- 验证结果 --------");
|
||||||
|
println!("[core配置验证] 进程退出状态: {:?}", output.status);
|
||||||
|
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
println!("[core配置验证] stderr输出:\n{}", stderr);
|
||||||
|
}
|
||||||
|
if !stdout.is_empty() {
|
||||||
|
println!("[core配置验证] stdout输出:\n{}", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
// 发送请求 发送5次
|
if has_error {
|
||||||
for i in 0..10 {
|
println!("[core配置验证] 发现错误,开始处理错误信息");
|
||||||
match clash_api::put_configs(path).await {
|
let error_msg = if !stdout.is_empty() {
|
||||||
Ok(_) => break,
|
stdout.to_string()
|
||||||
|
} else if !stderr.is_empty() {
|
||||||
|
stderr.to_string()
|
||||||
|
} else if let Some(code) = output.status.code() {
|
||||||
|
format!("验证进程异常退出,退出码: {}", code)
|
||||||
|
} else {
|
||||||
|
"验证进程被终止".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||||
|
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||||
|
} else {
|
||||||
|
println!("[core配置验证] 验证成功");
|
||||||
|
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||||
|
Ok((true, String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证运行时配置
|
||||||
|
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||||
|
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
|
self.validate_config_internal(config_path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证指定的配置文件
|
||||||
|
pub async fn validate_config_file(&self, config_path: &str, is_merge_file: Option<bool>) -> Result<(bool, String)> {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if !std::path::Path::new(config_path).exists() {
|
||||||
|
let error_msg = format!("File not found: {}", config_path);
|
||||||
|
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||||
|
return Ok((false, error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
|
||||||
|
if is_merge_file.unwrap_or(false) {
|
||||||
|
println!("[core配置验证] 检测到Merge文件,仅进行语法检查: {}", config_path);
|
||||||
|
return self.validate_file_syntax(config_path).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为脚本文件
|
||||||
|
let is_script = if config_path.ends_with(".js") {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
match self.is_script_file(config_path) {
|
||||||
|
Ok(result) => result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if i < 9 {
|
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||||
log::info!(target: "app", "{err}");
|
log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err);
|
||||||
} else {
|
return self.validate_config_internal(config_path).await;
|
||||||
bail!(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(100)).await;
|
};
|
||||||
|
|
||||||
|
if is_script {
|
||||||
|
log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path);
|
||||||
|
return self.validate_script_file(config_path).await;
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
// 对YAML配置文件使用Clash内核验证
|
||||||
|
log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path);
|
||||||
|
self.validate_config_internal(config_path).await
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// 检查文件是否为脚本文件
|
||||||
|
fn is_script_file(&self, path: &str) -> Result<bool> {
|
||||||
|
// 1. 先通过扩展名快速判断
|
||||||
|
if path.ends_with(".yaml") || path.ends_with(".yml") {
|
||||||
|
return Ok(false); // YAML文件不是脚本文件
|
||||||
|
} else if path.ends_with(".js") {
|
||||||
|
return Ok(true); // JS文件是脚本文件
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 读取文件内容
|
||||||
|
let content = match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err);
|
||||||
|
return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 检查是否存在明显的YAML特征
|
||||||
|
let has_yaml_features = content.contains(": ") ||
|
||||||
|
content.contains("#") ||
|
||||||
|
content.contains("---") ||
|
||||||
|
content.lines().any(|line| line.trim().starts_with("- "));
|
||||||
|
|
||||||
|
// 4. 检查是否存在明显的JS特征
|
||||||
|
let has_js_features = content.contains("function ") ||
|
||||||
|
content.contains("const ") ||
|
||||||
|
content.contains("let ") ||
|
||||||
|
content.contains("var ") ||
|
||||||
|
content.contains("//") ||
|
||||||
|
content.contains("/*") ||
|
||||||
|
content.contains("*/") ||
|
||||||
|
content.contains("export ") ||
|
||||||
|
content.contains("import ");
|
||||||
|
|
||||||
|
// 5. 决策逻辑
|
||||||
|
if has_yaml_features && !has_js_features {
|
||||||
|
// 只有YAML特征,没有JS特征
|
||||||
|
return Ok(false);
|
||||||
|
} else if has_js_features && !has_yaml_features {
|
||||||
|
// 只有JS特征,没有YAML特征
|
||||||
|
return Ok(true);
|
||||||
|
} else if has_yaml_features && has_js_features {
|
||||||
|
// 两种特征都有,需要更精细判断
|
||||||
|
// 优先检查是否有明确的JS结构特征
|
||||||
|
if content.contains("function main") ||
|
||||||
|
content.contains("module.exports") ||
|
||||||
|
content.contains("export default") {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查冒号后是否有空格(YAML的典型特征)
|
||||||
|
let yaml_pattern_count = content.lines()
|
||||||
|
.filter(|line| line.contains(": "))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if yaml_pattern_count > 2 {
|
||||||
|
return Ok(false); // 多个键值对格式,更可能是YAML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认情况:无法确定时,假设为非脚本文件(更安全)
|
||||||
|
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证脚本文件语法
|
||||||
|
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||||
|
// 读取脚本内容
|
||||||
|
let content = match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("Failed to read script file: {}", err);
|
||||||
|
log::warn!(target: "app", "脚本语法错误: {}", err);
|
||||||
|
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||||
|
return Ok((false, error_msg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(target: "app", "验证脚本文件: {}", path);
|
||||||
|
|
||||||
|
// 使用boa引擎进行基本语法检查
|
||||||
|
use boa_engine::{Context, Source};
|
||||||
|
|
||||||
|
let mut context = Context::default();
|
||||||
|
let result = context.eval(Source::from_bytes(&content));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
log::debug!(target: "app", "脚本语法验证通过: {}", path);
|
||||||
|
|
||||||
|
// 检查脚本是否包含main函数
|
||||||
|
if !content.contains("function main") && !content.contains("const main") && !content.contains("let main") {
|
||||||
|
let error_msg = "Script must contain a main function";
|
||||||
|
log::warn!(target: "app", "脚本缺少main函数: {}", path);
|
||||||
|
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||||
|
return Ok((false, error_msg.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((true, String::new()))
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("Script syntax error: {}", err);
|
||||||
|
log::warn!(target: "app", "脚本语法错误: {}", err);
|
||||||
|
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||||
|
Ok((false, error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新proxies等配置
|
||||||
|
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||||
|
println!("[core配置更新] 开始更新配置");
|
||||||
|
|
||||||
|
// 1. 先生成新的配置内容
|
||||||
|
println!("[core配置更新] 生成新的配置内容");
|
||||||
|
Config::generate().await?;
|
||||||
|
|
||||||
|
// 2. 生成临时文件并进行验证
|
||||||
|
println!("[core配置更新] 生成临时配置文件用于验证");
|
||||||
|
let temp_config = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let temp_config = dirs::path_to_str(&temp_config)?;
|
||||||
|
println!("[core配置更新] 临时配置文件路径: {}", temp_config);
|
||||||
|
|
||||||
|
// 3. 验证配置
|
||||||
|
match self.validate_config().await {
|
||||||
|
Ok((true, _)) => {
|
||||||
|
println!("[core配置更新] 配置验证通过");
|
||||||
|
// 4. 验证通过后,生成正式的运行时配置
|
||||||
|
println!("[core配置更新] 生成运行时配置");
|
||||||
|
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||||
|
let run_path = dirs::path_to_str(&run_path)?;
|
||||||
|
|
||||||
|
// 5. 应用新配置
|
||||||
|
println!("[core配置更新] 应用新配置");
|
||||||
|
for i in 0..3 {
|
||||||
|
match clash_api::put_configs(run_path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("[core配置更新] 配置应用成功");
|
||||||
|
Config::runtime().apply();
|
||||||
|
return Ok((true, String::new()));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if i < 2 {
|
||||||
|
println!("[core配置更新] 第{}次重试应用配置", i + 1);
|
||||||
|
log::info!(target: "app", "{err}");
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
} else {
|
||||||
|
println!("[core配置更新] 配置应用失败: {}", err);
|
||||||
|
Config::runtime().discard();
|
||||||
|
return Ok((false, err.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((true, String::new()))
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[core配置更新] 配置验证失败: {}", error_msg);
|
||||||
|
Config::runtime().discard();
|
||||||
|
Ok((false, error_msg))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[core配置更新] 验证过程发生错误: {}", e);
|
||||||
|
Config::runtime().discard();
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 只进行文件语法检查,不进行完整验证
|
||||||
|
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||||
|
println!("[core配置语法检查] 开始检查文件: {}", config_path);
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
let content = match std::fs::read_to_string(config_path) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("Failed to read file: {}", err);
|
||||||
|
println!("[core配置语法检查] 无法读取文件: {}", error_msg);
|
||||||
|
return Ok((false, error_msg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对YAML文件尝试解析,只检查语法正确性
|
||||||
|
println!("[core配置语法检查] 进行YAML语法检查");
|
||||||
|
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("[core配置语法检查] YAML语法检查通过");
|
||||||
|
Ok((true, String::new()))
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||||
|
let error_msg = format!("YAML syntax error: {}", err);
|
||||||
|
println!("[core配置语法检查] YAML语法错误: {}", error_msg);
|
||||||
|
Ok((false, error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
-10
@@ -1,11 +1,14 @@
|
|||||||
use crate::core::handle;
|
use crate::core::handle;
|
||||||
use crate::{config::Config, feat, log_err};
|
use crate::{config::Config, feat, log_err};
|
||||||
|
use crate::utils::resolve;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
|
use tauri::async_runtime;
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||||
}
|
}
|
||||||
@@ -21,8 +24,22 @@ impl Hotkey {
|
|||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
|
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
|
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||||
|
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||||
|
|
||||||
|
// 如果全局热键被禁用,则不注册热键
|
||||||
|
if !enable_global_hotkey {
|
||||||
|
println!("Global hotkey is disabled, skipping registration");
|
||||||
|
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||||
|
println!("Found {} hotkeys to register", hotkeys.len());
|
||||||
|
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
|
||||||
|
|
||||||
for hotkey in hotkeys.iter() {
|
for hotkey in hotkeys.iter() {
|
||||||
let mut iter = hotkey.split(',');
|
let mut iter = hotkey.split(',');
|
||||||
let func = iter.next();
|
let func = iter.next();
|
||||||
@@ -30,56 +47,151 @@ impl Hotkey {
|
|||||||
|
|
||||||
match (key, func) {
|
match (key, func) {
|
||||||
(Some(key), Some(func)) => {
|
(Some(key), Some(func)) => {
|
||||||
log_err!(self.register(key, func));
|
println!("Registering hotkey: {} -> {}", key, func);
|
||||||
|
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
|
||||||
|
if let Err(e) = self.register(key, func) {
|
||||||
|
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||||
|
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||||
|
} else {
|
||||||
|
println!("Successfully registered hotkey {} -> {}", key, func);
|
||||||
|
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let key = key.unwrap_or("None");
|
let key = key.unwrap_or("None");
|
||||||
let func = func.unwrap_or("None");
|
let func = func.unwrap_or("None");
|
||||||
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
println!("Invalid hotkey configuration: `{key}`:`{func}`");
|
||||||
|
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.lock().clone_from(hotkeys);
|
self.current.lock().clone_from(hotkeys);
|
||||||
|
} else {
|
||||||
|
println!("No hotkeys configured");
|
||||||
|
log::info!(target: "app", "No hotkeys configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(&self) -> Result<()> {
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let manager = app_handle.global_shortcut();
|
||||||
|
manager.unregister_all()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
|
|
||||||
|
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||||
|
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
if manager.is_registered(hotkey) {
|
||||||
|
println!("Hotkey {} was already registered, unregistering first", hotkey);
|
||||||
|
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
let f = match func.trim() {
|
||||||
"open_or_close_dashboard" => feat::open_or_close_dashboard,
|
"open_or_close_dashboard" => {
|
||||||
|
println!("Registering open_or_close_dashboard function");
|
||||||
|
log::info!(target: "app", "Registering open_or_close_dashboard function");
|
||||||
|
|| {
|
||||||
|
println!("=== Hotkey Dashboard Window Operation Start ===");
|
||||||
|
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
||||||
|
|
||||||
|
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||||
|
async_runtime::spawn_blocking(|| {
|
||||||
|
println!("Toggle dashboard window visibility");
|
||||||
|
log::info!(target: "app", "Toggle dashboard window visibility");
|
||||||
|
|
||||||
|
// 检查窗口是否存在
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
// 如果窗口可见,则隐藏它
|
||||||
|
if window.is_visible().unwrap_or(false) {
|
||||||
|
println!("Window is visible, hiding it");
|
||||||
|
log::info!(target: "app", "Window is visible, hiding it");
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
// 如果窗口不可见,则显示它
|
||||||
|
println!("Window is hidden, showing it");
|
||||||
|
log::info!(target: "app", "Window is hidden, showing it");
|
||||||
|
if window.is_minimized().unwrap_or(false) {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
}
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果窗口不存在,创建一个新窗口
|
||||||
|
println!("Window does not exist, creating a new one");
|
||||||
|
log::info!(target: "app", "Window does not exist, creating a new one");
|
||||||
|
resolve::create_window();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("=== Hotkey Dashboard Window Operation End ===");
|
||||||
|
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
||||||
|
}
|
||||||
|
},
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||||
"toggle_system_proxy" => feat::toggle_system_proxy,
|
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||||
"toggle_tun_mode" => feat::toggle_tun_mode,
|
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||||
"quit" => || feat::quit(Some(0)),
|
"quit" => || feat::quit(Some(0)),
|
||||||
|
|
||||||
_ => bail!("invalid function \"{func}\""),
|
_ => {
|
||||||
|
println!("Invalid function: {}", func);
|
||||||
|
log::error!(target: "app", "Invalid function: {}", func);
|
||||||
|
bail!("invalid function \"{func}\"");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let is_quit = func.trim() == "quit";
|
||||||
|
|
||||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
if hotkey.key == Code::KeyQ {
|
println!("Hotkey pressed: {:?}", hotkey);
|
||||||
|
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
|
||||||
|
|
||||||
|
if hotkey.key == Code::KeyQ && is_quit {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if window.is_focused().unwrap_or(false) {
|
if window.is_focused().unwrap_or(false) {
|
||||||
|
println!("Executing quit function");
|
||||||
|
log::info!(target: "app", "Executing quit function");
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f();
|
// 直接执行函数,不做任何状态检查
|
||||||
|
println!("Executing function directly");
|
||||||
|
log::info!(target: "app", "Executing function directly");
|
||||||
|
|
||||||
|
// 获取轻量模式状态和全局热键状态
|
||||||
|
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
|
||||||
|
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
|
// 在轻量模式下或配置了全局热键时,始终执行热键功能
|
||||||
|
if is_lite_mode || is_enable_global_hotkey {
|
||||||
|
f();
|
||||||
|
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||||
|
let is_visible = window.is_visible().unwrap_or(false);
|
||||||
|
let is_focused = window.is_focused().unwrap_or(false);
|
||||||
|
|
||||||
|
if is_focused && is_visible {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log::debug!(target: "app", "register hotkey {hotkey} {func}");
|
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
||||||
|
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +199,6 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
|
|
||||||
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use tokio::time::Duration;
|
|||||||
// Windows only
|
// Windows only
|
||||||
|
|
||||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||||
|
const REQUIRED_SERVICE_VERSION: &str = "1.0.1"; // 定义所需的服务版本号
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct ResponseBody {
|
pub struct ResponseBody {
|
||||||
@@ -19,6 +20,12 @@ pub struct ResponseBody {
|
|||||||
pub log_file: String,
|
pub log_file: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct VersionResponse {
|
||||||
|
pub service: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct JsonResponse {
|
pub struct JsonResponse {
|
||||||
pub code: u64,
|
pub code: u64,
|
||||||
@@ -26,6 +33,13 @@ pub struct JsonResponse {
|
|||||||
pub data: Option<ResponseBody>,
|
pub data: Option<ResponseBody>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct VersionJsonResponse {
|
||||||
|
pub code: u64,
|
||||||
|
pub msg: String,
|
||||||
|
pub data: Option<VersionResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn reinstall_service() -> Result<()> {
|
pub async fn reinstall_service() -> Result<()> {
|
||||||
log::info!(target:"app", "reinstall service");
|
log::info!(target:"app", "reinstall service");
|
||||||
@@ -177,8 +191,43 @@ pub async fn check_service() -> Result<JsonResponse> {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// check the service version
|
||||||
|
pub async fn check_service_version() -> Result<String> {
|
||||||
|
let url = format!("{SERVICE_URL}/version");
|
||||||
|
let response = reqwest::ClientBuilder::new()
|
||||||
|
.no_proxy()
|
||||||
|
.timeout(Duration::from_secs(3))
|
||||||
|
.build()?
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to the Clash Verge Service")?
|
||||||
|
.json::<VersionJsonResponse>()
|
||||||
|
.await
|
||||||
|
.context("failed to parse the Clash Verge Service version response")?;
|
||||||
|
|
||||||
|
match response.data {
|
||||||
|
Some(data) => Ok(data.version),
|
||||||
|
None => bail!("service version not found in response"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check if service needs to be reinstalled
|
||||||
|
pub async fn check_service_needs_reinstall() -> bool {
|
||||||
|
match check_service_version().await {
|
||||||
|
Ok(version) => version != REQUIRED_SERVICE_VERSION,
|
||||||
|
Err(_) => true, // 如果无法获取版本或服务未运行,也需要重新安装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// start the clash by service
|
/// start the clash by service
|
||||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||||
|
// 检查服务版本,如果不匹配则重新安装
|
||||||
|
if check_service_needs_reinstall().await {
|
||||||
|
log::info!(target: "app", "service version mismatch, reinstalling");
|
||||||
|
reinstall_service().await?;
|
||||||
|
}
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::feat;
|
use crate::feat;
|
||||||
|
use crate::core::CoreManager;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -172,7 +173,17 @@ impl Timer {
|
|||||||
/// the task runner
|
/// the task runner
|
||||||
async fn async_task(uid: String) {
|
async fn async_task(uid: String) {
|
||||||
log::info!(target: "app", "running timer task `{uid}`");
|
log::info!(target: "app", "running timer task `{uid}`");
|
||||||
crate::log_err!(feat::update_profile(uid, None).await);
|
|
||||||
|
// 使用更轻量级的更新方式
|
||||||
|
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
||||||
|
log::error!(target: "app", "timer task update error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有更新成功后才刷新配置
|
||||||
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
|
log::error!(target: "app", "timer task refresh error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use parking_lot::RwLock;
|
|||||||
pub use speed_rate::{SpeedRate, Traffic};
|
pub use speed_rate::{SpeedRate, Traffic};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::menu::CheckMenuItem;
|
use tauri::menu::{CheckMenuItem, IsMenuItem};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
@@ -71,20 +71,23 @@ impl Tray {
|
|||||||
let tray_incon_id = TrayIconId::new("main");
|
let tray_incon_id = TrayIconId::new("main");
|
||||||
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
tray.set_show_menu_on_left_click(false)?;
|
||||||
|
|
||||||
tray.on_tray_icon_event(|_, event| {
|
tray.on_tray_icon_event(|_, event| {
|
||||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let TrayIconEvent::Click {
|
if let TrayIconEvent::Click {
|
||||||
button: MouseButton::Right,
|
button: MouseButton::Left,
|
||||||
button_state: MouseButtonState::Down,
|
button_state: MouseButtonState::Down,
|
||||||
..
|
..
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
match tray_event.as_str() {
|
match tray_event.as_str() {
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
"main_window" => resolve::create_window(),
|
"main_window" => resolve::create_window(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,7 @@ impl Tray {
|
|||||||
{
|
{
|
||||||
match tray_event.as_str() {
|
match tray_event.as_str() {
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
"main_window" => resolve::create_window(),
|
"main_window" => resolve::create_window(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -124,6 +127,10 @@ impl Tray {
|
|||||||
.unwrap_or("rule")
|
.unwrap_or("rule")
|
||||||
.to_owned()
|
.to_owned()
|
||||||
};
|
};
|
||||||
|
let profile_uid_and_name = Config::profiles()
|
||||||
|
.data()
|
||||||
|
.all_profile_uid_and_name()
|
||||||
|
.unwrap_or(Vec::new());
|
||||||
|
|
||||||
let tray = app_handle.tray_by_id("main").unwrap();
|
let tray = app_handle.tray_by_id("main").unwrap();
|
||||||
let _ = tray.set_menu(Some(create_tray_menu(
|
let _ = tray.set_menu(Some(create_tray_menu(
|
||||||
@@ -131,6 +138,7 @@ impl Tray {
|
|||||||
Some(mode.as_str()),
|
Some(mode.as_str()),
|
||||||
*system_proxy,
|
*system_proxy,
|
||||||
*tun_mode,
|
*tun_mode,
|
||||||
|
profile_uid_and_name,
|
||||||
)?));
|
)?));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -217,23 +225,27 @@ impl Tray {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||||
let is_template =
|
let is_colorful = tray_icon == "colorful";
|
||||||
crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
|
||||||
|
// 处理图标和速率
|
||||||
let icon_bytes = if enable_tray_speed {
|
let final_icon_bytes = if enable_tray_speed {
|
||||||
let rate = rate.or_else(|| {
|
let rate = rate.or_else(|| {
|
||||||
self.speed_rate
|
self.speed_rate
|
||||||
.lock()
|
.lock()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 使用新的方法渲染图标和速率
|
||||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||||
} else {
|
} else {
|
||||||
icon_bytes
|
icon_bytes
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
// 设置系统托盘图标
|
||||||
let _ = tray.set_icon_as_template(is_template);
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||||
|
// 只对单色图标使用 template 模式
|
||||||
|
let _ = tray.set_icon_as_template(!is_colorful);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -363,6 +375,7 @@ fn create_tray_menu(
|
|||||||
mode: Option<&str>,
|
mode: Option<&str>,
|
||||||
system_proxy_enabled: bool,
|
system_proxy_enabled: bool,
|
||||||
tun_mode_enabled: bool,
|
tun_mode_enabled: bool,
|
||||||
|
profile_uid_and_name: Vec<(String, String)>,
|
||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
) -> Result<tauri::menu::Menu<Wry>> {
|
||||||
let mode = mode.unwrap_or("");
|
let mode = mode.unwrap_or("");
|
||||||
let version = VERSION.get().unwrap();
|
let version = VERSION.get().unwrap();
|
||||||
@@ -382,6 +395,25 @@ fn create_tray_menu(
|
|||||||
.collect::<std::collections::HashMap<String, String>>()
|
.collect::<std::collections::HashMap<String, String>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||||
|
.iter()
|
||||||
|
.map(|(profile_uid, profile_name)| {
|
||||||
|
let is_current_profile = Config::profiles().data().is_current_profile_index(profile_uid.to_string());
|
||||||
|
CheckMenuItem::with_id(
|
||||||
|
app_handle,
|
||||||
|
&format!("profiles_{}", profile_uid),
|
||||||
|
t(&profile_name),
|
||||||
|
true,
|
||||||
|
is_current_profile,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}).collect();
|
||||||
|
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let open_window = &MenuItem::with_id(
|
let open_window = &MenuItem::with_id(
|
||||||
app_handle,
|
app_handle,
|
||||||
@@ -422,6 +454,14 @@ fn create_tray_menu(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let profiles = &Submenu::with_id_and_items(
|
||||||
|
app_handle,
|
||||||
|
"profiles",
|
||||||
|
t("Profiles"),
|
||||||
|
true,
|
||||||
|
&profile_menu_items,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
let system_proxy = &CheckMenuItem::with_id(
|
let system_proxy = &CheckMenuItem::with_id(
|
||||||
app_handle,
|
app_handle,
|
||||||
"system_proxy",
|
"system_proxy",
|
||||||
@@ -530,6 +570,8 @@ fn create_tray_menu(
|
|||||||
global_mode,
|
global_mode,
|
||||||
direct_mode,
|
direct_mode,
|
||||||
separator,
|
separator,
|
||||||
|
profiles,
|
||||||
|
separator,
|
||||||
system_proxy,
|
system_proxy,
|
||||||
tun_mode,
|
tun_mode,
|
||||||
copy_env,
|
copy_env,
|
||||||
@@ -552,7 +594,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
}
|
}
|
||||||
"open_window" => resolve::create_window(),
|
"open_window" => resolve::create_window(),
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
"copy_env" => feat::copy_clash_env(),
|
"copy_env" => feat::copy_clash_env(),
|
||||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||||
@@ -562,6 +604,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
"quit" => {
|
"quit" => {
|
||||||
println!("quit");
|
println!("quit");
|
||||||
feat::quit(Some(0));
|
feat::quit(Some(0));
|
||||||
|
},
|
||||||
|
id if id.starts_with("profiles_") => {
|
||||||
|
let profile_index = &id["profiles_".len()..];
|
||||||
|
feat::toggle_proxy_profile(profile_index.into());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::core::clash_api::{get_traffic_ws_url, Rate};
|
|||||||
use crate::utils::help::format_bytes_speed;
|
use crate::utils::help::format_bytes_speed;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use image::{ImageBuffer, Rgba};
|
use image::{Rgba, GenericImageView, RgbaImage};
|
||||||
use imageproc::drawing::draw_text_mut;
|
use imageproc::drawing::draw_text_mut;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rusttype::{Font, Scale};
|
use rusttype::{Font, Scale};
|
||||||
@@ -13,22 +13,45 @@ use tokio_tungstenite::tungstenite::Message;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SpeedRate {
|
pub struct SpeedRate {
|
||||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||||
|
last_update: Arc<Mutex<std::time::Instant>>,
|
||||||
|
// 移除 base_image,不再缓存原始图像
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpeedRate {
|
impl SpeedRate {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
||||||
|
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
||||||
let mut rates = self.rate.lock();
|
let mut rates = self.rate.lock();
|
||||||
|
let mut last_update = self.last_update.lock();
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
// 限制更新频率为每秒最多2次(500ms)
|
||||||
|
if now.duration_since(*last_update).as_millis() < 500 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let (current, previous) = &mut *rates;
|
let (current, previous) = &mut *rates;
|
||||||
|
|
||||||
|
// 如果速率变化不大(小于10%),则不更新
|
||||||
|
let should_update = {
|
||||||
|
let up_change = (current.up as f64 - up as f64).abs() / (current.up as f64 + 1.0);
|
||||||
|
let down_change = (current.down as f64 - down as f64).abs() / (current.down as f64 + 1.0);
|
||||||
|
up_change > 0.1 || down_change > 0.1
|
||||||
|
};
|
||||||
|
|
||||||
|
if !should_update {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
*previous = current.clone();
|
*previous = current.clone();
|
||||||
current.up = up;
|
current.up = up;
|
||||||
current.down = down;
|
current.down = down;
|
||||||
|
*last_update = now;
|
||||||
|
|
||||||
if previous != current {
|
if previous != current {
|
||||||
Some(current.clone())
|
Some(current.clone())
|
||||||
@@ -43,92 +66,114 @@ impl SpeedRate {
|
|||||||
Some(current.clone())
|
Some(current.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_speed_text(icon: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
// 分离图标加载和速率渲染
|
||||||
|
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||||
let img = image::load_from_memory(&icon)?;
|
|
||||||
let (width, height) = (img.width(), img.height());
|
// 加载原始图标
|
||||||
|
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||||
let mut image = ImageBuffer::new((width as f32 * 4.0) as u32, height);
|
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||||
image::imageops::replace(&mut image, &img, 0, 0);
|
|
||||||
|
// 判断是否为彩色图标
|
||||||
let font =
|
let is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||||
Font::try_from_bytes(include_bytes!("../../../assets/fonts/SFCompact.ttf")).unwrap();
|
|
||||||
|
// 增加文本宽度和间距
|
||||||
// 修改颜色和阴影参数
|
let text_width = 580; // 文本区域宽度
|
||||||
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); // 纯白色
|
let total_width = icon_width + text_width;
|
||||||
let shadow_color = Rgba([0u8, 0u8, 0u8, 180u8]); // 半透明黑色阴影
|
|
||||||
let base_size = height as f32 * 0.5;
|
// 创建新的透明画布
|
||||||
let scale = Scale::uniform(base_size);
|
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||||
|
|
||||||
|
// 将原始图标绘制到新画布的左侧
|
||||||
|
for y in 0..icon_height {
|
||||||
|
for x in 0..icon_width {
|
||||||
|
let pixel = icon_image.get_pixel(x, y);
|
||||||
|
combined_image.put_pixel(x, y, pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择文本颜色
|
||||||
|
let (text_color, shadow_color) = if is_colorful {
|
||||||
|
// 彩色图标使用黑色文本和轻微白色阴影
|
||||||
|
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
|
||||||
|
} else {
|
||||||
|
// 单色图标使用白色文本和轻微黑色阴影
|
||||||
|
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 120u8]))
|
||||||
|
};
|
||||||
|
|
||||||
|
// 减小字体大小以适应文本区域
|
||||||
|
let font = Font::try_from_bytes(include_bytes!("../../../assets/fonts/SF-Pro.ttf")).unwrap();
|
||||||
|
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||||
|
let scale = Scale::uniform(font_size);
|
||||||
|
|
||||||
|
// 使用更简洁的速率格式
|
||||||
let up_text = format_bytes_speed(rate.up);
|
let up_text = format_bytes_speed(rate.up);
|
||||||
let down_text = format_bytes_speed(rate.down);
|
let down_text = format_bytes_speed(rate.down);
|
||||||
|
|
||||||
// 计算文本位置(保持不变)
|
// 计算文本位置,确保垂直间距合适
|
||||||
let up_width = font
|
// 修改文本位置为居右显示
|
||||||
.layout(&up_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
|
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||||
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||||
.last()
|
|
||||||
.unwrap_or(0.0);
|
// 计算右对齐的文本位置
|
||||||
|
let up_text_x = total_width - up_text_width;
|
||||||
let down_width = font
|
let down_text_x = total_width - down_text_width;
|
||||||
.layout(&down_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
|
|
||||||
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||||
.last()
|
let text_height = font_size as i32;
|
||||||
.unwrap_or(0.0);
|
let total_text_height = text_height * 2;
|
||||||
|
let up_y = (icon_height as i32 - total_text_height) / 2;
|
||||||
let right_margin = 8;
|
let down_y = up_y + text_height;
|
||||||
let canvas_width = width * 4;
|
|
||||||
let up_x = canvas_width as f32 - up_width - right_margin as f32;
|
// 绘制速率文本(先阴影后文字)
|
||||||
let down_x = canvas_width as f32 - down_width - right_margin as f32;
|
let shadow_offset = 1;
|
||||||
|
|
||||||
// 添加阴影效果
|
// 绘制上行速率
|
||||||
let shadow_offset = 1; // 阴影偏移量
|
|
||||||
|
|
||||||
// 绘制上行速率(先画阴影,再画文字)
|
|
||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut combined_image,
|
||||||
shadow_color,
|
shadow_color,
|
||||||
up_x as i32 + shadow_offset,
|
up_text_x as i32 + shadow_offset,
|
||||||
1 + shadow_offset,
|
up_y + shadow_offset,
|
||||||
scale,
|
scale,
|
||||||
&font,
|
&font,
|
||||||
&up_text,
|
&up_text,
|
||||||
);
|
);
|
||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut combined_image,
|
||||||
text_color,
|
text_color,
|
||||||
up_x as i32,
|
up_text_x as i32,
|
||||||
1,
|
up_y,
|
||||||
scale,
|
scale,
|
||||||
&font,
|
&font,
|
||||||
&up_text,
|
&up_text,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 绘制下行速率(先画阴影,再画文字)
|
// 绘制下行速率
|
||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut combined_image,
|
||||||
shadow_color,
|
shadow_color,
|
||||||
down_x as i32 + shadow_offset,
|
down_text_x as i32 + shadow_offset,
|
||||||
height as i32 - (base_size as i32) - 1 + shadow_offset,
|
down_y + shadow_offset,
|
||||||
scale,
|
scale,
|
||||||
&font,
|
&font,
|
||||||
&down_text,
|
&down_text,
|
||||||
);
|
);
|
||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut combined_image,
|
||||||
text_color,
|
text_color,
|
||||||
down_x as i32,
|
down_text_x as i32,
|
||||||
height as i32 - (base_size as i32) - 1,
|
down_y,
|
||||||
scale,
|
scale,
|
||||||
&font,
|
&font,
|
||||||
&down_text,
|
&down_text,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut bytes: Vec<u8> = Vec::new();
|
// 将结果转换为 PNG 数据
|
||||||
let mut cursor = Cursor::new(&mut bytes);
|
let mut bytes = Vec::new();
|
||||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
+125
-42
@@ -1,55 +1,138 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{Mapping, Sequence, Value};
|
use serde_yaml::{Mapping, Sequence, Value};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct SeqMap {
|
pub struct SeqMap {
|
||||||
prepend: Sequence,
|
pub prepend: Sequence,
|
||||||
append: Sequence,
|
pub append: Sequence,
|
||||||
delete: Sequence,
|
pub delete: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
|
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||||
let mut prepend = seq_map.prepend;
|
let SeqMap {
|
||||||
let append = seq_map.append;
|
prepend,
|
||||||
let delete = seq_map.delete;
|
append,
|
||||||
|
delete,
|
||||||
|
} = seq;
|
||||||
|
|
||||||
let origin_seq = config.get(name).map_or(Sequence::default(), |val| {
|
let mut new_seq = Sequence::new();
|
||||||
val.as_sequence().unwrap_or(&Sequence::default()).clone()
|
new_seq.extend(prepend);
|
||||||
});
|
|
||||||
let mut seq = origin_seq.clone();
|
|
||||||
|
|
||||||
let mut delete_names = Vec::new();
|
if let Some(Value::Sequence(origin)) = config.get(field) {
|
||||||
for item in delete {
|
// Filter out deleted items
|
||||||
let item = item.clone();
|
let filtered: Sequence = origin
|
||||||
if let Some(name) = if item.is_string() {
|
.iter()
|
||||||
Some(item)
|
.filter(|item| {
|
||||||
} else {
|
if let Value::String(s) = item {
|
||||||
item.get("name").cloned()
|
!delete.contains(s)
|
||||||
} {
|
} else if let Value::Mapping(m) = item {
|
||||||
delete_names.push(name.clone());
|
if let Some(Value::String(name)) = m.get("name") {
|
||||||
|
!delete.contains(name)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
new_seq.extend(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_seq.extend(append);
|
||||||
|
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
|
||||||
|
|
||||||
|
// If this is proxies field, we also need to filter proxy-groups
|
||||||
|
if field == "proxies" {
|
||||||
|
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
|
||||||
|
let mut new_groups = Sequence::new();
|
||||||
|
for group in groups {
|
||||||
|
if let Value::Mapping(group_map) = group {
|
||||||
|
let mut new_group = group_map.clone();
|
||||||
|
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
|
||||||
|
let filtered_proxies: Sequence = proxies
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
if let Value::String(name) = p {
|
||||||
|
!delete.contains(name)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
new_group.insert(
|
||||||
|
Value::String("proxies".into()),
|
||||||
|
Value::Sequence(filtered_proxies),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
new_groups.push(Value::Mapping(new_group));
|
||||||
|
} else {
|
||||||
|
new_groups.push(group.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.insert(
|
||||||
|
Value::String("proxy-groups".into()),
|
||||||
|
Value::Sequence(new_groups),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seq.retain(|x| {
|
|
||||||
if let Some(x_name) = if x.is_string() {
|
|
||||||
Some(x)
|
|
||||||
} else {
|
|
||||||
x.get("name")
|
|
||||||
} {
|
|
||||||
!delete_names.contains(x_name)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
prepend.reverse();
|
|
||||||
for item in prepend {
|
|
||||||
seq.insert(0, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in append {
|
|
||||||
seq.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut config = config.clone();
|
|
||||||
config.insert(Value::from(name), Value::from(seq));
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_yaml::Value;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_proxy_and_references() {
|
||||||
|
let config_str = r#"
|
||||||
|
proxies:
|
||||||
|
- name: "proxy1"
|
||||||
|
type: "ss"
|
||||||
|
- name: "proxy2"
|
||||||
|
type: "vmess"
|
||||||
|
proxy-groups:
|
||||||
|
- name: "group1"
|
||||||
|
type: "select"
|
||||||
|
proxies:
|
||||||
|
- "proxy1"
|
||||||
|
- "proxy2"
|
||||||
|
- name: "group2"
|
||||||
|
type: "select"
|
||||||
|
proxies:
|
||||||
|
- "proxy1"
|
||||||
|
"#;
|
||||||
|
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||||
|
|
||||||
|
let seq = SeqMap {
|
||||||
|
prepend: Sequence::new(),
|
||||||
|
append: Sequence::new(),
|
||||||
|
delete: vec!["proxy1".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
config = use_seq(seq, config, "proxies");
|
||||||
|
|
||||||
|
// Check if proxy1 is removed from proxies
|
||||||
|
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
|
assert_eq!(proxies.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
proxies[0].as_mapping().unwrap().get("name").unwrap().as_str().unwrap(),
|
||||||
|
"proxy2"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if proxy1 is removed from all groups
|
||||||
|
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||||
|
let group1_proxies = groups[0].as_mapping().unwrap()
|
||||||
|
.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
|
let group2_proxies = groups[1].as_mapping().unwrap()
|
||||||
|
.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(group1_proxies.len(), 1);
|
||||||
|
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||||
|
assert_eq!(group2_proxies.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,14 +36,30 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if enable {
|
if enable {
|
||||||
revise!(dns_val, "enable", true);
|
// 检查现有的 enhanced-mode 设置
|
||||||
revise!(dns_val, "ipv6", ipv6_val);
|
let current_mode = dns_val
|
||||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
.get(&Value::from("enhanced-mode"))
|
||||||
revise!(dns_val, "fake-ip-range", "172.29.0.1/16");
|
.and_then(|v| v.as_str())
|
||||||
#[cfg(target_os = "macos")]
|
.unwrap_or("fake-ip");
|
||||||
{
|
|
||||||
crate::utils::resolve::restore_public_dns().await;
|
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
||||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
if current_mode == "fake-ip" || !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||||
|
revise!(dns_val, "enable", true);
|
||||||
|
revise!(dns_val, "ipv6", ipv6_val);
|
||||||
|
|
||||||
|
if !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||||
|
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dns_val.contains_key(&Value::from("fake-ip-range")) {
|
||||||
|
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
crate::utils::resolve::restore_public_dns().await;
|
||||||
|
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
revise!(
|
revise!(
|
||||||
@@ -72,7 +88,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
dns_val
|
dns_val
|
||||||
.get("fake-ip-range")
|
.get("fake-ip-range")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("172.29.0.1/16")
|
.unwrap_or("198.18.0.1/16")
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
+136
-19
@@ -4,6 +4,7 @@
|
|||||||
//! - timer 定时器
|
//! - timer 定时器
|
||||||
//! - cmds 页面调用
|
//! - cmds 页面调用
|
||||||
//!
|
//!
|
||||||
|
use crate::cmds;
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::core::*;
|
use crate::core::*;
|
||||||
use crate::log_err;
|
use crate::log_err;
|
||||||
@@ -16,18 +17,46 @@ use std::fs;
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
// 打开面板
|
// 打开面板
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn open_or_close_dashboard() {
|
pub fn open_or_close_dashboard() {
|
||||||
|
println!("Attempting to open/close dashboard");
|
||||||
|
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
println!("Found existing window");
|
||||||
|
log::info!(target: "app", "Found existing window");
|
||||||
|
|
||||||
// 如果窗口存在,则切换其显示状态
|
// 如果窗口存在,则切换其显示状态
|
||||||
if window.is_visible().unwrap_or(false) {
|
match window.is_visible() {
|
||||||
let _ = window.hide();
|
Ok(visible) => {
|
||||||
} else {
|
println!("Window visibility status: {}", visible);
|
||||||
let _ = window.show();
|
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||||
let _ = window.set_focus();
|
|
||||||
|
if visible {
|
||||||
|
println!("Attempting to hide window");
|
||||||
|
log::info!(target: "app", "Attempting to hide window");
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
println!("Attempting to show and focus window");
|
||||||
|
log::info!(target: "app", "Attempting to show and focus window");
|
||||||
|
if window.is_minimized().unwrap_or(false) {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
}
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get window visibility: {:?}", e);
|
||||||
|
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
println!("No existing window found, creating new window");
|
||||||
|
log::info!(target: "app", "No existing window found, creating new window");
|
||||||
resolve::create_window();
|
resolve::create_window();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,16 +85,44 @@ pub fn restart_app() {
|
|||||||
resolve::resolve_reset();
|
resolve::resolve_reset();
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
let _ = app_handle.save_window_state(StateFlags::default());
|
|
||||||
tauri::process::restart(&app_handle.env());
|
tauri::process::restart(&app_handle.env());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置窗口状态监控,实时保存窗口位置和大小
|
||||||
|
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
|
||||||
|
let window = app_handle.get_webview_window("main").unwrap();
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
|
||||||
|
// 监听窗口移动事件
|
||||||
|
let app_handle_move = app_handle_clone.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
match event {
|
||||||
|
// 窗口移动时保存状态
|
||||||
|
tauri::WindowEvent::Moved(_) => {
|
||||||
|
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||||
|
},
|
||||||
|
// 窗口调整大小时保存状态
|
||||||
|
tauri::WindowEvent::Resized(_) => {
|
||||||
|
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||||
|
},
|
||||||
|
// 其他可能改变窗口状态的事件
|
||||||
|
tauri::WindowEvent::ScaleFactorChanged { .. } => {
|
||||||
|
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||||
|
},
|
||||||
|
// 窗口关闭时保存
|
||||||
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
|
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 切换模式 rule/global/direct/script mode
|
// 切换模式 rule/global/direct/script mode
|
||||||
pub fn change_clash_mode(mode: String) {
|
pub fn change_clash_mode(mode: String) {
|
||||||
let mut mapping = Mapping::new();
|
let mut mapping = Mapping::new();
|
||||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
|
|
||||||
@@ -94,7 +151,7 @@ pub fn toggle_system_proxy() {
|
|||||||
match patch_verge(IVerge {
|
match patch_verge(IVerge {
|
||||||
enable_system_proxy: Some(!enable),
|
enable_system_proxy: Some(!enable),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
})
|
}, false)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
@@ -103,8 +160,23 @@ pub fn toggle_system_proxy() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换代理文件
|
||||||
|
pub fn toggle_proxy_profile(profile_index: String) {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
match cmds::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = tray::Tray::global().update_menu();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 切换tun模式
|
// 切换tun模式
|
||||||
pub fn toggle_tun_mode() {
|
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||||
let enable = Config::verge().data().enable_tun_mode;
|
let enable = Config::verge().data().enable_tun_mode;
|
||||||
let enable = enable.unwrap_or(false);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
@@ -112,7 +184,7 @@ pub fn toggle_tun_mode() {
|
|||||||
match patch_verge(IVerge {
|
match patch_verge(IVerge {
|
||||||
enable_tun_mode: Some(!enable),
|
enable_tun_mode: Some(!enable),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
})
|
}, not_save_file.unwrap_or(false))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
@@ -124,6 +196,7 @@ pub fn toggle_tun_mode() {
|
|||||||
pub fn quit(code: Option<i32>) {
|
pub fn quit(code: Option<i32>) {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
handle::Handle::global().set_is_exiting();
|
handle::Handle::global().set_is_exiting();
|
||||||
|
toggle_tun_mode(Some(true));
|
||||||
resolve::resolve_reset();
|
resolve::resolve_reset();
|
||||||
log_err!(handle::Handle::global().get_window().unwrap().close());
|
log_err!(handle::Handle::global().get_window().unwrap().close());
|
||||||
app_handle.exit(code.unwrap_or(0));
|
app_handle.exit(code.unwrap_or(0));
|
||||||
@@ -164,7 +237,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
|
|
||||||
/// 修改verge的订阅
|
/// 修改verge的订阅
|
||||||
/// 一般都是一个个的修改
|
/// 一般都是一个个的修改
|
||||||
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||||
Config::verge().draft().patch_config(patch.clone());
|
Config::verge().draft().patch_config(patch.clone());
|
||||||
|
|
||||||
let tun_mode = patch.enable_tun_mode;
|
let tun_mode = patch.enable_tun_mode;
|
||||||
@@ -175,6 +248,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
let proxy_bypass = patch.system_proxy_bypass;
|
let proxy_bypass = patch.system_proxy_bypass;
|
||||||
let language = patch.language;
|
let language = patch.language;
|
||||||
let mixed_port = patch.verge_mixed_port;
|
let mixed_port = patch.verge_mixed_port;
|
||||||
|
let lite_mode = patch.enable_lite_mode;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let tray_icon = patch.tray_icon;
|
let tray_icon = patch.tray_icon;
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -195,10 +269,12 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
let http_enabled = patch.verge_http_enabled;
|
let http_enabled = patch.verge_http_enabled;
|
||||||
let http_port = patch.verge_port;
|
let http_port = patch.verge_port;
|
||||||
let enable_tray_speed = patch.enable_tray_speed;
|
let enable_tray_speed = patch.enable_tray_speed;
|
||||||
|
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||||
|
|
||||||
let res: std::result::Result<(), anyhow::Error> = {
|
let res: std::result::Result<(), anyhow::Error> = {
|
||||||
let mut should_restart_core = false;
|
let mut should_restart_core = false;
|
||||||
let mut should_update_clash_config = false;
|
let mut should_update_clash_config = false;
|
||||||
|
let mut should_update_verge_config = false;
|
||||||
let mut should_update_launch = false;
|
let mut should_update_launch = false;
|
||||||
let mut should_update_sysproxy = false;
|
let mut should_update_sysproxy = false;
|
||||||
let mut should_update_systray_icon = false;
|
let mut should_update_systray_icon = false;
|
||||||
@@ -212,12 +288,13 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
should_update_systray_tooltip = true;
|
should_update_systray_tooltip = true;
|
||||||
should_update_systray_icon = true;
|
should_update_systray_icon = true;
|
||||||
}
|
}
|
||||||
|
if enable_global_hotkey.is_some() {
|
||||||
|
should_update_verge_config = true;
|
||||||
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
if redir_enabled.is_some() || redir_port.is_some() {
|
if redir_enabled.is_some() || redir_port.is_some() {
|
||||||
should_restart_core = true;
|
should_restart_core = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||||
should_restart_core = true;
|
should_restart_core = true;
|
||||||
@@ -272,6 +349,10 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
CoreManager::global().update_config().await?;
|
CoreManager::global().update_config().await?;
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
|
if should_update_verge_config {
|
||||||
|
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||||
|
handle::Handle::refresh_verge();
|
||||||
|
}
|
||||||
if should_update_launch {
|
if should_update_launch {
|
||||||
sysopt::Sysopt::global().update_launch()?;
|
sysopt::Sysopt::global().update_launch()?;
|
||||||
}
|
}
|
||||||
@@ -295,12 +376,31 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
if should_update_systray_tooltip {
|
if should_update_systray_tooltip {
|
||||||
tray::Tray::global().update_tooltip()?;
|
tray::Tray::global().update_tooltip()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理轻量模式切换
|
||||||
|
if lite_mode.is_some() {
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
if lite_mode.unwrap() {
|
||||||
|
// 完全退出 webview 进程
|
||||||
|
window.close()?; // 先关闭窗口
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
if let Some(webview) = app_handle.get_webview_window("main") {
|
||||||
|
webview.destroy()?; // 销毁 webview 进程
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve::create_window(); // 重新创建窗口
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<Result<()>>::Ok(())
|
<Result<()>>::Ok(())
|
||||||
};
|
};
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
Config::verge().apply();
|
Config::verge().apply();
|
||||||
Config::verge().data().save_file()?;
|
if !not_save_file {
|
||||||
|
Config::verge().data().save_file()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -314,6 +414,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
/// 更新某个profile
|
/// 更新某个profile
|
||||||
/// 如果更新当前订阅就激活订阅
|
/// 如果更新当前订阅就激活订阅
|
||||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||||
|
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||||
|
|
||||||
let url_opt = {
|
let url_opt = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
let profiles = profiles.latest();
|
||||||
@@ -321,33 +423,44 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||||
|
|
||||||
if !is_remote {
|
if !is_remote {
|
||||||
None // 直接更新
|
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||||
|
None // 非远程订阅直接更新
|
||||||
} else if item.url.is_none() {
|
} else if item.url.is_none() {
|
||||||
|
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||||
bail!("failed to get the profile item url");
|
bail!("failed to get the profile item url");
|
||||||
} else {
|
} else {
|
||||||
|
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
|
||||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let should_update = match url_opt {
|
let should_update = match url_opt {
|
||||||
Some((url, opt)) => {
|
Some((url, opt)) => {
|
||||||
|
println!("[订阅更新] 开始下载新的订阅内容");
|
||||||
let merged_opt = PrfOption::merge(opt, option);
|
let merged_opt = PrfOption::merge(opt, option);
|
||||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||||
|
|
||||||
|
println!("[订阅更新] 更新订阅配置");
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let mut profiles = profiles.latest();
|
let mut profiles = profiles.latest();
|
||||||
profiles.update_item(uid.clone(), item)?;
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
Some(uid) == profiles.get_current()
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
|
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
|
is_current
|
||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
|
println!("[订阅更新] 更新内核配置");
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
println!("[订阅更新] 更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
println!("[订阅更新] 更新失败: {}", err);
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
}
|
}
|
||||||
@@ -359,10 +472,13 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
|
|
||||||
/// copy env variable
|
/// copy env variable
|
||||||
pub fn copy_clash_env() {
|
pub fn copy_clash_env() {
|
||||||
|
// 从环境变量获取IP地址,默认127.0.0.1
|
||||||
|
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||||
let http_proxy = format!("http://127.0.0.1:{}", port);
|
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
||||||
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
||||||
|
|
||||||
let sh =
|
let sh =
|
||||||
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||||
@@ -504,7 +620,8 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
|||||||
webdav_username,
|
webdav_username,
|
||||||
webdav_password,
|
webdav_password,
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
})
|
},
|
||||||
|
false)
|
||||||
.await
|
.await
|
||||||
);
|
);
|
||||||
// 最后删除临时文件
|
// 最后删除临时文件
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod feat;
|
|||||||
mod utils;
|
mod utils;
|
||||||
use crate::core::hotkey;
|
use crate::core::hotkey;
|
||||||
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
||||||
|
use config::Config;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ pub fn run() {
|
|||||||
// clash
|
// clash
|
||||||
cmds::get_clash_info,
|
cmds::get_clash_info,
|
||||||
cmds::patch_clash_config,
|
cmds::patch_clash_config,
|
||||||
|
cmds::patch_clash_mode,
|
||||||
cmds::change_clash_core,
|
cmds::change_clash_core,
|
||||||
cmds::get_runtime_config,
|
cmds::get_runtime_config,
|
||||||
cmds::get_runtime_yaml,
|
cmds::get_runtime_yaml,
|
||||||
@@ -112,6 +114,9 @@ pub fn run() {
|
|||||||
cmds::delete_profile,
|
cmds::delete_profile,
|
||||||
cmds::read_profile_file,
|
cmds::read_profile_file,
|
||||||
cmds::save_profile_file,
|
cmds::save_profile_file,
|
||||||
|
// script validation
|
||||||
|
cmds::script_validate_notice,
|
||||||
|
cmds::validate_script_file,
|
||||||
// clash api
|
// clash api
|
||||||
cmds::clash_api_get_proxy_delay,
|
cmds::clash_api_get_proxy_delay,
|
||||||
// backup
|
// backup
|
||||||
@@ -159,6 +164,12 @@ pub fn run() {
|
|||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
if !is_enable_global_hotkey {
|
||||||
|
log_err!(hotkey::Hotkey::global().init())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Focused(false) => {
|
tauri::WindowEvent::Focused(false) => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -169,6 +180,12 @@ pub fn run() {
|
|||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
if !is_enable_global_hotkey {
|
||||||
|
log_err!(hotkey::Hotkey::global().reset())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Destroyed => {
|
tauri::WindowEvent::Destroyed => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ pub fn format_bytes_speed(speed: u64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_bytes_speed() {
|
fn test_format_bytes_speed() {
|
||||||
assert_eq!(format_bytes_speed(0), "0B/s");
|
assert_eq!(format_bytes_speed(0), "0B/s");
|
||||||
|
|||||||
@@ -95,16 +95,19 @@ pub async fn resolve_setup(app: &mut App) {
|
|||||||
log_err!(tray::Tray::global().init());
|
log_err!(tray::Tray::global().init());
|
||||||
log_err!(tray::Tray::global().create_systray());
|
log_err!(tray::Tray::global().create_systray());
|
||||||
|
|
||||||
|
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
||||||
|
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
||||||
|
|
||||||
|
// 初始化热键
|
||||||
|
log::trace!(target: "app", "init hotkeys");
|
||||||
|
log_err!(hotkey::Hotkey::global().init());
|
||||||
|
|
||||||
let silent_start = { Config::verge().data().enable_silent_start };
|
let silent_start = { Config::verge().data().enable_silent_start };
|
||||||
if !silent_start.unwrap_or(false) {
|
if !silent_start.unwrap_or(false) {
|
||||||
create_window();
|
create_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
|
||||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
|
||||||
|
|
||||||
log_err!(tray::Tray::global().update_part());
|
log_err!(tray::Tray::global().update_part());
|
||||||
log_err!(hotkey::Hotkey::global().init());
|
|
||||||
log_err!(timer::Timer::global().init());
|
log_err!(timer::Timer::global().init());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +126,18 @@ pub fn resolve_reset() {
|
|||||||
|
|
||||||
/// create main window
|
/// create main window
|
||||||
pub fn create_window() {
|
pub fn create_window() {
|
||||||
|
println!("Starting to create window");
|
||||||
|
log::info!(target: "app", "Starting to create window");
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
println!("Found existing window, trying to show it");
|
||||||
|
log::info!(target: "app", "Found existing window, trying to show it");
|
||||||
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
if window.is_minimized().unwrap_or(false) {
|
||||||
|
println!("Window is minimized, unminimizing");
|
||||||
|
log::info!(target: "app", "Window is minimized, unminimizing");
|
||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
}
|
}
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
@@ -134,25 +145,27 @@ pub fn create_window() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("Creating new window");
|
||||||
|
log::info!(target: "app", "Creating new window");
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let _ = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
"main".to_string(),
|
"main".to_string(),
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
)
|
)
|
||||||
.title("Clash Verge")
|
.title("Clash Verge")
|
||||||
.visible(false)
|
|
||||||
.inner_size(890.0, 700.0)
|
.inner_size(890.0, 700.0)
|
||||||
.min_inner_size(620.0, 550.0)
|
.min_inner_size(620.0, 550.0)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.maximizable(true)
|
.maximizable(true)
|
||||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||||
.transparent(true)
|
.transparent(true)
|
||||||
.shadow(false)
|
.shadow(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let _ = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
"main".to_string(),
|
"main".to_string(),
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
@@ -162,11 +175,10 @@ pub fn create_window() {
|
|||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
.inner_size(890.0, 700.0)
|
.inner_size(890.0, 700.0)
|
||||||
.min_inner_size(620.0, 550.0)
|
.min_inner_size(620.0, 550.0)
|
||||||
.build()
|
.build();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let _ = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
"main".to_string(),
|
"main".to_string(),
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
@@ -176,8 +188,23 @@ pub fn create_window() {
|
|||||||
.inner_size(890.0, 700.0)
|
.inner_size(890.0, 700.0)
|
||||||
.min_inner_size(620.0, 550.0)
|
.min_inner_size(620.0, 550.0)
|
||||||
.transparent(true)
|
.transparent(true)
|
||||||
.build()
|
.build();
|
||||||
.unwrap();
|
|
||||||
|
match window {
|
||||||
|
Ok(window) => {
|
||||||
|
println!("Window created successfully, attempting to show");
|
||||||
|
log::info!(target: "app", "Window created successfully, attempting to show");
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
|
||||||
|
// 设置窗口状态监控,实时保存窗口位置和大小
|
||||||
|
crate::feat::setup_window_state_monitor(&app_handle);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to create window: {:?}", e);
|
||||||
|
log::error!(target: "app", "Failed to create window: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"copyright": "GNU General Public License v3.0",
|
"copyright": "GNU General Public License v3.0",
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "Clash Verge Rev",
|
"shortDescription": "Clash Verge Rev",
|
||||||
"createUpdaterArtifacts": "v1Compatible"
|
"createUpdaterArtifacts": true
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "pnpm run web:build",
|
"beforeBuildCommand": "pnpm run web:build",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"devUrl": "http://localhost:3000/"
|
"devUrl": "http://localhost:3000/"
|
||||||
},
|
},
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "2.0.3",
|
"version": "2.1.2",
|
||||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
@@ -33,7 +33,10 @@
|
|||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||||
]
|
],
|
||||||
|
"windows": {
|
||||||
|
"installMode": "basicUi"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
"desktop": {
|
"desktop": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"trayIcon": {
|
"trayIcon": {
|
||||||
"iconPath": "icons/tray-icon.ico",
|
"iconPath": "icons/tray-icon.ico",
|
||||||
"iconAsTemplate": true,
|
"iconAsTemplate": true,
|
||||||
"menuOnLeftClick": false
|
"showMenuOnLeftClick": false
|
||||||
},
|
},
|
||||||
"windows": []
|
"windows": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 2px;
|
right: 1px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface InnerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NoticeInner = (props: InnerProps) => {
|
const NoticeInner = (props: InnerProps) => {
|
||||||
const { type, message, duration = 1500, onClose } = props;
|
const { type, message, duration, onClose } = props;
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const [isDark, setIsDark] = useState(false);
|
const [isDark, setIsDark] = useState(false);
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
@@ -43,7 +43,7 @@ const NoticeInner = (props: InnerProps) => {
|
|||||||
|
|
||||||
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
|
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
|
||||||
const unlisten = appWindow.onThemeChanged((e) =>
|
const unlisten = appWindow.onThemeChanged((e) =>
|
||||||
setIsDark(e.payload === "dark")
|
setIsDark(e.payload === "dark"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -72,7 +72,7 @@ const NoticeInner = (props: InnerProps) => {
|
|||||||
<Snackbar
|
<Snackbar
|
||||||
open={visible}
|
open={visible}
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
autoHideDuration={duration}
|
autoHideDuration={duration === -1 ? null : duration || 1500}
|
||||||
onClose={onAutoClose}
|
onClose={onAutoClose}
|
||||||
message={msgElement}
|
message={msgElement}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -105,25 +105,55 @@ let parent: HTMLDivElement = null!;
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const Notice: NoticeInstance = (props) => {
|
export const Notice: NoticeInstance = (props) => {
|
||||||
|
const { type, message, duration } = props;
|
||||||
|
|
||||||
|
// 验证必要的参数
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
parent = document.createElement("div");
|
parent = document.createElement("div");
|
||||||
|
parent.setAttribute("id", "notice-container"); // 添加 id 便于调试
|
||||||
document.body.appendChild(parent);
|
document.body.appendChild(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
parent.appendChild(container);
|
parent.appendChild(container);
|
||||||
|
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
const onUnmount = () => {
|
const onUnmount = () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
if (parent) setTimeout(() => parent.removeChild(container), 500);
|
if (parent && container.parentNode === parent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
parent.removeChild(container);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
root.render(<NoticeInner {...props} onClose={onUnmount} />);
|
root.render(
|
||||||
|
<NoticeInner
|
||||||
|
type={type}
|
||||||
|
message={message}
|
||||||
|
duration={duration || 1500}
|
||||||
|
onClose={onUnmount}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
(["info", "error", "success"] as const).forEach((type) => {
|
(["info", "error", "success"] as const).forEach((type) => {
|
||||||
Notice[type] = (message, duration) => {
|
Notice[type] = (message: ReactNode, duration?: number) => {
|
||||||
setTimeout(() => Notice({ type, message, duration }), 0);
|
// 确保消息不为空
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notice({
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
// 错误类型通知显示 8 秒,其他类型默认 1.5 秒
|
||||||
|
duration: type === "error" ? 8000 : duration || 1500,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, SvgIcon, TextField, styled } from "@mui/material";
|
import { Box, SvgIcon, TextField, styled } from "@mui/material";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
||||||
@@ -22,7 +22,20 @@ type SearchProps = {
|
|||||||
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BaseSearchBox = styled((props: SearchProps) => {
|
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
||||||
|
paddingRight: "4px",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root svg[aria-label='active'] path": {
|
||||||
|
fill: theme.palette.primary.light,
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
|
||||||
|
fill: "#A7A7A7",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BaseSearchBox = (props: SearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
|
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
|
||||||
@@ -43,58 +56,58 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
|||||||
inheritViewBox: true,
|
inheritViewBox: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const createMatcher = useMemo(() => {
|
||||||
if (!inputRef.current) return;
|
return (searchText: string) => {
|
||||||
|
|
||||||
onChange({
|
|
||||||
target: inputRef.current,
|
|
||||||
} as ChangeEvent<HTMLInputElement>);
|
|
||||||
}, [matchCase, matchWholeWord, useRegularExpression]);
|
|
||||||
|
|
||||||
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
props.onSearch(
|
|
||||||
(content) => doSearch([content], e.target?.value ?? "").length > 0,
|
|
||||||
{
|
|
||||||
text: e.target?.value ?? "",
|
|
||||||
matchCase,
|
|
||||||
matchWholeWord,
|
|
||||||
useRegularExpression,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doSearch = (searchList: string[], searchItem: string) => {
|
|
||||||
setErrorMessage("");
|
|
||||||
return searchList.filter((item) => {
|
|
||||||
try {
|
try {
|
||||||
let searchItemCopy = searchItem;
|
return (content: string) => {
|
||||||
if (!matchCase) {
|
if (!searchText) return true;
|
||||||
item = item.toLowerCase();
|
|
||||||
searchItemCopy = searchItemCopy.toLowerCase();
|
let item = !matchCase ? content.toLowerCase() : content;
|
||||||
}
|
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
|
||||||
if (matchWholeWord) {
|
|
||||||
const regex = new RegExp(`\\b${searchItemCopy}\\b`);
|
|
||||||
if (useRegularExpression) {
|
if (useRegularExpression) {
|
||||||
const regexWithOptions = new RegExp(searchItemCopy);
|
return new RegExp(searchItem).test(item);
|
||||||
return regexWithOptions.test(item) && regex.test(item);
|
|
||||||
} else {
|
|
||||||
return regex.test(item);
|
|
||||||
}
|
}
|
||||||
} else if (useRegularExpression) {
|
|
||||||
const regex = new RegExp(searchItemCopy);
|
if (matchWholeWord) {
|
||||||
return regex.test(item);
|
return new RegExp(`\\b${searchItem}\\b`).test(item);
|
||||||
} else {
|
}
|
||||||
return item.includes(searchItemCopy);
|
|
||||||
}
|
return item.includes(searchItem);
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorMessage(`${err}`);
|
setErrorMessage(`${err}`);
|
||||||
|
return () => true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}, [matchCase, matchWholeWord, useRegularExpression]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
const value = inputRef.current.value;
|
||||||
|
setErrorMessage("");
|
||||||
|
props.onSearch(createMatcher(value), {
|
||||||
|
text: value,
|
||||||
|
matchCase,
|
||||||
|
matchWholeWord,
|
||||||
|
useRegularExpression,
|
||||||
|
});
|
||||||
|
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]);
|
||||||
|
|
||||||
|
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const value = e.target?.value ?? "";
|
||||||
|
setErrorMessage("");
|
||||||
|
props.onSearch(createMatcher(value), {
|
||||||
|
text: value,
|
||||||
|
matchCase,
|
||||||
|
matchWholeWord,
|
||||||
|
useRegularExpression,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={errorMessage} placement="bottom-start">
|
<Tooltip title={errorMessage} placement="bottom-start">
|
||||||
<TextField
|
<StyledTextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
@@ -115,9 +128,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
|||||||
component={matchCaseIcon}
|
component={matchCaseIcon}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
aria-label={matchCase ? "active" : "inactive"}
|
aria-label={matchCase ? "active" : "inactive"}
|
||||||
onClick={() => {
|
onClick={() => setMatchCase(!matchCase)}
|
||||||
setMatchCase(!matchCase);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -127,9 +138,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
|||||||
component={matchWholeWordIcon}
|
component={matchWholeWordIcon}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
aria-label={matchWholeWord ? "active" : "inactive"}
|
aria-label={matchWholeWord ? "active" : "inactive"}
|
||||||
onClick={() => {
|
onClick={() => setMatchWholeWord(!matchWholeWord)}
|
||||||
setMatchWholeWord(!matchWholeWord);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -139,28 +148,16 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
|||||||
component={useRegularExpressionIcon}
|
component={useRegularExpressionIcon}
|
||||||
aria-label={useRegularExpression ? "active" : "inactive"}
|
aria-label={useRegularExpression ? "active" : "inactive"}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
setUseRegularExpression(!useRegularExpression);
|
setUseRegularExpression(!useRegularExpression)
|
||||||
}}
|
}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})(({ theme }) => ({
|
};
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
|
||||||
"padding-right": "4px",
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root svg[aria-label='active'] path": {
|
|
||||||
fill: theme.palette.primary.light,
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
|
|
||||||
fill: "#A7A7A7",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||||
const host = metadata.host
|
const host = metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||||
|
const Destination = metadata.destinationIP
|
||||||
|
? metadata.destinationIP
|
||||||
|
: metadata.remoteDestination;
|
||||||
|
|
||||||
const information = [
|
const information = [
|
||||||
{ label: t("Host"), value: host },
|
{ label: t("Host"), value: host },
|
||||||
@@ -79,7 +82,8 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
label: t("Source"),
|
label: t("Source"),
|
||||||
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
},
|
},
|
||||||
{ label: t("Destination IP"), value: metadata.destinationIP },
|
{ label: t("Destination"), value: Destination },
|
||||||
|
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
|
||||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from "@/services/states";
|
||||||
import { truncateStr } from "@/utils/truncate-str";
|
import { truncateStr } from "@/utils/truncate-str";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
@@ -21,12 +21,24 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
||||||
|
() => {
|
||||||
|
const saved = localStorage.getItem("connection-table-widths");
|
||||||
|
return saved ? JSON.parse(saved) : {};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const [columns] = useState<GridColDef[]>([
|
const [columns] = useState<GridColDef[]>([
|
||||||
{ field: "host", headerName: t("Host"), flex: 220, minWidth: 220 },
|
{
|
||||||
|
field: "host",
|
||||||
|
headerName: t("Host"),
|
||||||
|
width: columnWidths["host"] || 220,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "download",
|
field: "download",
|
||||||
headerName: t("Downloaded"),
|
headerName: t("Downloaded"),
|
||||||
width: 88,
|
width: columnWidths["download"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
||||||
@@ -34,7 +46,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
{
|
{
|
||||||
field: "upload",
|
field: "upload",
|
||||||
headerName: t("Uploaded"),
|
headerName: t("Uploaded"),
|
||||||
width: 88,
|
width: columnWidths["upload"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
||||||
@@ -42,7 +54,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: "dlSpeed",
|
||||||
headerName: t("DL Speed"),
|
headerName: t("DL Speed"),
|
||||||
width: 88,
|
width: columnWidths["dlSpeed"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
||||||
@@ -50,18 +62,33 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: "ulSpeed",
|
||||||
headerName: t("UL Speed"),
|
headerName: t("UL Speed"),
|
||||||
width: 88,
|
width: columnWidths["ulSpeed"] || 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
||||||
},
|
},
|
||||||
{ field: "chains", headerName: t("Chains"), flex: 360, minWidth: 360 },
|
{
|
||||||
{ field: "rule", headerName: t("Rule"), flex: 300, minWidth: 250 },
|
field: "chains",
|
||||||
{ field: "process", headerName: t("Process"), flex: 240, minWidth: 120 },
|
headerName: t("Chains"),
|
||||||
|
width: columnWidths["chains"] || 340,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "rule",
|
||||||
|
headerName: t("Rule"),
|
||||||
|
width: columnWidths["rule"] || 280,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "process",
|
||||||
|
headerName: t("Process"),
|
||||||
|
width: columnWidths["process"] || 220,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "time",
|
field: "time",
|
||||||
headerName: t("Time"),
|
headerName: t("Time"),
|
||||||
flex: 120,
|
width: columnWidths["time"] || 120,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
@@ -69,26 +96,56 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
new Date(v2).getTime() - new Date(v1).getTime(),
|
new Date(v2).getTime() - new Date(v1).getTime(),
|
||||||
valueFormatter: (value: number) => dayjs(value).fromNow(),
|
valueFormatter: (value: number) => dayjs(value).fromNow(),
|
||||||
},
|
},
|
||||||
{ field: "source", headerName: t("Source"), flex: 200, minWidth: 130 },
|
|
||||||
{
|
{
|
||||||
field: "destinationIP",
|
field: "source",
|
||||||
headerName: t("Destination IP"),
|
headerName: t("Source"),
|
||||||
flex: 200,
|
width: columnWidths["source"] || 200,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
{ field: "type", headerName: t("Type"), flex: 160, minWidth: 100 },
|
{
|
||||||
|
field: "remoteDestination",
|
||||||
|
headerName: t("Destination"),
|
||||||
|
width: columnWidths["remoteDestination"] || 200,
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "type",
|
||||||
|
headerName: t("Type"),
|
||||||
|
width: columnWidths["type"] || 160,
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Saving column widths:", columnWidths);
|
||||||
|
localStorage.setItem(
|
||||||
|
"connection-table-widths",
|
||||||
|
JSON.stringify(columnWidths),
|
||||||
|
);
|
||||||
|
}, [columnWidths]);
|
||||||
|
|
||||||
|
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||||
|
const { colDef, width } = params;
|
||||||
|
console.log("Column resize:", colDef.field, width);
|
||||||
|
setColumnWidths((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[colDef.field]: width,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const connRows = useMemo(() => {
|
const connRows = useMemo(() => {
|
||||||
return connections.map((each) => {
|
return connections.map((each) => {
|
||||||
const { metadata, rulePayload } = each;
|
const { metadata, rulePayload } = each;
|
||||||
const chains = [...each.chains].reverse().join(" / ");
|
const chains = [...each.chains].reverse().join(" / ");
|
||||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||||
|
const Destination = metadata.destinationIP
|
||||||
|
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||||
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||||
return {
|
return {
|
||||||
id: each.id,
|
id: each.id,
|
||||||
host: metadata.host
|
host: metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||||
download: each.download,
|
download: each.download,
|
||||||
upload: each.upload,
|
upload: each.upload,
|
||||||
dlSpeed: each.curDownload,
|
dlSpeed: each.curDownload,
|
||||||
@@ -98,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
process: truncateStr(metadata.process || metadata.processPath),
|
process: truncateStr(metadata.process || metadata.processPath),
|
||||||
time: each.start,
|
time: each.start,
|
||||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
destinationIP: metadata.destinationIP,
|
remoteDestination: Destination,
|
||||||
type: `${metadata.type}(${metadata.network})`,
|
type: `${metadata.type}(${metadata.network})`,
|
||||||
connectionData: each,
|
connectionData: each,
|
||||||
};
|
};
|
||||||
@@ -115,12 +172,14 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
border: "none",
|
border: "none",
|
||||||
"div:focus": { outline: "none !important" },
|
"div:focus": { outline: "none !important" },
|
||||||
"& div[aria-rowindex]": {
|
"& .MuiDataGrid-columnHeader": {
|
||||||
backgroundColor: `${backgroundColor} !important`,
|
userSelect: "none",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
columnVisibilityModel={columnVisible}
|
columnVisibilityModel={columnVisible}
|
||||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||||
|
onColumnResize={handleColumnResize}
|
||||||
|
disableColumnMenu={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ const LogItem = ({ value, searchState }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Item>
|
<Item>
|
||||||
<div>
|
<div>
|
||||||
<span className="time">{value.time}</span>
|
<span className="time">{renderHighlightText(value.time || "")}</span>
|
||||||
<span className="type" data-type={value.type.toLowerCase()}>
|
<span className="type" data-type={value.type.toLowerCase()}>
|
||||||
{value.type}
|
{renderHighlightText(value.type)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, useEffect } 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 {
|
||||||
@@ -16,6 +16,173 @@ import { ProxyRender } from "./proxy-render";
|
|||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScrollTopButton } from "../layout/scroll-top-button";
|
import { ScrollTopButton } from "../layout/scroll-top-button";
|
||||||
|
import { Box, styled } from "@mui/material";
|
||||||
|
import { memo } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
// 将选择器组件抽离出来,避免主组件重渲染时重复创建样式
|
||||||
|
const AlphabetSelector = styled(Box)(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
right: 4,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
background: "transparent",
|
||||||
|
zIndex: 1000,
|
||||||
|
gap: "2px",
|
||||||
|
// padding: "4px 2px",
|
||||||
|
willChange: "transform",
|
||||||
|
"&:hover": {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
borderRadius: "8px",
|
||||||
|
},
|
||||||
|
"& .scroll-container": {
|
||||||
|
overflow: "hidden",
|
||||||
|
maxHeight: "inherit",
|
||||||
|
},
|
||||||
|
"& .letter-container": {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "2px",
|
||||||
|
transition: "transform 0.2s ease",
|
||||||
|
},
|
||||||
|
"& .letter": {
|
||||||
|
padding: "1px 4px",
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
position: "relative",
|
||||||
|
width: "1.5em",
|
||||||
|
height: "1.5em",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||||
|
transform: "scale(1) translateZ(0)",
|
||||||
|
backfaceVisibility: "hidden",
|
||||||
|
borderRadius: "6px",
|
||||||
|
"&:hover": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
transform: "scale(1.4) translateZ(0)",
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 创建一个单独的 Tooltip 组件
|
||||||
|
const Tooltip = styled("div")(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
boxShadow: theme.shadows[3],
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
pointerEvents: "none",
|
||||||
|
"&::after": {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
right: "-4px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderTop: "4px solid transparent",
|
||||||
|
borderBottom: "4px solid transparent",
|
||||||
|
borderLeft: `4px solid ${theme.palette.background.paper}`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 抽离字母选择器子组件
|
||||||
|
const LetterItem = memo(
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
onClick,
|
||||||
|
getFirstChar,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
onClick: (name: string) => void;
|
||||||
|
getFirstChar: (str: string) => string;
|
||||||
|
}) => {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const letterRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [tooltipPosition, setTooltipPosition] = useState({
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
});
|
||||||
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const updateTooltipPosition = useCallback(() => {
|
||||||
|
if (!letterRef.current) return;
|
||||||
|
const rect = letterRef.current.getBoundingClientRect();
|
||||||
|
setTooltipPosition({
|
||||||
|
top: rect.top + rect.height / 2,
|
||||||
|
right: window.innerWidth - rect.left + 8,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showTooltip) {
|
||||||
|
updateTooltipPosition();
|
||||||
|
}
|
||||||
|
}, [showTooltip, updateTooltipPosition]);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
setShowTooltip(true);
|
||||||
|
// 添加 200ms 的延迟,避免鼠标快速划过时触发滚动
|
||||||
|
hoverTimeoutRef.current = setTimeout(() => {
|
||||||
|
onClick(name);
|
||||||
|
}, 100);
|
||||||
|
}, [name, onClick]);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setShowTooltip(false);
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={letterRef}
|
||||||
|
className="letter"
|
||||||
|
onClick={() => onClick(name)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<span>{getFirstChar(name)}</span>
|
||||||
|
</div>
|
||||||
|
{showTooltip &&
|
||||||
|
createPortal(
|
||||||
|
<Tooltip
|
||||||
|
style={{
|
||||||
|
top: tooltipPosition.top,
|
||||||
|
right: tooltipPosition.right,
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Tooltip>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode: string;
|
mode: string;
|
||||||
@@ -32,22 +199,128 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
|
const scrollPositionRef = useRef<Record<string, number>>({});
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
|
const scrollerRef = useRef<Element | null>(null);
|
||||||
|
const letterContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const alphabetSelectorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [maxHeight, setMaxHeight] = useState("auto");
|
||||||
|
|
||||||
// 添加滚动处理函数
|
// 使用useMemo缓存字母索引数据
|
||||||
const handleScroll = (e: any) => {
|
const { groupFirstLetters, letterIndexMap } = useMemo(() => {
|
||||||
const scrollTop = e.target.scrollTop;
|
const letters = new Set<string>();
|
||||||
setShowScrollTop(scrollTop > 100);
|
const indexMap: Record<string, number> = {};
|
||||||
};
|
|
||||||
|
renderList.forEach((item, index) => {
|
||||||
|
if (item.type === 0) {
|
||||||
|
const fullName = item.group.name;
|
||||||
|
letters.add(fullName);
|
||||||
|
if (!(fullName in indexMap)) {
|
||||||
|
indexMap[fullName] = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupFirstLetters: Array.from(letters),
|
||||||
|
letterIndexMap: indexMap,
|
||||||
|
};
|
||||||
|
}, [renderList]);
|
||||||
|
|
||||||
|
// 缓存getFirstChar函数
|
||||||
|
const getFirstChar = useCallback((str: string) => {
|
||||||
|
const regex = /\p{Extended_Pictographic}|\p{L}|\p{N}|./u;
|
||||||
|
const match = str.match(regex);
|
||||||
|
return match ? match[0] : str.charAt(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 从 localStorage 恢复滚动位置
|
||||||
|
useEffect(() => {
|
||||||
|
if (renderList.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedPositions = localStorage.getItem("proxy-scroll-positions");
|
||||||
|
if (savedPositions) {
|
||||||
|
const positions = JSON.parse(savedPositions);
|
||||||
|
scrollPositionRef.current = positions;
|
||||||
|
const savedPosition = positions[mode];
|
||||||
|
|
||||||
|
if (savedPosition !== undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
virtuosoRef.current?.scrollTo({
|
||||||
|
top: savedPosition,
|
||||||
|
behavior: "auto",
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error restoring scroll position:", e);
|
||||||
|
}
|
||||||
|
}, [mode, renderList]);
|
||||||
|
|
||||||
|
// 使用防抖保存滚动位置
|
||||||
|
const saveScrollPosition = useCallback(
|
||||||
|
(scrollTop: number) => {
|
||||||
|
try {
|
||||||
|
scrollPositionRef.current[mode] = scrollTop;
|
||||||
|
localStorage.setItem(
|
||||||
|
"proxy-scroll-positions",
|
||||||
|
JSON.stringify(scrollPositionRef.current),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving scroll position:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 优化滚动处理函数,使用防抖
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
debounce((e: any) => {
|
||||||
|
const scrollTop = e.target.scrollTop;
|
||||||
|
setShowScrollTop(scrollTop > 100);
|
||||||
|
saveScrollPosition(scrollTop);
|
||||||
|
}, 16),
|
||||||
|
[saveScrollPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加和清理滚动事件监听器
|
||||||
|
useEffect(() => {
|
||||||
|
const currentScroller = scrollerRef.current;
|
||||||
|
if (currentScroller) {
|
||||||
|
currentScroller.addEventListener("scroll", handleScroll, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
currentScroller.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
// 滚动到顶部
|
// 滚动到顶部
|
||||||
const scrollToTop = () => {
|
const scrollToTop = useCallback(() => {
|
||||||
virtuosoRef.current?.scrollTo?.({
|
virtuosoRef.current?.scrollTo?.({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
};
|
saveScrollPosition(0);
|
||||||
|
}, [saveScrollPosition]);
|
||||||
|
|
||||||
|
// 处理字母点击,使用useCallback
|
||||||
|
const handleLetterClick = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
const index = letterIndexMap[name];
|
||||||
|
if (index !== undefined) {
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index,
|
||||||
|
align: "start",
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[letterIndexMap],
|
||||||
|
);
|
||||||
|
|
||||||
// 切换分组的节点代理
|
// 切换分组的节点代理
|
||||||
const handleChangeProxy = useLockFn(
|
const handleChangeProxy = useLockFn(
|
||||||
@@ -134,6 +407,64 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加滚轮事件处理函数
|
||||||
|
const handleWheel = useCallback((e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!letterContainerRef.current) return;
|
||||||
|
|
||||||
|
const container = letterContainerRef.current;
|
||||||
|
const scrollAmount = e.deltaY;
|
||||||
|
const currentTransform = new WebKitCSSMatrix(container.style.transform);
|
||||||
|
const currentY = currentTransform.m42 || 0;
|
||||||
|
|
||||||
|
const containerHeight = container.getBoundingClientRect().height;
|
||||||
|
const parentHeight =
|
||||||
|
container.parentElement?.getBoundingClientRect().height || 0;
|
||||||
|
const maxScroll = Math.max(0, containerHeight - parentHeight);
|
||||||
|
|
||||||
|
let newY = currentY - scrollAmount;
|
||||||
|
newY = Math.min(0, Math.max(-maxScroll, newY));
|
||||||
|
|
||||||
|
container.style.transform = `translateY(${newY}px)`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 添加和移除滚轮事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
const container = letterContainerRef.current?.parentElement;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("wheel", handleWheel);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [handleWheel]);
|
||||||
|
|
||||||
|
// 添加窗口大小变化监听和最大高度计算
|
||||||
|
const updateMaxHeight = useCallback(() => {
|
||||||
|
if (!alphabetSelectorRef.current) return;
|
||||||
|
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const bottomMargin = 60; // 底部边距
|
||||||
|
const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍
|
||||||
|
const availableHeight = windowHeight - (topMargin + bottomMargin);
|
||||||
|
|
||||||
|
// 调整选择器的位置,使其偏下
|
||||||
|
const offsetPercentage =
|
||||||
|
(((topMargin - bottomMargin) / windowHeight) * 100) / 2;
|
||||||
|
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
|
||||||
|
|
||||||
|
setMaxHeight(`${availableHeight}px`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
useEffect(() => {
|
||||||
|
updateMaxHeight();
|
||||||
|
window.addEventListener("resize", updateMaxHeight);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", updateMaxHeight);
|
||||||
|
};
|
||||||
|
}, [updateMaxHeight]);
|
||||||
|
|
||||||
if (mode === "direct") {
|
if (mode === "direct") {
|
||||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
return <BaseEmpty text={t("clash_mode_direct")} />;
|
||||||
}
|
}
|
||||||
@@ -142,30 +473,57 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
<div style={{ position: "relative", height: "100%" }}>
|
<div style={{ position: "relative", height: "100%" }}>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
style={{ height: "calc(100% - 16px)" }}
|
style={{ height: "calc(100% - 14px)" }}
|
||||||
totalCount={renderList.length}
|
totalCount={renderList.length}
|
||||||
increaseViewportBy={256}
|
increaseViewportBy={{ top: 256, bottom: 256 }}
|
||||||
|
overscan={150}
|
||||||
|
defaultItemHeight={56}
|
||||||
scrollerRef={(ref) => {
|
scrollerRef={(ref) => {
|
||||||
if (ref) {
|
scrollerRef.current = ref as Element;
|
||||||
ref.addEventListener("scroll", handleScroll);
|
}}
|
||||||
}
|
components={{
|
||||||
|
Footer: () => <div style={{ height: "8px" }} />,
|
||||||
}}
|
}}
|
||||||
itemContent={(index) => (
|
itemContent={(index) => (
|
||||||
<>
|
<ProxyRender
|
||||||
<ProxyRender
|
key={renderList[index].key}
|
||||||
key={renderList[index].key}
|
item={renderList[index]}
|
||||||
item={renderList[index]}
|
indent={mode === "rule" || mode === "script"}
|
||||||
indent={mode === "rule" || mode === "script"}
|
onLocation={handleLocation}
|
||||||
onLocation={handleLocation}
|
onCheckAll={handleCheckAll}
|
||||||
onCheckAll={handleCheckAll}
|
onHeadState={onHeadState}
|
||||||
onHeadState={onHeadState}
|
onChangeProxy={handleChangeProxy}
|
||||||
onChangeProxy={handleChangeProxy}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
|
|
||||||
|
<AlphabetSelector ref={alphabetSelectorRef} style={{ maxHeight }}>
|
||||||
|
<div className="scroll-container">
|
||||||
|
<div ref={letterContainerRef} className="letter-container">
|
||||||
|
{groupFirstLetters.map((name) => (
|
||||||
|
<LetterItem
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
onClick={handleLetterClick}
|
||||||
|
getFirstChar={getFirstChar}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlphabetSelector>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 简单的防抖函数
|
||||||
|
function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number,
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={<StyledPrimary>{group.name}</StyledPrimary>}
|
primary={<StyledPrimary>{group.name}</StyledPrimary>}
|
||||||
secondary={
|
secondary={
|
||||||
<ListItemTextChild
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -108,16 +108,19 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
pt: "2px",
|
pt: "2px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ marginTop: "2px" }}>
|
<Box component="span" sx={{ marginTop: "2px" }}>
|
||||||
<StyledTypeBox>{group.type}</StyledTypeBox>
|
<StyledTypeBox>{group.type}</StyledTypeBox>
|
||||||
<StyledSubtitle sx={{ color: "text.secondary" }}>
|
<StyledSubtitle sx={{ color: "text.secondary" }}>
|
||||||
{group.now}
|
{group.now}
|
||||||
</StyledSubtitle>
|
</StyledSubtitle>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemTextChild>
|
</Box>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{
|
slotProps={{
|
||||||
sx: { display: "flex", alignItems: "center", color: "#ccc" },
|
secondary: {
|
||||||
|
component: "div",
|
||||||
|
sx: { display: "flex", alignItems: "center", color: "#ccc" },
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||||
@@ -204,7 +207,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StyledPrimary = styled("span")`
|
const StyledPrimary = styled("span")`
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -219,11 +222,7 @@ const StyledSubtitle = styled("span")`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ListItemTextChild = styled("span")`
|
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid #ccc",
|
border: "1px solid #ccc",
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useCallback } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
import { getProxies } from "@/services/api";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { filterSort } from "./use-filter-sort";
|
import { filterSort } from "./use-filter-sort";
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "./use-head-state";
|
} from "./use-head-state";
|
||||||
|
|
||||||
export interface IRenderItem {
|
export interface IRenderItem {
|
||||||
// 组 | head | item | empty | item col
|
// 组 | head | item | empty | item col
|
||||||
type: 0 | 1 | 2 | 3 | 4;
|
type: 0 | 1 | 2 | 3 | 4;
|
||||||
key: string;
|
key: string;
|
||||||
group: IProxyGroupItem;
|
group: IProxyGroupItem;
|
||||||
@@ -19,32 +19,59 @@ export interface IRenderItem {
|
|||||||
col?: number;
|
col?: number;
|
||||||
proxyCol?: IProxyItem[];
|
proxyCol?: IProxyItem[];
|
||||||
headState?: HeadState;
|
headState?: HeadState;
|
||||||
|
// 新增支持图标和其他元数据
|
||||||
|
icon?: string;
|
||||||
|
provider?: string;
|
||||||
|
testUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优化列布局计算
|
||||||
|
const calculateColumns = (width: number, configCol: number): number => {
|
||||||
|
if (configCol > 0 && configCol < 6) return configCol;
|
||||||
|
|
||||||
|
if (width > 1920) return 5;
|
||||||
|
if (width > 1450) return 4;
|
||||||
|
if (width > 1024) return 3;
|
||||||
|
if (width > 900) return 2;
|
||||||
|
if (width >= 600) return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 优化分组逻辑
|
||||||
|
const groupProxies = <T = any>(list: T[], size: number): T[][] => {
|
||||||
|
return list.reduce((acc, item) => {
|
||||||
|
const lastGroup = acc[acc.length - 1];
|
||||||
|
if (!lastGroup || lastGroup.length >= size) {
|
||||||
|
acc.push([item]);
|
||||||
|
} else {
|
||||||
|
lastGroup.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as T[][]);
|
||||||
|
};
|
||||||
|
|
||||||
export const useRenderList = (mode: string) => {
|
export const useRenderList = (mode: string) => {
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
||||||
"getProxies",
|
"getProxies",
|
||||||
getProxies,
|
getProxies,
|
||||||
{ refreshInterval: 45000 },
|
{
|
||||||
|
refreshInterval: 2000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { width } = useWindowWidth();
|
const { width } = useWindowWidth();
|
||||||
|
|
||||||
let col = Math.floor(verge?.proxy_layout_column || 6);
|
|
||||||
|
|
||||||
// 自适应
|
|
||||||
if (col >= 6 || col <= 0) {
|
|
||||||
if (width > 1450) col = 4;
|
|
||||||
else if (width > 1024) col = 3;
|
|
||||||
else if (width > 900) col = 2;
|
|
||||||
else if (width >= 600) col = 2;
|
|
||||||
else col = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [headStates, setHeadState] = useHeadStateNew();
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
|
|
||||||
// make sure that fetch the proxies successfully
|
// 计算列数
|
||||||
|
const col = useMemo(
|
||||||
|
() => calculateColumns(width, verge?.proxy_layout_column || 6),
|
||||||
|
[width, verge?.proxy_layout_column],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保代理数据加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxiesData) return;
|
if (!proxiesData) return;
|
||||||
const { groups, proxies } = proxiesData;
|
const { groups, proxies } = proxiesData;
|
||||||
@@ -55,22 +82,29 @@ export const useRenderList = (mode: string) => {
|
|||||||
) {
|
) {
|
||||||
setTimeout(() => mutateProxies(), 500);
|
setTimeout(() => mutateProxies(), 500);
|
||||||
}
|
}
|
||||||
}, [proxiesData, mode]);
|
}, [proxiesData, mode, mutateProxies]);
|
||||||
|
|
||||||
|
// 处理渲染列表
|
||||||
const renderList: IRenderItem[] = useMemo(() => {
|
const renderList: IRenderItem[] = useMemo(() => {
|
||||||
if (!proxiesData) return [];
|
if (!proxiesData) return [];
|
||||||
|
|
||||||
// global 和 direct 使用展开的样式
|
|
||||||
const useRule = mode === "rule" || mode === "script";
|
const useRule = mode === "rule" || mode === "script";
|
||||||
const renderGroups =
|
const renderGroups =
|
||||||
(useRule && proxiesData.groups.length
|
useRule && proxiesData.groups.length
|
||||||
? proxiesData.groups
|
? proxiesData.groups
|
||||||
: [proxiesData.global!]) || [];
|
: [proxiesData.global!];
|
||||||
|
|
||||||
const retList = renderGroups.flatMap((group) => {
|
const retList = renderGroups.flatMap((group) => {
|
||||||
const headState = headStates[group.name] || DEFAULT_STATE;
|
const headState = headStates[group.name] || DEFAULT_STATE;
|
||||||
const ret: IRenderItem[] = [
|
const ret: IRenderItem[] = [
|
||||||
{ type: 0, key: group.name, group, headState },
|
{
|
||||||
|
type: 0,
|
||||||
|
key: group.name,
|
||||||
|
group,
|
||||||
|
headState,
|
||||||
|
icon: group.icon,
|
||||||
|
testUrl: group.testUrl,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (headState?.open || !useRule) {
|
if (headState?.open || !useRule) {
|
||||||
@@ -81,61 +115,56 @@ export const useRenderList = (mode: string) => {
|
|||||||
headState.sortType,
|
headState.sortType,
|
||||||
);
|
);
|
||||||
|
|
||||||
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
|
ret.push({
|
||||||
|
type: 1,
|
||||||
|
key: `head-${group.name}`,
|
||||||
|
group,
|
||||||
|
headState,
|
||||||
|
});
|
||||||
|
|
||||||
if (!proxies.length) {
|
if (!proxies.length) {
|
||||||
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
|
ret.push({
|
||||||
}
|
type: 3,
|
||||||
|
key: `empty-${group.name}`,
|
||||||
// 支持多列布局
|
group,
|
||||||
if (col > 1) {
|
headState,
|
||||||
|
});
|
||||||
|
} else if (col > 1) {
|
||||||
return ret.concat(
|
return ret.concat(
|
||||||
groupList(proxies, col).map((proxyCol) => ({
|
groupProxies(proxies, col).map((proxyCol) => ({
|
||||||
type: 4,
|
type: 4,
|
||||||
key: `col-${group.name}-${proxyCol[0].name}`,
|
key: `col-${group.name}-${proxyCol[0].name}`,
|
||||||
group,
|
group,
|
||||||
headState,
|
headState,
|
||||||
col,
|
col,
|
||||||
proxyCol,
|
proxyCol,
|
||||||
|
provider: proxyCol[0].provider,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return ret.concat(
|
||||||
|
proxies.map((proxy) => ({
|
||||||
|
type: 2,
|
||||||
|
key: `${group.name}-${proxy!.name}`,
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
headState,
|
||||||
|
provider: proxy.provider,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.concat(
|
|
||||||
proxies.map((proxy) => ({
|
|
||||||
type: 2,
|
|
||||||
key: `${group.name}-${proxy!.name}`,
|
|
||||||
group,
|
|
||||||
proxy,
|
|
||||||
headState,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!useRule) return retList.slice(1);
|
if (!useRule) return retList.slice(1);
|
||||||
return retList.filter((item) => item.group.hidden === false);
|
return retList.filter((item) => !item.group.hidden);
|
||||||
}, [headStates, proxiesData, mode, col]);
|
}, [headStates, proxiesData, mode, col]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderList,
|
renderList,
|
||||||
onProxies: mutateProxies,
|
onProxies: mutateProxies,
|
||||||
onHeadState: setHeadState,
|
onHeadState: setHeadState,
|
||||||
|
currentColumns: col,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function groupList<T = any>(list: T[], size: number): T[][] {
|
|
||||||
return list.reduce((p, n) => {
|
|
||||||
if (!p.length) return [[n]];
|
|
||||||
|
|
||||||
const i = p.length - 1;
|
|
||||||
if (p[i].length < size) {
|
|
||||||
p[i].push(n);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.push([n]);
|
|
||||||
return p;
|
|
||||||
}, [] as T[][]);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
mutate("getClashConfig");
|
mutate("getClashConfig");
|
||||||
mutate("getVersion");
|
mutate("getVersion");
|
||||||
}, 100);
|
}, 100);
|
||||||
Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000);
|
// Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { styled, Typography } from "@mui/material";
|
import { styled, Typography, Switch } from "@mui/material";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||||
import { HotkeyInput } from "./hotkey-input";
|
import { HotkeyInput } from "./hotkey-input";
|
||||||
@@ -29,6 +29,9 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge();
|
||||||
|
|
||||||
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
||||||
|
const [enableGlobalHotkey, setEnableHotkey] = useState(
|
||||||
|
verge?.enable_global_hotkey ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
@@ -69,7 +72,10 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchVerge({ hotkeys });
|
await patchVerge({
|
||||||
|
hotkeys,
|
||||||
|
enable_global_hotkey: enableGlobalHotkey,
|
||||||
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
Notice.error(err.message || err.toString());
|
||||||
@@ -80,13 +86,22 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Hotkey Setting")}
|
title={t("Hotkey Setting")}
|
||||||
contentSx={{ width: 450, maxHeight: 330 }}
|
contentSx={{ width: 450, maxHeight: 380 }}
|
||||||
okBtn={t("Save")}
|
okBtn={t("Save")}
|
||||||
cancelBtn={t("Cancel")}
|
cancelBtn={t("Cancel")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onSave}
|
onOk={onSave}
|
||||||
>
|
>
|
||||||
|
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||||
|
<Typography>{t("Enable Global Hotkey")}</Typography>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={enableGlobalHotkey}
|
||||||
|
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ItemWrapper>
|
||||||
|
|
||||||
{HOTKEY_FUNC.map((func) => (
|
{HOTKEY_FUNC.map((func) => (
|
||||||
<ItemWrapper key={func}>
|
<ItemWrapper key={func}>
|
||||||
<Typography>{t(func)}</Typography>
|
<Typography>{t(func)}</Typography>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
open: () => {
|
open: () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setValues({
|
setValues({
|
||||||
stack: clash?.tun.stack ?? "mixed",
|
stack: clash?.tun.stack ?? "gvisor",
|
||||||
device: clash?.tun.device ?? "Mihomo",
|
device: clash?.tun.device ?? "Mihomo",
|
||||||
autoRoute: clash?.tun["auto-route"] ?? true,
|
autoRoute: clash?.tun["auto-route"] ?? true,
|
||||||
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
|
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
|
||||||
@@ -89,7 +89,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
let tun = {
|
let tun = {
|
||||||
stack: "mixed",
|
stack: "gvisor",
|
||||||
device: "Mihomo",
|
device: "Mihomo",
|
||||||
"auto-route": true,
|
"auto-route": true,
|
||||||
"auto-detect-interface": true,
|
"auto-detect-interface": true,
|
||||||
@@ -98,7 +98,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
mtu: 1500,
|
mtu: 1500,
|
||||||
};
|
};
|
||||||
setValues({
|
setValues({
|
||||||
stack: "mixed",
|
stack: "gvisor",
|
||||||
device: "Mihomo",
|
device: "Mihomo",
|
||||||
autoRoute: true,
|
autoRoute: true,
|
||||||
autoDetectInterface: true,
|
autoDetectInterface: true,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsRounded } from "@mui/icons-material";
|
import {
|
||||||
|
SettingsRounded,
|
||||||
|
PlayArrowRounded,
|
||||||
|
PauseRounded,
|
||||||
|
} from "@mui/icons-material";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { DialogRef, Notice, Switch } from "@/components/base";
|
import { DialogRef, Notice, Switch } from "@/components/base";
|
||||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||||
@@ -9,6 +13,7 @@ import { GuardState } from "./mods/guard-state";
|
|||||||
import { SysproxyViewer } from "./mods/sysproxy-viewer";
|
import { SysproxyViewer } from "./mods/sysproxy-viewer";
|
||||||
import { TunViewer } from "./mods/tun-viewer";
|
import { TunViewer } from "./mods/tun-viewer";
|
||||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||||
|
import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
@@ -19,6 +24,9 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
|
|
||||||
|
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
||||||
|
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
const tunRef = useRef<DialogRef>(null);
|
const tunRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
@@ -27,13 +35,25 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
enable_auto_launch,
|
enable_auto_launch,
|
||||||
enable_silent_start,
|
enable_silent_start,
|
||||||
enable_system_proxy,
|
enable_system_proxy,
|
||||||
|
proxy_auto_config,
|
||||||
} = verge ?? {};
|
} = verge ?? {};
|
||||||
|
|
||||||
|
const isProxyEnabled = proxy_auto_config
|
||||||
|
? autoproxy?.enable
|
||||||
|
: sysproxy?.enable;
|
||||||
|
|
||||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false);
|
mutateVerge({ ...verge, ...patch }, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateProxyStatus = async () => {
|
||||||
|
// 等待一小段时间让系统代理状态变化
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
await mutate("getSystemProxy");
|
||||||
|
await mutate("getAutotemProxy");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t("System Setting")}>
|
<SettingList title={t("System Setting")}>
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
<SysproxyViewer ref={sysproxyRef} />
|
||||||
@@ -73,6 +93,11 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
icon={SettingsRounded}
|
icon={SettingsRounded}
|
||||||
onClick={() => sysproxyRef.current?.open()}
|
onClick={() => sysproxyRef.current?.open()}
|
||||||
/>
|
/>
|
||||||
|
{isProxyEnabled ? (
|
||||||
|
<PlayArrowRounded sx={{ color: "success.main", mr: 1 }} />
|
||||||
|
) : (
|
||||||
|
<PauseRounded sx={{ color: "error.main", mr: 1 }} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -82,7 +107,10 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={onSwitchFormat}
|
onFormat={onSwitchFormat}
|
||||||
onChange={(e) => onChangeData({ enable_system_proxy: e })}
|
onChange={(e) => onChangeData({ enable_system_proxy: e })}
|
||||||
onGuard={(e) => patchVerge({ enable_system_proxy: e })}
|
onGuard={async (e) => {
|
||||||
|
await patchVerge({ enable_system_proxy: e });
|
||||||
|
await updateProxyStatus();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Switch edge="end" />
|
<Switch edge="end" />
|
||||||
</GuardState>
|
</GuardState>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import {
|
||||||
|
exitApp,
|
||||||
|
openAppDir,
|
||||||
|
openCoreDir,
|
||||||
|
openLogsDir,
|
||||||
|
openDevTools,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { version } from "@root/package.json";
|
||||||
|
import { DialogRef, Notice } from "@/components/base";
|
||||||
|
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||||
|
import { ConfigViewer } from "./mods/config-viewer";
|
||||||
|
import { HotkeyViewer } from "./mods/hotkey-viewer";
|
||||||
|
import { MiscViewer } from "./mods/misc-viewer";
|
||||||
|
import { ThemeViewer } from "./mods/theme-viewer";
|
||||||
|
import { LayoutViewer } from "./mods/layout-viewer";
|
||||||
|
import { UpdateViewer } from "./mods/update-viewer";
|
||||||
|
import { BackupViewer } from "./mods/backup-viewer";
|
||||||
|
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onError?: (err: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingVergeAdvanced = ({ onError }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||||
|
const configRef = useRef<DialogRef>(null);
|
||||||
|
const hotkeyRef = useRef<DialogRef>(null);
|
||||||
|
const miscRef = useRef<DialogRef>(null);
|
||||||
|
const themeRef = useRef<DialogRef>(null);
|
||||||
|
const layoutRef = useRef<DialogRef>(null);
|
||||||
|
const updateRef = useRef<DialogRef>(null);
|
||||||
|
const backupRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
|
const onCheckUpdate = async () => {
|
||||||
|
try {
|
||||||
|
const info = await checkUpdate();
|
||||||
|
if (!info?.available) {
|
||||||
|
Notice.success(t("Currently on the Latest Version"));
|
||||||
|
} else {
|
||||||
|
updateRef.current?.open();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(err.message || err.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingList title={t("Verge Advanced Setting")}>
|
||||||
|
<ThemeViewer ref={themeRef} />
|
||||||
|
<ConfigViewer ref={configRef} />
|
||||||
|
<HotkeyViewer ref={hotkeyRef} />
|
||||||
|
<MiscViewer ref={miscRef} />
|
||||||
|
<LayoutViewer ref={layoutRef} />
|
||||||
|
<UpdateViewer ref={updateRef} />
|
||||||
|
<BackupViewer ref={backupRef} />
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
onClick={() => backupRef.current?.open()}
|
||||||
|
label={t("Backup Setting")}
|
||||||
|
extra={
|
||||||
|
<TooltipIcon
|
||||||
|
title={t("Backup Setting Info")}
|
||||||
|
sx={{ opacity: "0.7" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
onClick={() => configRef.current?.open()}
|
||||||
|
label={t("Runtime Config")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
onClick={openAppDir}
|
||||||
|
label={t("Open Conf Dir")}
|
||||||
|
extra={
|
||||||
|
<TooltipIcon
|
||||||
|
title={t("Open Conf Dir Info")}
|
||||||
|
sx={{ opacity: "0.7" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
|
||||||
|
|
||||||
|
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
|
||||||
|
|
||||||
|
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
|
||||||
|
|
||||||
|
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
label={t("Lite Mode")}
|
||||||
|
extra={
|
||||||
|
<TooltipIcon title={t("Lite Mode Info")} sx={{ opacity: "0.7" }} />
|
||||||
|
}
|
||||||
|
onClick={() => patchVerge({ enable_lite_mode: true })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
onClick={() => {
|
||||||
|
exitApp();
|
||||||
|
}}
|
||||||
|
label={t("Exit")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingItem label={t("Verge Version")}>
|
||||||
|
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingList>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingVergeAdvanced;
|
||||||
+11
-75
@@ -1,18 +1,9 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
|
import { Button, MenuItem, Select, Input } from "@mui/material";
|
||||||
import {
|
import { copyClashEnv } from "@/services/cmds";
|
||||||
exitApp,
|
|
||||||
openAppDir,
|
|
||||||
openCoreDir,
|
|
||||||
openLogsDir,
|
|
||||||
openDevTools,
|
|
||||||
copyClashEnv,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { version } from "@root/package.json";
|
|
||||||
import { DialogRef, Notice } from "@/components/base";
|
import { DialogRef, Notice } from "@/components/base";
|
||||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||||
import { ThemeModeSwitch } from "./mods/theme-mode-switch";
|
import { ThemeModeSwitch } from "./mods/theme-mode-switch";
|
||||||
@@ -49,7 +40,7 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
|
|||||||
return { code, label: labels[code] };
|
return { code, label: labels[code] };
|
||||||
});
|
});
|
||||||
|
|
||||||
const SettingVerge = ({ onError }: Props) => {
|
const SettingVergeBasic = ({ onError }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||||
@@ -69,30 +60,17 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
const updateRef = useRef<DialogRef>(null);
|
const updateRef = useRef<DialogRef>(null);
|
||||||
const backupRef = useRef<DialogRef>(null);
|
const backupRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: any) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false);
|
mutateVerge({ ...verge, ...patch }, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCheckUpdate = async () => {
|
|
||||||
try {
|
|
||||||
const info = await checkUpdate();
|
|
||||||
if (!info?.available) {
|
|
||||||
Notice.success(t("Currently on the Latest Version"));
|
|
||||||
} else {
|
|
||||||
updateRef.current?.open();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
Notice.error(err.message || err.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyClashEnv = useCallback(async () => {
|
const onCopyClashEnv = useCallback(async () => {
|
||||||
await copyClashEnv();
|
await copyClashEnv();
|
||||||
Notice.success(t("Copy Success"), 1000);
|
Notice.success(t("Copy Success"), 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t("Verge Setting")}>
|
<SettingList title={t("Verge Basic Setting")}>
|
||||||
<ThemeViewer ref={themeRef} />
|
<ThemeViewer ref={themeRef} />
|
||||||
<ConfigViewer ref={configRef} />
|
<ConfigViewer ref={configRef} />
|
||||||
<HotkeyViewer ref={hotkeyRef} />
|
<HotkeyViewer ref={hotkeyRef} />
|
||||||
@@ -181,7 +159,11 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
>
|
>
|
||||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||||
{routers.map((page: { label: string; path: string }) => {
|
{routers.map((page: { label: string; path: string }) => {
|
||||||
return <MenuItem value={page.path}>{t(page.label)}</MenuItem>;
|
return (
|
||||||
|
<MenuItem key={page.path} value={page.path}>
|
||||||
|
{t(page.label)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
@@ -256,54 +238,8 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
onClick={() => hotkeyRef.current?.open()}
|
onClick={() => hotkeyRef.current?.open()}
|
||||||
label={t("Hotkey Setting")}
|
label={t("Hotkey Setting")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
onClick={() => backupRef.current?.open()}
|
|
||||||
label={t("Backup Setting")}
|
|
||||||
extra={
|
|
||||||
<TooltipIcon
|
|
||||||
title={t("Backup Setting Info")}
|
|
||||||
sx={{ opacity: "0.7" }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
onClick={() => configRef.current?.open()}
|
|
||||||
label={t("Runtime Config")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
onClick={openAppDir}
|
|
||||||
label={t("Open Conf Dir")}
|
|
||||||
extra={
|
|
||||||
<TooltipIcon
|
|
||||||
title={t("Open Conf Dir Info")}
|
|
||||||
sx={{ opacity: "0.7" }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
|
|
||||||
|
|
||||||
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
|
|
||||||
|
|
||||||
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
|
|
||||||
|
|
||||||
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
onClick={() => {
|
|
||||||
exitApp();
|
|
||||||
}}
|
|
||||||
label={t("Exit")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem label={t("Verge Version")}>
|
|
||||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
|
||||||
</SettingItem>
|
|
||||||
</SettingList>
|
</SettingList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingVerge;
|
export default SettingVergeBasic;
|
||||||
+19
-25
@@ -34,34 +34,25 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface LogStore {
|
interface LogStore {
|
||||||
logs: Record<LogLevel, ILogItem[]>;
|
logs: ILogItem[];
|
||||||
clearLogs: (level?: LogLevel) => void;
|
clearLogs: () => void;
|
||||||
appendLog: (level: LogLevel, log: ILogItem) => void;
|
appendLog: (log: ILogItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useLogStore = create<LogStore>(
|
const useLogStore = create<LogStore>(
|
||||||
(set: (fn: (state: LogStore) => Partial<LogStore>) => void) => ({
|
(set: (fn: (state: LogStore) => Partial<LogStore>) => void) => ({
|
||||||
logs: {
|
logs: [],
|
||||||
warning: [],
|
clearLogs: () =>
|
||||||
info: [],
|
set(() => ({
|
||||||
debug: [],
|
logs: [],
|
||||||
error: [],
|
|
||||||
all: [],
|
|
||||||
},
|
|
||||||
clearLogs: (level?: LogLevel) =>
|
|
||||||
set((state: LogStore) => ({
|
|
||||||
logs: level
|
|
||||||
? { ...state.logs, [level]: [] }
|
|
||||||
: { warning: [], info: [], debug: [], error: [], all: [] },
|
|
||||||
})),
|
})),
|
||||||
appendLog: (level: LogLevel, log: ILogItem) =>
|
appendLog: (log: ILogItem) =>
|
||||||
set((state: LogStore) => {
|
set((state: LogStore) => {
|
||||||
const currentLogs = state.logs[level];
|
|
||||||
const newLogs =
|
const newLogs =
|
||||||
currentLogs.length >= MAX_LOG_NUM
|
state.logs.length >= MAX_LOG_NUM
|
||||||
? [...currentLogs.slice(1), log]
|
? [...state.logs.slice(1), log]
|
||||||
: [...currentLogs, log];
|
: [...state.logs, log];
|
||||||
return { logs: { ...state.logs, [level]: newLogs } };
|
return { logs: newLogs };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -84,7 +75,7 @@ export const useLogData = (logLevel: LogLevel) => {
|
|||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
const data = JSON.parse(event.data) as ILogItem;
|
const data = JSON.parse(event.data) as ILogItem;
|
||||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||||
appendLog(logLevel, { ...data, time });
|
appendLog({ ...data, time });
|
||||||
},
|
},
|
||||||
onerror() {
|
onerror() {
|
||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
@@ -98,10 +89,13 @@ export const useLogData = (logLevel: LogLevel) => {
|
|||||||
};
|
};
|
||||||
}, [clashInfo, enableLog, logLevel]);
|
}, [clashInfo, enableLog, logLevel]);
|
||||||
|
|
||||||
return logs[logLevel];
|
// 根据当前选择的日志等级过滤日志
|
||||||
|
return logLevel === "all"
|
||||||
|
? logs
|
||||||
|
: logs.filter((log) => log.type.toLowerCase() === logLevel);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出清空日志的方法
|
// 导出清空日志的方法
|
||||||
export const clearLogs = (level?: LogLevel) => {
|
export const clearLogs = () => {
|
||||||
useLogStore.getState().clearLogs(level);
|
useLogStore.getState().clearLogs();
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-3
@@ -178,7 +178,8 @@
|
|||||||
"Process": "عملية",
|
"Process": "عملية",
|
||||||
"Time": "الوقت",
|
"Time": "الوقت",
|
||||||
"Source": "المصدر",
|
"Source": "المصدر",
|
||||||
"Destination IP": "عنوان IP الوجهة",
|
"Destination": "عنوان IP الوجهة",
|
||||||
|
"DestinationPort": "ميناء الوجهة",
|
||||||
"Close Connection": "إغلاق الاتصال",
|
"Close Connection": "إغلاق الاتصال",
|
||||||
"Rules": "القواعد",
|
"Rules": "القواعد",
|
||||||
"Rule Provider": "مزود القواعد",
|
"Rule Provider": "مزود القواعد",
|
||||||
@@ -326,6 +327,7 @@
|
|||||||
"Default Latency Test Info": "يُستخدم فقط لاختبار طلب HTTP العميل. لن يؤثر على ملف التكوين",
|
"Default Latency Test Info": "يُستخدم فقط لاختبار طلب HTTP العميل. لن يؤثر على ملف التكوين",
|
||||||
"Default Latency Timeout": "مهلة التأخير الافتراضية",
|
"Default Latency Timeout": "مهلة التأخير الافتراضية",
|
||||||
"Hotkey Setting": "إعدادات الاختصارات",
|
"Hotkey Setting": "إعدادات الاختصارات",
|
||||||
|
"Enable Global Hotkey": "تمكين مفتاح التشغيل السريع العالمي",
|
||||||
"open_or_close_dashboard": "فتح/إغلاق لوحة التحكم",
|
"open_or_close_dashboard": "فتح/إغلاق لوحة التحكم",
|
||||||
"clash_mode_rule": "وضع القواعد",
|
"clash_mode_rule": "وضع القواعد",
|
||||||
"clash_mode_global": "الوضع العالمي",
|
"clash_mode_global": "الوضع العالمي",
|
||||||
@@ -368,7 +370,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "تم منح الأذونات بنجاح لـ {{core}} Core",
|
"Permissions Granted Successfully for _clash Core": "تم منح الأذونات بنجاح لـ {{core}} Core",
|
||||||
"Core Version Updated": "تم تحديث إصدار النواة",
|
"Core Version Updated": "تم تحديث إصدار النواة",
|
||||||
"Clash Core Restarted": "تم إعادة تشغيل نواة Clash",
|
"Clash Core Restarted": "تم إعادة تشغيل نواة Clash",
|
||||||
"Switched to _clash Core": "تم التبديل إلى {{core}} Core",
|
|
||||||
"GeoData Updated": "تم تحديث البيانات الجغرافية",
|
"GeoData Updated": "تم تحديث البيانات الجغرافية",
|
||||||
"Currently on the Latest Version": "أنت على أحدث إصدار حاليًا",
|
"Currently on the Latest Version": "أنت على أحدث إصدار حاليًا",
|
||||||
"Import Subscription Successful": "تم استيراد الاشتراك بنجاح",
|
"Import Subscription Successful": "تم استيراد الاشتراك بنجاح",
|
||||||
@@ -434,5 +435,19 @@
|
|||||||
"Rule Mode": "وضع القواعد",
|
"Rule Mode": "وضع القواعد",
|
||||||
"Global Mode": "الوضع العالمي",
|
"Global Mode": "الوضع العالمي",
|
||||||
"Direct Mode": "الوضع المباشر",
|
"Direct Mode": "الوضع المباشر",
|
||||||
"Enable Tray Speed": "تفعيل سرعة التراي"
|
"Enable Tray Speed": "تفعيل سرعة التراي",
|
||||||
|
"Lite Mode": "وضع الأداء الخفيف",
|
||||||
|
"Lite Mode Info": "إيقاف الواجهة الرسومية والإبقاء على تشغيل النواة",
|
||||||
|
"Config Validation Failed": "فشل التحقق من تكوين الاشتراك، يرجى فحص ملف التكوين، تم التراجع عن التغييرات، تفاصيل الخطأ:",
|
||||||
|
"Boot Config Validation Failed": "فشل التحقق من التكوين عند الإقلاع، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:",
|
||||||
|
"Core Change Config Validation Failed": "فشل التحقق من التكوين عند تغيير النواة، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:",
|
||||||
|
"Config Validation Process Terminated": "تم إنهاء عملية التحقق",
|
||||||
|
"Script Syntax Error": "خطأ في بناء جملة السكريبت، تم التراجع عن التغييرات",
|
||||||
|
"Script Missing Main": "خطأ في السكريبت، تم التراجع عن التغييرات",
|
||||||
|
"File Not Found": "الملف غير موجود، تم التراجع عن التغييرات",
|
||||||
|
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
|
||||||
|
"Core Changed Successfully": "تم تغيير النواة بنجاح",
|
||||||
|
"Failed to Change Core": "فشل تغيير النواة",
|
||||||
|
"Verge Basic Setting": "الإعدادات الأساسية Verge",
|
||||||
|
"Verge Advanced Setting": "الإعدادات الأساسية Verge"
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-3
@@ -178,7 +178,8 @@
|
|||||||
"Process": "Process",
|
"Process": "Process",
|
||||||
"Time": "Time",
|
"Time": "Time",
|
||||||
"Source": "Source",
|
"Source": "Source",
|
||||||
"Destination IP": "Destination IP",
|
"Destination": "Destination",
|
||||||
|
"DestinationPort": "Destination Port",
|
||||||
"Close Connection": "Close Connection",
|
"Close Connection": "Close Connection",
|
||||||
"Rules": "Rules",
|
"Rules": "Rules",
|
||||||
"Rule Provider": "Rule Provider",
|
"Rule Provider": "Rule Provider",
|
||||||
@@ -276,7 +277,8 @@
|
|||||||
"Open UWP tool": "Open UWP tool",
|
"Open UWP tool": "Open UWP tool",
|
||||||
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
|
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
|
||||||
"Update GeoData": "Update GeoData",
|
"Update GeoData": "Update GeoData",
|
||||||
"Verge Setting": "Verge Setting",
|
"Verge Basic Setting": "Verge Basic Setting",
|
||||||
|
"Verge Advanced Setting": "Verge Advanced Setting",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
"Theme Mode": "Theme Mode",
|
"Theme Mode": "Theme Mode",
|
||||||
"theme.light": "Light",
|
"theme.light": "Light",
|
||||||
@@ -328,6 +330,7 @@
|
|||||||
"Default Latency Test Info": "Used for HTTP client request testing only and won't make a difference to the configuration file",
|
"Default Latency Test Info": "Used for HTTP client request testing only and won't make a difference to the configuration file",
|
||||||
"Default Latency Timeout": "Default Latency Timeout",
|
"Default Latency Timeout": "Default Latency Timeout",
|
||||||
"Hotkey Setting": "Hotkey Setting",
|
"Hotkey Setting": "Hotkey Setting",
|
||||||
|
"Enable Global Hotkey": "Enable Global Hotkey",
|
||||||
"open_or_close_dashboard": "Open/Close Dashboard",
|
"open_or_close_dashboard": "Open/Close Dashboard",
|
||||||
"clash_mode_rule": "Rule Mode",
|
"clash_mode_rule": "Rule Mode",
|
||||||
"clash_mode_global": "Global Mode",
|
"clash_mode_global": "Global Mode",
|
||||||
@@ -437,5 +440,30 @@
|
|||||||
"Rule Mode": "Rule Mode",
|
"Rule Mode": "Rule Mode",
|
||||||
"Global Mode": "Global Mode",
|
"Global Mode": "Global Mode",
|
||||||
"Direct Mode": "Direct Mode",
|
"Direct Mode": "Direct Mode",
|
||||||
"Enable Tray Speed": "Enable Tray Speed"
|
"Enable Tray Speed": "Enable Tray Speed",
|
||||||
|
"Lite Mode": "Lightweight Mode",
|
||||||
|
"Lite Mode Info": "Close the GUI and keep only the kernel running",
|
||||||
|
"Config Validation Failed": "Subscription configuration validation failed. Please check the subscription configuration file; modifications have been rolled back.",
|
||||||
|
"Boot Config Validation Failed": "Boot subscription configuration validation failed. Started with the default configuration; please check the subscription configuration file.",
|
||||||
|
"Core Change Config Validation Failed": "Configuration validation failed when switching the kernel. Started with the default configuration; please check the subscription configuration file.",
|
||||||
|
"Config Validation Process Terminated": "The validation process has been terminated.",
|
||||||
|
"Script Syntax Error": "Script syntax error, changes reverted",
|
||||||
|
"Script Missing Main": "Script error, changes reverted",
|
||||||
|
"File Not Found": "File missing, changes reverted",
|
||||||
|
"Script File Error": "Script file error, changes reverted",
|
||||||
|
"Core Changed Successfully": "Core changed successfully",
|
||||||
|
"Failed to Change Core": "Failed to change core",
|
||||||
|
"YAML Syntax Error": "YAML syntax error, changes reverted",
|
||||||
|
"YAML Read Error": "YAML read error, changes reverted",
|
||||||
|
"YAML Mapping Error": "YAML mapping error, changes reverted",
|
||||||
|
"YAML Key Error": "YAML key error, changes reverted",
|
||||||
|
"YAML Error": "YAML error, changes reverted",
|
||||||
|
"Merge File Syntax Error": "Merge file syntax error, changes reverted",
|
||||||
|
"Merge File Mapping Error": "Merge file mapping error, changes reverted",
|
||||||
|
"Merge File Key Error": "Merge file key error, changes reverted",
|
||||||
|
"Merge File Error": "Merge file error, changes reverted",
|
||||||
|
"Validate YAML File": "Validate YAML File",
|
||||||
|
"Validate Merge File": "Validate Merge File",
|
||||||
|
"Validation Success": "Validation Success",
|
||||||
|
"Validation Failed": "Validation Failed"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -178,7 +178,8 @@
|
|||||||
"Process": "فرآیند",
|
"Process": "فرآیند",
|
||||||
"Time": "زمان",
|
"Time": "زمان",
|
||||||
"Source": "منبع",
|
"Source": "منبع",
|
||||||
"Destination IP": "آدرس IP مقصد",
|
"Destination": "آدرس IP مقصد",
|
||||||
|
"DestinationPort": "بندر هدف",
|
||||||
"Close Connection": "بستن اتصال",
|
"Close Connection": "بستن اتصال",
|
||||||
"Rules": "قوانین",
|
"Rules": "قوانین",
|
||||||
"Rule Provider": "تأمینکننده قانون",
|
"Rule Provider": "تأمینکننده قانون",
|
||||||
@@ -328,6 +329,7 @@
|
|||||||
"Default Latency Test Info": "فقط برای تست درخواستهای کلاینت HTTP استفاده میشود و بر فایل پیکربندی تأثیری نخواهد داشت",
|
"Default Latency Test Info": "فقط برای تست درخواستهای کلاینت HTTP استفاده میشود و بر فایل پیکربندی تأثیری نخواهد داشت",
|
||||||
"Default Latency Timeout": "زمان انتظار تأخیر پیشفرض",
|
"Default Latency Timeout": "زمان انتظار تأخیر پیشفرض",
|
||||||
"Hotkey Setting": "تنظیمات کلیدهای میانبر",
|
"Hotkey Setting": "تنظیمات کلیدهای میانبر",
|
||||||
|
"Enable Global Hotkey": "فعال کردن کلید میانبر سراسری",
|
||||||
"open_or_close_dashboard": "باز/بستن داشبورد",
|
"open_or_close_dashboard": "باز/بستن داشبورد",
|
||||||
"clash_mode_rule": "حالت قانون",
|
"clash_mode_rule": "حالت قانون",
|
||||||
"clash_mode_global": "حالت جهانی",
|
"clash_mode_global": "حالت جهانی",
|
||||||
@@ -371,7 +373,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد",
|
"Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد",
|
||||||
"Core Version Updated": "نسخه هسته بهروزرسانی شد",
|
"Core Version Updated": "نسخه هسته بهروزرسانی شد",
|
||||||
"Clash Core Restarted": "هسته Clash مجدداً راهاندازی شد",
|
"Clash Core Restarted": "هسته Clash مجدداً راهاندازی شد",
|
||||||
"Switched to _clash Core": "تغییر به هسته {{core}}",
|
|
||||||
"GeoData Updated": "GeoData بهروزرسانی شد",
|
"GeoData Updated": "GeoData بهروزرسانی شد",
|
||||||
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
|
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
|
||||||
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
|
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
|
||||||
@@ -431,5 +432,19 @@
|
|||||||
"Rule Mode": "حالت قوانین",
|
"Rule Mode": "حالت قوانین",
|
||||||
"Global Mode": "حالت جهانی",
|
"Global Mode": "حالت جهانی",
|
||||||
"Direct Mode": "حالت مستقیم",
|
"Direct Mode": "حالت مستقیم",
|
||||||
"Enable Tray Speed": "فعال کردن سرعت ترای"
|
"Enable Tray Speed": "فعال کردن سرعت ترای",
|
||||||
|
"Lite Mode": "در فارسی",
|
||||||
|
"Lite Mode Info": "رابط کاربری گرافیکی را ببندید و فقط هسته را در حال اجرا نگه دارید",
|
||||||
|
"Config Validation Failed": "اعتبارسنجی پیکربندی اشتراک ناموفق بود، فایل پیکربندی را بررسی کنید، تغییرات برگشت داده شد، جزئیات خطا:",
|
||||||
|
"Boot Config Validation Failed": "اعتبارسنجی پیکربندی هنگام راهاندازی ناموفق بود، پیکربندی پیشفرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:",
|
||||||
|
"Core Change Config Validation Failed": "اعتبارسنجی پیکربندی هنگام تغییر هسته ناموفق بود، پیکربندی پیشفرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:",
|
||||||
|
"Config Validation Process Terminated": "فرآیند اعتبارسنجی متوقف شد",
|
||||||
|
"Script Syntax Error": "خطای نحوی اسکریپت، تغییرات برگشت داده شد",
|
||||||
|
"Script Missing Main": "خطای اسکریپت، تغییرات برگشت داده شد",
|
||||||
|
"File Not Found": "فایل یافت نشد، تغییرات برگشت داده شد",
|
||||||
|
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
|
||||||
|
"Core Changed Successfully": "هسته با موفقیت تغییر کرد",
|
||||||
|
"Failed to Change Core": "تغییر هسته ناموفق بود",
|
||||||
|
"Verge Basic Setting": "تنظیمات پایه Verge",
|
||||||
|
"Verge Advanced Setting": "تنظیمات پیشرفته Verge"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -210,7 +210,8 @@
|
|||||||
"Process": "Proses",
|
"Process": "Proses",
|
||||||
"Time": "Waktu",
|
"Time": "Waktu",
|
||||||
"Source": "Sumber",
|
"Source": "Sumber",
|
||||||
"Destination IP": "IP Tujuan",
|
"Destination": "IP Tujuan",
|
||||||
|
"DestinationPort": "Port Tujuan",
|
||||||
"Close Connection": "Tutup Koneksi",
|
"Close Connection": "Tutup Koneksi",
|
||||||
"Rules": "Aturan",
|
"Rules": "Aturan",
|
||||||
"Rule Provider": "Penyedia Aturan",
|
"Rule Provider": "Penyedia Aturan",
|
||||||
@@ -359,6 +360,7 @@
|
|||||||
"Default Latency Test Info": "Digunakan hanya untuk pengujian permintaan klien HTTP dan tidak akan mempengaruhi file konfigurasi",
|
"Default Latency Test Info": "Digunakan hanya untuk pengujian permintaan klien HTTP dan tidak akan mempengaruhi file konfigurasi",
|
||||||
"Default Latency Timeout": "Waktu Habis Latensi Default",
|
"Default Latency Timeout": "Waktu Habis Latensi Default",
|
||||||
"Hotkey Setting": "Pengaturan Pintasan",
|
"Hotkey Setting": "Pengaturan Pintasan",
|
||||||
|
"Enable Global Hotkey": "Aktifkan Hotkey Global",
|
||||||
"open_or_close_dashboard": "Buka/Tutup Dasbor",
|
"open_or_close_dashboard": "Buka/Tutup Dasbor",
|
||||||
"clash_mode_rule": "Mode Aturan",
|
"clash_mode_rule": "Mode Aturan",
|
||||||
"clash_mode_global": "Mode Global",
|
"clash_mode_global": "Mode Global",
|
||||||
@@ -402,7 +404,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "Izin Berhasil Diberikan untuk Core {{core}}",
|
"Permissions Granted Successfully for _clash Core": "Izin Berhasil Diberikan untuk Core {{core}}",
|
||||||
"Core Version Updated": "Versi Core Diperbarui",
|
"Core Version Updated": "Versi Core Diperbarui",
|
||||||
"Clash Core Restarted": "Core Clash Dimulai Ulang",
|
"Clash Core Restarted": "Core Clash Dimulai Ulang",
|
||||||
"Switched to _clash Core": "Beralih ke Core {{core}}",
|
|
||||||
"GeoData Updated": "GeoData Diperbarui",
|
"GeoData Updated": "GeoData Diperbarui",
|
||||||
"Currently on the Latest Version": "Saat ini pada Versi Terbaru",
|
"Currently on the Latest Version": "Saat ini pada Versi Terbaru",
|
||||||
"Import Subscription Successful": "Berlangganan Berhasil Diimpor",
|
"Import Subscription Successful": "Berlangganan Berhasil Diimpor",
|
||||||
@@ -430,5 +431,19 @@
|
|||||||
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
|
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
|
||||||
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
||||||
"Failed to fetch backup files": "Gagal mengambil file cadangan",
|
"Failed to fetch backup files": "Gagal mengambil file cadangan",
|
||||||
"Enable Tray Speed": "Aktifkan Tray Speed"
|
"Enable Tray Speed": "Aktifkan Tray Speed",
|
||||||
|
"Lite Mode": "Mode Ringan",
|
||||||
|
"Lite Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan",
|
||||||
|
"Config Validation Failed": "Validasi konfigurasi langganan gagal, periksa file konfigurasi, perubahan dibatalkan, detail kesalahan:",
|
||||||
|
"Boot Config Validation Failed": "Validasi konfigurasi saat boot gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:",
|
||||||
|
"Core Change Config Validation Failed": "Validasi konfigurasi saat ganti inti gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:",
|
||||||
|
"Config Validation Process Terminated": "Proses validasi dihentikan",
|
||||||
|
"Script Syntax Error": "Kesalahan sintaks skrip, perubahan dibatalkan",
|
||||||
|
"Script Missing Main": "Kesalahan skrip, perubahan dibatalkan",
|
||||||
|
"File Not Found": "File tidak ditemukan, perubahan dibatalkan",
|
||||||
|
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
|
||||||
|
"Core Changed Successfully": "Inti berhasil diubah",
|
||||||
|
"Failed to Change Core": "Gagal mengubah inti",
|
||||||
|
"Verge Basic Setting": "Pengaturan Dasar Verge",
|
||||||
|
"Verge Advanced Setting": "Pengaturan Lanjutan Verge"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -178,7 +178,8 @@
|
|||||||
"Process": "Процесс",
|
"Process": "Процесс",
|
||||||
"Time": "Время подключения",
|
"Time": "Время подключения",
|
||||||
"Source": "Исходный адрес",
|
"Source": "Исходный адрес",
|
||||||
"Destination IP": "IP-адрес назначения",
|
"Destination": "IP-адрес назначения",
|
||||||
|
"DestinationPort": "Целевой порт",
|
||||||
"Close Connection": "Закрыть соединение",
|
"Close Connection": "Закрыть соединение",
|
||||||
"Rules": "Правила",
|
"Rules": "Правила",
|
||||||
"Rule Provider": "Провайдер правило",
|
"Rule Provider": "Провайдер правило",
|
||||||
@@ -328,6 +329,7 @@
|
|||||||
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
||||||
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
||||||
"Hotkey Setting": "Настройки клавиатурных сокращений",
|
"Hotkey Setting": "Настройки клавиатурных сокращений",
|
||||||
|
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
|
||||||
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
||||||
"clash_mode_rule": "Режим правил",
|
"clash_mode_rule": "Режим правил",
|
||||||
"clash_mode_global": "Глобальный режим",
|
"clash_mode_global": "Глобальный режим",
|
||||||
@@ -371,7 +373,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
|
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
|
||||||
"Core Version Updated": "Обновлена версия ядра",
|
"Core Version Updated": "Обновлена версия ядра",
|
||||||
"Clash Core Restarted": "Clash ядра перезапущено",
|
"Clash Core Restarted": "Clash ядра перезапущено",
|
||||||
"Switched to _clash Core": "Переключено на ядра {{core}}",
|
|
||||||
"GeoData Updated": "GeoData Обновлена",
|
"GeoData Updated": "GeoData Обновлена",
|
||||||
"Currently on the Latest Version": "В настоящее время используется последняя версия",
|
"Currently on the Latest Version": "В настоящее время используется последняя версия",
|
||||||
"Import subscription successful": "Импорт подписки успешно",
|
"Import subscription successful": "Импорт подписки успешно",
|
||||||
@@ -431,5 +432,19 @@
|
|||||||
"Rule Mode": "Режим правил",
|
"Rule Mode": "Режим правил",
|
||||||
"Global Mode": "Глобальный режим",
|
"Global Mode": "Глобальный режим",
|
||||||
"Direct Mode": "Прямой режим",
|
"Direct Mode": "Прямой режим",
|
||||||
"Enable Tray Speed": "Включить скорость в лотке"
|
"Enable Tray Speed": "Включить скорость в лотке",
|
||||||
|
"Lite Mode": "Облегченный режим",
|
||||||
|
"Lite Mode Info": "Закройте графический интерфейс и оставьте работать только ядро",
|
||||||
|
"Config Validation Failed": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:",
|
||||||
|
"Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||||
|
"Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||||
|
"Config Validation Process Terminated": "Процесс проверки прерван",
|
||||||
|
"Script Syntax Error": "Ошибка синтаксиса скрипта, изменения отменены",
|
||||||
|
"Script Missing Main": "Ошибка скрипта, изменения отменены",
|
||||||
|
"File Not Found": "Файл не найден, изменения отменены",
|
||||||
|
"Script File Error": "Ошибка файла скрипта, изменения отменены",
|
||||||
|
"Core Changed Successfully": "Ядро успешно сменено",
|
||||||
|
"Failed to Change Core": "Не удалось сменить ядро",
|
||||||
|
"Verge Basic Setting": "Основные настройки Verge",
|
||||||
|
"Verge Advanced Setting": "Расширенные настройки Verge"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -178,7 +178,8 @@
|
|||||||
"Process": "Процесс",
|
"Process": "Процесс",
|
||||||
"Time": "Тоташу вакыты",
|
"Time": "Тоташу вакыты",
|
||||||
"Source": "Чыганак адресы",
|
"Source": "Чыганак адресы",
|
||||||
"Destination IP": "Максат IP-адресы",
|
"Destination": "Максат IP-адресы",
|
||||||
|
"DestinationPort": "Барасы порты",
|
||||||
"Close Connection": "Тоташуны ябу",
|
"Close Connection": "Тоташуны ябу",
|
||||||
"Rules": "Кагыйдәләр",
|
"Rules": "Кагыйдәләр",
|
||||||
"Rule Provider": "Кагыйдә провайдеры",
|
"Rule Provider": "Кагыйдә провайдеры",
|
||||||
@@ -327,6 +328,7 @@
|
|||||||
"Default Latency Test Info": "Бу фәкать клиентның HTTP сораулары тесты өчен кулланыла, конфигурация файлына йогынты ясамый",
|
"Default Latency Test Info": "Бу фәкать клиентның HTTP сораулары тесты өчен кулланыла, конфигурация файлына йогынты ясамый",
|
||||||
"Default Latency Timeout": "Тоткарлануның стандарт таймауты",
|
"Default Latency Timeout": "Тоткарлануның стандарт таймауты",
|
||||||
"Hotkey Setting": "Клавиатура төймәләре (hotkey) көйләүләре",
|
"Hotkey Setting": "Клавиатура төймәләре (hotkey) көйләүләре",
|
||||||
|
"Enable Global Hotkey": "Глобаль Хоткейны кушу",
|
||||||
"open_or_close_dashboard": "Панельне ачу/ябу",
|
"open_or_close_dashboard": "Панельне ачу/ябу",
|
||||||
"clash_mode_rule": "Кагыйдәләр режимы",
|
"clash_mode_rule": "Кагыйдәләр режимы",
|
||||||
"clash_mode_global": "Глобаль режим",
|
"clash_mode_global": "Глобаль режим",
|
||||||
@@ -370,7 +372,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "{{core}} ядросы өчен рөхсәтләр бирелде",
|
"Permissions Granted Successfully for _clash Core": "{{core}} ядросы өчен рөхсәтләр бирелде",
|
||||||
"Core Version Updated": "Ядро версиясе яңартылды",
|
"Core Version Updated": "Ядро версиясе яңартылды",
|
||||||
"Clash Core Restarted": "Clash ядросы яңадан башланды",
|
"Clash Core Restarted": "Clash ядросы яңадан башланды",
|
||||||
"Switched to _clash Core": "{{core}} ядросына күчү башкарылды",
|
|
||||||
"GeoData Updated": "GeoData яңартылды",
|
"GeoData Updated": "GeoData яңартылды",
|
||||||
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
|
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
|
||||||
"Import subscription successful": "Подписка уңышлы импортланды",
|
"Import subscription successful": "Подписка уңышлы импортланды",
|
||||||
@@ -430,5 +431,19 @@
|
|||||||
"Rule Mode": "Кагыйдә режимы",
|
"Rule Mode": "Кагыйдә режимы",
|
||||||
"Global Mode": "Глобаль режим",
|
"Global Mode": "Глобаль режим",
|
||||||
"Direct Mode": "Туры режим",
|
"Direct Mode": "Туры режим",
|
||||||
"Enable Tray Speed": "Трей скоростьне үстерү"
|
"Enable Tray Speed": "Трей скоростьне үстерү",
|
||||||
|
"Lite Mode": "Җиңел Режим",
|
||||||
|
"Lite Mode Info": "GUI-ны ябыгыз һәм бары тик төшне генә эшләтеп калдырыгыз",
|
||||||
|
"Config Validation Failed": "Язылу көйләү тикшерүе уңышсыз, көйләү файлын тикшерегез, үзгәрешләр кире кайтарылды, хата:",
|
||||||
|
"Boot Config Validation Failed": "Йөкләү вакытында көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:",
|
||||||
|
"Core Change Config Validation Failed": "Ядро алыштырганда көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:",
|
||||||
|
"Config Validation Process Terminated": "Тикшерү процессы туктатылды",
|
||||||
|
"Script Syntax Error": "Скрипт синтаксик хатасы, үзгәрешләр кире кайтарылды",
|
||||||
|
"Script Missing Main": "Скрипт хатасы, үзгәрешләр кире кайтарылды",
|
||||||
|
"File Not Found": "Файл табылмады, үзгәрешләр кире кайтарылды",
|
||||||
|
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
|
||||||
|
"Core Changed Successfully": "Ядро уңышлы алыштырылды",
|
||||||
|
"Failed to Change Core": "Ядро алыштыру уңышсыз булды",
|
||||||
|
"Verge Basic Setting": "Verge Төп көйләүләр",
|
||||||
|
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр"
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-5
@@ -135,9 +135,9 @@
|
|||||||
"Hidden": "隐藏代理组",
|
"Hidden": "隐藏代理组",
|
||||||
"Group Name Required": "代理组名称不能为空",
|
"Group Name Required": "代理组名称不能为空",
|
||||||
"Group Name Already Exists": "代理组名称已存在",
|
"Group Name Already Exists": "代理组名称已存在",
|
||||||
"Extend Config": "扩展配置",
|
"Extend Config": "扩展覆写配置",
|
||||||
"Extend Script": "扩展脚本",
|
"Extend Script": "扩展脚本",
|
||||||
"Global Merge": "全局扩展配置",
|
"Global Merge": "全局扩展覆写配置",
|
||||||
"Global Script": "全局扩展脚本",
|
"Global Script": "全局扩展脚本",
|
||||||
"Type": "类型",
|
"Type": "类型",
|
||||||
"Name": "名称",
|
"Name": "名称",
|
||||||
@@ -178,7 +178,8 @@
|
|||||||
"Process": "进程",
|
"Process": "进程",
|
||||||
"Time": "连接时间",
|
"Time": "连接时间",
|
||||||
"Source": "源地址",
|
"Source": "源地址",
|
||||||
"Destination IP": "目标地址",
|
"Destination": "目标地址",
|
||||||
|
"DestinationPort": "目标端口",
|
||||||
"Close Connection": "关闭连接",
|
"Close Connection": "关闭连接",
|
||||||
"Rules": "规则",
|
"Rules": "规则",
|
||||||
"Rule Provider": "规则集合",
|
"Rule Provider": "规则集合",
|
||||||
@@ -328,6 +329,7 @@
|
|||||||
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
|
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
|
||||||
"Default Latency Timeout": "测试超时时间",
|
"Default Latency Timeout": "测试超时时间",
|
||||||
"Hotkey Setting": "热键设置",
|
"Hotkey Setting": "热键设置",
|
||||||
|
"Enable Global Hotkey": "启用全局热键",
|
||||||
"open_or_close_dashboard": "打开/关闭面板",
|
"open_or_close_dashboard": "打开/关闭面板",
|
||||||
"clash_mode_rule": "规则模式",
|
"clash_mode_rule": "规则模式",
|
||||||
"clash_mode_global": "全局模式",
|
"clash_mode_global": "全局模式",
|
||||||
@@ -371,7 +373,6 @@
|
|||||||
"Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功",
|
"Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功",
|
||||||
"Core Version Updated": "内核版本已更新",
|
"Core Version Updated": "内核版本已更新",
|
||||||
"Clash Core Restarted": "已重启 Clash 内核",
|
"Clash Core Restarted": "已重启 Clash 内核",
|
||||||
"Switched to _clash Core": "已切换至 {{core}} 内核",
|
|
||||||
"GeoData Updated": "已更新 GeoData",
|
"GeoData Updated": "已更新 GeoData",
|
||||||
"Currently on the Latest Version": "当前已是最新版本",
|
"Currently on the Latest Version": "当前已是最新版本",
|
||||||
"Import Subscription Successful": "导入订阅成功",
|
"Import Subscription Successful": "导入订阅成功",
|
||||||
@@ -431,5 +432,32 @@
|
|||||||
"Rule Mode": "规则模式",
|
"Rule Mode": "规则模式",
|
||||||
"Global Mode": "全局模式",
|
"Global Mode": "全局模式",
|
||||||
"Direct Mode": "直连模式",
|
"Direct Mode": "直连模式",
|
||||||
"Enable Tray Speed": "启用托盘速率"
|
"Enable Tray Speed": "启用托盘速率",
|
||||||
|
"Lite Mode": "轻量模式",
|
||||||
|
"Lite Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||||
|
"Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,变更已撤销,错误详情:",
|
||||||
|
"Boot Config Validation Failed": "启动订阅配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:",
|
||||||
|
"Core Change Config Validation Failed": "切换内核时配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:",
|
||||||
|
"Config Validation Process Terminated": "验证进程被终止",
|
||||||
|
"Script Syntax Error": "脚本语法错误,变更已撤销",
|
||||||
|
"Script Missing Main": "脚本错误,变更已撤销",
|
||||||
|
"File Not Found": "文件丢失,变更已撤销",
|
||||||
|
"Script File Error": "脚本文件错误,变更已撤销",
|
||||||
|
"Core Changed Successfully": "内核切换成功",
|
||||||
|
"Failed to Change Core": "无法切换内核",
|
||||||
|
"YAML Syntax Error": "YAML语法错误,变更已撤销",
|
||||||
|
"YAML Read Error": "YAML读取错误,变更已撤销",
|
||||||
|
"YAML Mapping Error": "YAML映射错误,变更已撤销",
|
||||||
|
"YAML Key Error": "YAML键错误,变更已撤销",
|
||||||
|
"YAML Error": "YAML错误,变更已撤销",
|
||||||
|
"Merge File Syntax Error": "覆写文件语法错误,变更已撤销",
|
||||||
|
"Merge File Mapping Error": "覆写文件映射错误,变更已撤销",
|
||||||
|
"Merge File Key Error": "覆写文件键错误,变更已撤销",
|
||||||
|
"Merge File Error": "覆写文件错误,变更已撤销",
|
||||||
|
"Validate YAML File": "验证YAML文件",
|
||||||
|
"Validate Merge File": "验证覆写文件",
|
||||||
|
"Validation Success": "验证成功",
|
||||||
|
"Validation Failed": "验证失败",
|
||||||
|
"Verge Basic Setting": "Verge 基础设置",
|
||||||
|
"Verge Advanced Setting": "Verge 高级设置"
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-67
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||||||
import i18next from "i18next";
|
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 } from "react";
|
import { useEffect, useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
|
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
|
||||||
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
|
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
|
||||||
@@ -27,6 +27,7 @@ import { getPortableFlag } from "@/services/cmds";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
||||||
import { useListen } from "@/hooks/use-listen";
|
import { useListen } from "@/hooks/use-listen";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow();
|
const appWindow = getCurrentWebviewWindow();
|
||||||
export let portableFlag = false;
|
export let portableFlag = false;
|
||||||
@@ -35,73 +36,178 @@ dayjs.extend(relativeTime);
|
|||||||
|
|
||||||
const OS = getSystem();
|
const OS = getSystem();
|
||||||
|
|
||||||
|
// 通知处理函数
|
||||||
|
const handleNoticeMessage = (
|
||||||
|
status: string,
|
||||||
|
msg: string,
|
||||||
|
t: (key: string) => string,
|
||||||
|
navigate: (path: string, options?: any) => void,
|
||||||
|
) => {
|
||||||
|
console.log("[通知监听] 收到消息:", status, msg);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "import_sub_url::ok":
|
||||||
|
navigate("/profile", { state: { current: msg } });
|
||||||
|
Notice.success(t("Import Subscription Successful"));
|
||||||
|
break;
|
||||||
|
case "import_sub_url::error":
|
||||||
|
navigate("/profile");
|
||||||
|
Notice.error(msg);
|
||||||
|
break;
|
||||||
|
case "set_config::error":
|
||||||
|
Notice.error(msg);
|
||||||
|
break;
|
||||||
|
case "config_validate::boot_error":
|
||||||
|
Notice.error(`${t("Boot Config Validation Failed")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::core_change":
|
||||||
|
Notice.error(`${t("Core Change Config Validation Failed")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::error":
|
||||||
|
Notice.error(`${t("Config Validation Failed")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::process_terminated":
|
||||||
|
Notice.error(t("Config Validation Process Terminated"));
|
||||||
|
break;
|
||||||
|
case "config_validate::stdout_error":
|
||||||
|
Notice.error(`${t("Config Validation Failed")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::script_error":
|
||||||
|
Notice.error(`${t("Script File Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::script_syntax_error":
|
||||||
|
Notice.error(`${t("Script Syntax Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::script_missing_main":
|
||||||
|
Notice.error(`${t("Script Missing Main")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::file_not_found":
|
||||||
|
Notice.error(`${t("File Not Found")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::yaml_syntax_error":
|
||||||
|
Notice.error(`${t("YAML Syntax Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::yaml_read_error":
|
||||||
|
Notice.error(`${t("YAML Read Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::yaml_mapping_error":
|
||||||
|
Notice.error(`${t("YAML Mapping Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::yaml_key_error":
|
||||||
|
Notice.error(`${t("YAML Key Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::yaml_error":
|
||||||
|
Notice.error(`${t("YAML Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::merge_syntax_error":
|
||||||
|
Notice.error(`${t("Merge File Syntax Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::merge_mapping_error":
|
||||||
|
Notice.error(`${t("Merge File Mapping Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::merge_key_error":
|
||||||
|
Notice.error(`${t("Merge File Key Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_validate::merge_error":
|
||||||
|
Notice.error(`${t("Merge File Error")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_core::change_success":
|
||||||
|
Notice.success(`${t("Core Changed Successfully")}: ${msg}`);
|
||||||
|
break;
|
||||||
|
case "config_core::change_error":
|
||||||
|
Notice.error(`${t("Failed to Change Core")}: ${msg}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const mode = useThemeMode();
|
const mode = useThemeMode();
|
||||||
const isDark = mode === "light" ? false : true;
|
const isDark = mode === "light" ? false : true;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { language, start_page } = verge || {};
|
const { language, start_page } = verge ?? {};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const routersEles = useRoutes(routers);
|
const routersEles = useRoutes(routers);
|
||||||
const { addListener, setupCloseListener } = useListen();
|
const { addListener, setupCloseListener } = useListen();
|
||||||
if (!routersEles) return null;
|
|
||||||
|
|
||||||
setupCloseListener();
|
const handleNotice = useCallback(
|
||||||
|
(payload: [string, string]) => {
|
||||||
|
const [status, msg] = payload;
|
||||||
|
handleNoticeMessage(status, msg, t, navigate);
|
||||||
|
},
|
||||||
|
[t, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addListener("verge://refresh-clash-config", async () => {
|
const listeners = [
|
||||||
// the clash info may be updated
|
// 配置更新监听
|
||||||
await getAxios(true);
|
addListener("verge://refresh-clash-config", async () => {
|
||||||
mutate("getProxies");
|
await getAxios(true);
|
||||||
mutate("getVersion");
|
mutate("getProxies");
|
||||||
mutate("getClashConfig");
|
mutate("getVersion");
|
||||||
mutate("getProxyProviders");
|
mutate("getClashConfig");
|
||||||
});
|
mutate("getProxyProviders");
|
||||||
|
}),
|
||||||
|
|
||||||
// update the verge config
|
// verge 配置更新监听
|
||||||
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig"));
|
addListener("verge://refresh-verge-config", () =>
|
||||||
|
mutate("getVergeConfig"),
|
||||||
|
),
|
||||||
|
|
||||||
// 设置提示监听
|
// 通知消息监听
|
||||||
addListener("verge://notice-message", ({ payload }) => {
|
addListener("verge://notice-message", ({ payload }) =>
|
||||||
const [status, msg] = payload as [string, string];
|
handleNotice(payload as [string, string]),
|
||||||
switch (status) {
|
),
|
||||||
case "import_sub_url::ok":
|
];
|
||||||
navigate("/profile", { state: { current: msg } });
|
|
||||||
|
|
||||||
Notice.success(t("Import Subscription Successful"));
|
// 设置窗口显示/隐藏监听
|
||||||
break;
|
const setupWindowListeners = async () => {
|
||||||
case "import_sub_url::error":
|
const [hideUnlisten, showUnlisten] = await Promise.all([
|
||||||
navigate("/profile");
|
listen("verge://hide-window", () => appWindow.hide()),
|
||||||
Notice.error(msg);
|
listen("verge://show-window", () => appWindow.show()),
|
||||||
break;
|
]);
|
||||||
case "set_config::error":
|
|
||||||
Notice.error(msg);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
return () => {
|
||||||
portableFlag = await getPortableFlag();
|
hideUnlisten();
|
||||||
await appWindow.unminimize();
|
showUnlisten();
|
||||||
await appWindow.show();
|
};
|
||||||
await appWindow.setFocus();
|
};
|
||||||
}, 50);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
setupCloseListener();
|
||||||
|
const cleanupWindow = setupWindowListeners();
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
// 清理主要监听器
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
if (typeof listener.then === "function") {
|
||||||
|
listener.then((unlisten) => unlisten());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 清理窗口监听器
|
||||||
|
cleanupWindow.then((cleanup) => cleanup());
|
||||||
|
};
|
||||||
|
}, [handleNotice]);
|
||||||
|
|
||||||
|
// 语言和起始页设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (language) {
|
if (language) {
|
||||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||||
i18next.changeLanguage(language);
|
i18next.changeLanguage(language);
|
||||||
}
|
}
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (start_page) {
|
if (start_page) {
|
||||||
navigate(start_page);
|
navigate(start_page, { replace: true });
|
||||||
}
|
}
|
||||||
}, [language, start_page]);
|
}, [start_page]);
|
||||||
|
|
||||||
|
if (!routersEles) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||||
@@ -111,29 +217,24 @@ const Layout = () => {
|
|||||||
elevation={0}
|
elevation={0}
|
||||||
className={`${OS} layout`}
|
className={`${OS} layout`}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
// only prevent it on Windows
|
|
||||||
const validList = ["input", "textarea"];
|
|
||||||
const target = e.currentTarget;
|
|
||||||
if (
|
if (
|
||||||
OS === "windows" &&
|
OS === "windows" &&
|
||||||
!(
|
!["input", "textarea"].includes(
|
||||||
validList.includes(target.tagName.toLowerCase()) ||
|
e.currentTarget.tagName.toLowerCase(),
|
||||||
target.isContentEditable
|
) &&
|
||||||
)
|
!e.currentTarget.isContentEditable
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={[
|
sx={[
|
||||||
({ palette }) => ({
|
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||||
bgcolor: palette.background.paper,
|
|
||||||
}),
|
|
||||||
OS === "linux"
|
OS === "linux"
|
||||||
? {
|
? {
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
border: "2px solid var(--divider-color)",
|
border: "1px solid var(--divider-color)",
|
||||||
width: "calc(100vw - 4px)",
|
width: "calc(100vw - 0px)",
|
||||||
height: "calc(100vh - 4px)",
|
height: "calc(100vh - 0px)",
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
]}
|
]}
|
||||||
@@ -160,7 +261,7 @@ const Layout = () => {
|
|||||||
/>
|
/>
|
||||||
<LogoSvg fill={isDark ? "white" : "black"} />
|
<LogoSvg fill={isDark ? "white" : "black"} />
|
||||||
</div>
|
</div>
|
||||||
{<UpdateButton className="the-newbtn" />}
|
<UpdateButton className="the-newbtn" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List className="the-menu">
|
<List className="the-menu">
|
||||||
@@ -181,16 +282,14 @@ const Layout = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="layout__right">
|
<div className="layout__right">
|
||||||
{
|
<div className="the-bar">
|
||||||
<div className="the-bar">
|
<div
|
||||||
<div
|
className="the-dragbar"
|
||||||
className="the-dragbar"
|
data-tauri-drag-region="true"
|
||||||
data-tauri-drag-region="true"
|
style={{ width: "100%" }}
|
||||||
style={{ width: "100%" }}
|
/>
|
||||||
></div>
|
{OS !== "macos" && <LayoutControl />}
|
||||||
{OS !== "macos" && <LayoutControl />}
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<TransitionGroup className="the-content">
|
<TransitionGroup className="the-content">
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
|
|||||||
+91
-56
@@ -1,9 +1,14 @@
|
|||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState, useCallback } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, Button, IconButton, MenuItem } from "@mui/material";
|
import { Box, Button, IconButton, MenuItem } from "@mui/material";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TableChartRounded, TableRowsRounded } from "@mui/icons-material";
|
import {
|
||||||
|
TableChartRounded,
|
||||||
|
TableRowsRounded,
|
||||||
|
PlayCircleOutlineRounded,
|
||||||
|
PauseCircleOutlineRounded,
|
||||||
|
} from "@mui/icons-material";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { useConnectionSetting } from "@/services/states";
|
import { useConnectionSetting } from "@/services/states";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
import { useClashInfo } from "@/hooks/use-clash";
|
||||||
@@ -15,7 +20,10 @@ import {
|
|||||||
ConnectionDetailRef,
|
ConnectionDetailRef,
|
||||||
} from "@/components/connection/connection-detail";
|
} from "@/components/connection/connection-detail";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
import {
|
||||||
|
BaseSearchBox,
|
||||||
|
type SearchState,
|
||||||
|
} from "@/components/base/base-search-box";
|
||||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||||
import useSWRSubscription from "swr/subscription";
|
import useSWRSubscription from "swr/subscription";
|
||||||
import { createSockette } from "@/utils/websocket";
|
import { createSockette } from "@/utils/websocket";
|
||||||
@@ -55,6 +63,9 @@ const ConnectionsPage = () => {
|
|||||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
||||||
|
|
||||||
const { data: connData = initConn } = useSWRSubscription<
|
const { data: connData = initConn } = useSWRSubscription<
|
||||||
IConnections,
|
IConnections,
|
||||||
any,
|
any,
|
||||||
@@ -67,9 +78,7 @@ const ConnectionsPage = () => {
|
|||||||
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
|
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
|
||||||
{
|
{
|
||||||
onmessage(event) {
|
onmessage(event) {
|
||||||
// meta v1.15.0 出现 data.connections 为 null 的情况
|
|
||||||
const data = JSON.parse(event.data) as IConnections;
|
const data = JSON.parse(event.data) as IConnections;
|
||||||
// 尽量与前一次 connections 的展示顺序保持一致
|
|
||||||
next(null, (old = initConn) => {
|
next(null, (old = initConn) => {
|
||||||
const oldConn = old.connections;
|
const oldConn = old.connections;
|
||||||
const maxLen = data.connections?.length;
|
const maxLen = data.connections?.length;
|
||||||
@@ -114,39 +123,61 @@ const ConnectionsPage = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filterConn, download, upload] = useMemo(() => {
|
const displayData = useMemo(() => {
|
||||||
|
return isPaused ? (frozenData ?? connData) : connData;
|
||||||
|
}, [isPaused, frozenData, connData]);
|
||||||
|
|
||||||
|
const [filterConn] = useMemo(() => {
|
||||||
const orderFunc = orderOpts[curOrderOpt];
|
const orderFunc = orderOpts[curOrderOpt];
|
||||||
let connections = connData.connections.filter((conn) =>
|
let connections = displayData.connections.filter((conn) => {
|
||||||
match(conn.metadata.host || conn.metadata.destinationIP || ""),
|
const { host, destinationIP, process } = conn.metadata;
|
||||||
);
|
return (
|
||||||
|
match(host || "") || match(destinationIP || "") || match(process || "")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (orderFunc) connections = orderFunc(connections);
|
if (orderFunc) connections = orderFunc(connections);
|
||||||
|
|
||||||
let download = 0;
|
return [connections];
|
||||||
let upload = 0;
|
}, [displayData, match, curOrderOpt]);
|
||||||
connections.forEach((x) => {
|
|
||||||
download += x.download;
|
|
||||||
upload += x.upload;
|
|
||||||
});
|
|
||||||
return [connections, download, upload];
|
|
||||||
}, [connData, match, curOrderOpt]);
|
|
||||||
|
|
||||||
const onCloseAll = useLockFn(closeAllConnections);
|
const onCloseAll = useLockFn(closeAllConnections);
|
||||||
|
|
||||||
const detailRef = useRef<ConnectionDetailRef>(null!);
|
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||||
|
|
||||||
|
const handleSearch = useCallback((match: (content: string) => boolean) => {
|
||||||
|
setMatch(() => match);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePauseToggle = useCallback(() => {
|
||||||
|
setIsPaused((prev) => {
|
||||||
|
if (!prev) {
|
||||||
|
setFrozenData(connData);
|
||||||
|
} else {
|
||||||
|
setFrozenData(null);
|
||||||
|
}
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
|
}, [connData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
|
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "auto",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
header={
|
header={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
<Box sx={{ mx: 1 }}>
|
<Box sx={{ mx: 1 }}>
|
||||||
{t("Downloaded")}: {parseTraffic(download)}
|
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ mx: 1 }}>
|
<Box sx={{ mx: 1 }}>
|
||||||
{t("Uploaded")}: {parseTraffic(upload)}
|
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -160,16 +191,23 @@ const ConnectionsPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isTableLayout ? (
|
{isTableLayout ? (
|
||||||
<span title={t("List View")}>
|
<TableRowsRounded titleAccess={t("List View")} />
|
||||||
<TableRowsRounded fontSize="inherit" />
|
|
||||||
</span>
|
|
||||||
) : (
|
) : (
|
||||||
<span title={t("Table View")}>
|
<TableChartRounded titleAccess={t("Table View")} />
|
||||||
<TableChartRounded fontSize="inherit" />
|
)}
|
||||||
</span>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={handlePauseToggle}
|
||||||
|
title={isPaused ? t("Resume") : t("Pause")}
|
||||||
|
>
|
||||||
|
{isPaused ? (
|
||||||
|
<PlayCircleOutlineRounded />
|
||||||
|
) : (
|
||||||
|
<PauseCircleOutlineRounded />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Button size="small" variant="contained" onClick={onCloseAll}>
|
<Button size="small" variant="contained" onClick={onCloseAll}>
|
||||||
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
|
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -185,6 +223,9 @@ const ConnectionsPage = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
|
position: "sticky",
|
||||||
|
top: 0,
|
||||||
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isTableLayout && (
|
{!isTableLayout && (
|
||||||
@@ -199,37 +240,31 @@ const ConnectionsPage = () => {
|
|||||||
))}
|
))}
|
||||||
</BaseStyledSelect>
|
</BaseStyledSelect>
|
||||||
)}
|
)}
|
||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={handleSearch} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
{filterConn.length === 0 ? (
|
||||||
height="calc(100% - 65px)"
|
<BaseEmpty />
|
||||||
sx={{
|
) : isTableLayout ? (
|
||||||
userSelect: "text",
|
<ConnectionTable
|
||||||
margin: "10px",
|
connections={filterConn}
|
||||||
borderRadius: "8px",
|
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
/>
|
||||||
}}
|
) : (
|
||||||
>
|
<Virtuoso
|
||||||
{filterConn.length === 0 ? (
|
style={{
|
||||||
<BaseEmpty />
|
flex: 1,
|
||||||
) : isTableLayout ? (
|
borderRadius: "8px",
|
||||||
<ConnectionTable
|
}}
|
||||||
connections={filterConn}
|
data={filterConn}
|
||||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
itemContent={(_, item) => (
|
||||||
/>
|
<ConnectionItem
|
||||||
) : (
|
value={item}
|
||||||
<Virtuoso
|
onShowDetail={() => detailRef.current?.open(item)}
|
||||||
data={filterConn}
|
/>
|
||||||
itemContent={(_, item) => (
|
)}
|
||||||
<ConnectionItem
|
/>
|
||||||
value={item}
|
)}
|
||||||
onShowDetail={() => detailRef.current?.open(item)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<ConnectionDetail ref={detailRef} />
|
<ConnectionDetail ref={detailRef} />
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
|
|||||||
+31
-28
@@ -32,11 +32,15 @@ const LogPage = () => {
|
|||||||
|
|
||||||
const filterLogs = useMemo(() => {
|
const filterLogs = useMemo(() => {
|
||||||
return logData
|
return logData
|
||||||
? logData.filter((data) =>
|
? logData.filter((data) => {
|
||||||
logLevel === "all"
|
// 构建完整的搜索文本,包含时间、类型和内容
|
||||||
? match(data.payload)
|
const searchText =
|
||||||
: data.type.includes(logLevel) && match(data.payload),
|
`${data.time || ""} ${data.type} ${data.payload}`.toLowerCase();
|
||||||
)
|
|
||||||
|
return logLevel === "all"
|
||||||
|
? match(searchText)
|
||||||
|
: data.type.toLowerCase() === logLevel && match(searchText);
|
||||||
|
})
|
||||||
: [];
|
: [];
|
||||||
}, [logData, logLevel, match]);
|
}, [logData, logLevel, match]);
|
||||||
|
|
||||||
@@ -44,7 +48,12 @@ const LogPage = () => {
|
|||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
title={t("Logs")}
|
title={t("Logs")}
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
header={
|
header={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -65,7 +74,7 @@ const LogPage = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearLogs(logLevel);
|
clearLogs();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("Clear")}
|
{t("Clear")}
|
||||||
@@ -102,27 +111,21 @@ const LogPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
{filterLogs.length > 0 ? (
|
||||||
height="calc(100% - 65px)"
|
<Virtuoso
|
||||||
sx={{
|
initialTopMostItemIndex={999}
|
||||||
margin: "10px",
|
data={filterLogs}
|
||||||
borderRadius: "8px",
|
style={{
|
||||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
itemContent={(index, item) => (
|
||||||
{filterLogs.length > 0 ? (
|
<LogItem value={item} searchState={searchState} />
|
||||||
<Virtuoso
|
)}
|
||||||
initialTopMostItemIndex={999}
|
followOutput={"smooth"}
|
||||||
data={filterLogs}
|
/>
|
||||||
itemContent={(index, item) => (
|
) : (
|
||||||
<LogItem value={item} searchState={searchState} />
|
<BaseEmpty />
|
||||||
)}
|
)}
|
||||||
followOutput={"smooth"}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<BaseEmpty />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+47
-48
@@ -27,7 +27,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
importProfile,
|
importProfile,
|
||||||
enhanceProfiles,
|
enhanceProfiles,
|
||||||
restartCore,
|
//restartCore,
|
||||||
getRuntimeLogs,
|
getRuntimeLogs,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
@@ -174,11 +174,11 @@ const ProfilePage = () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchProfiles({ current: profile });
|
const success = await patchProfiles({ current: profile });
|
||||||
await mutateLogs();
|
await mutateLogs();
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
await activateSelected();
|
await activateSelected();
|
||||||
if (notifySuccess) {
|
if (notifySuccess && success) {
|
||||||
Notice.success(t("Profile Switched"), 1000);
|
Notice.success(t("Profile Switched"), 1000);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -367,20 +367,19 @@ const ProfilePage = () => {
|
|||||||
{t("New")}
|
{t("New")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box
|
|
||||||
sx={{
|
<DndContext
|
||||||
pt: 1,
|
sensors={sensors}
|
||||||
mb: 0.5,
|
collisionDetection={closestCenter}
|
||||||
pl: "10px",
|
onDragEnd={onDragEnd}
|
||||||
mr: "10px",
|
|
||||||
height: "calc(100% - 68px)",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DndContext
|
<Box
|
||||||
sensors={sensors}
|
sx={{
|
||||||
collisionDetection={closestCenter}
|
pl: "10px",
|
||||||
onDragEnd={onDragEnd}
|
pr: "10px",
|
||||||
|
height: "calc(100% - 48px)",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ mb: 1.5 }}>
|
<Box sx={{ mb: 1.5 }}>
|
||||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||||
@@ -401,8 +400,8 @@ const ProfilePage = () => {
|
|||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
if (prev !== curr && profiles.current === item.uid) {
|
if (prev !== curr && profiles.current === item.uid) {
|
||||||
await onEnhance(false);
|
await onEnhance(false);
|
||||||
await restartCore();
|
// await restartCore();
|
||||||
Notice.success(t("Clash Core Restarted"), 1000);
|
// Notice.success(t("Clash Core Restarted"), 1000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDelete={() => onDelete(item.uid)}
|
onDelete={() => onDelete(item.uid)}
|
||||||
@@ -412,38 +411,38 @@ const ProfilePage = () => {
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
</Box>
|
</Box>
|
||||||
</DndContext>
|
<Divider
|
||||||
<Divider
|
variant="middle"
|
||||||
variant="middle"
|
flexItem
|
||||||
flexItem
|
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
></Divider>
|
||||||
></Divider>
|
<Box sx={{ mt: 1.5, mb: "10px" }}>
|
||||||
<Box sx={{ mt: 1.5 }}>
|
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
<ProfileMore
|
||||||
<ProfileMore
|
id="Merge"
|
||||||
id="Merge"
|
onSave={async (prev, curr) => {
|
||||||
onSave={async (prev, curr) => {
|
if (prev !== curr) {
|
||||||
if (prev !== curr) {
|
await onEnhance(false);
|
||||||
await onEnhance(false);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||||
|
<ProfileMore
|
||||||
|
id="Script"
|
||||||
|
logInfo={chainLogs["Script"]}
|
||||||
|
onSave={async (prev, curr) => {
|
||||||
|
if (prev !== curr) {
|
||||||
|
await onEnhance(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
</Box>
|
||||||
<ProfileMore
|
|
||||||
id="Script"
|
|
||||||
logInfo={chainLogs["Script"]}
|
|
||||||
onSave={async (prev, curr) => {
|
|
||||||
if (prev !== curr) {
|
|
||||||
await onEnhance(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</DndContext>
|
||||||
|
|
||||||
<ProfileViewer
|
<ProfileViewer
|
||||||
ref={viewerRef}
|
ref={viewerRef}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useLockFn } from "ahooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Box, Button, ButtonGroup } from "@mui/material";
|
import { Box, Button, ButtonGroup } from "@mui/material";
|
||||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
import { closeAllConnections, getClashConfig } from "@/services/api";
|
||||||
import { patchClashConfig } from "@/services/cmds";
|
import { patchClashConfig, patchClashMode } from "@/services/cmds";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { BasePage } from "@/components/base";
|
import { BasePage } from "@/components/base";
|
||||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||||
@@ -29,7 +29,7 @@ const ProxyPage = () => {
|
|||||||
if (mode !== curMode && verge?.auto_close_connection) {
|
if (mode !== curMode && verge?.auto_close_connection) {
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
}
|
}
|
||||||
await patchClashConfig({ mode });
|
await patchClashMode(mode);
|
||||||
mutateClash();
|
mutateClash();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ const ProxyPage = () => {
|
|||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{ height: "101.5%" }}
|
||||||
title={t("Proxy Groups")}
|
title={t("Proxy Groups")}
|
||||||
header={
|
header={
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
|||||||
+27
-29
@@ -39,7 +39,12 @@ const RulesPage = () => {
|
|||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
title={t("Rules")}
|
title={t("Rules")}
|
||||||
contentStyle={{ height: "100%" }}
|
contentStyle={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
header={
|
header={
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<ProviderButton />
|
<ProviderButton />
|
||||||
@@ -59,34 +64,27 @@ const RulesPage = () => {
|
|||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
{rules.length > 0 ? (
|
||||||
height="calc(100% - 65px)"
|
<>
|
||||||
sx={{
|
<Virtuoso
|
||||||
margin: "10px",
|
ref={virtuosoRef}
|
||||||
borderRadius: "8px",
|
data={rules}
|
||||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
style={{
|
||||||
position: "relative",
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
itemContent={(index, item) => (
|
||||||
{rules.length > 0 ? (
|
<RuleItem index={index + 1} value={item} />
|
||||||
<>
|
)}
|
||||||
<Virtuoso
|
followOutput={"smooth"}
|
||||||
ref={virtuosoRef}
|
scrollerRef={(ref) => {
|
||||||
data={rules}
|
if (ref) ref.addEventListener("scroll", handleScroll);
|
||||||
itemContent={(index, item) => (
|
}}
|
||||||
<RuleItem index={index + 1} value={item} />
|
/>
|
||||||
)}
|
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
||||||
followOutput={"smooth"}
|
</>
|
||||||
scrollerRef={(ref) => {
|
) : (
|
||||||
if (ref) ref.addEventListener("scroll", handleScroll);
|
<BaseEmpty />
|
||||||
}}
|
)}
|
||||||
/>
|
|
||||||
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<BaseEmpty />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-3
@@ -1,10 +1,18 @@
|
|||||||
import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
|
import {
|
||||||
|
Box,
|
||||||
|
ButtonGroup,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BasePage, Notice } from "@/components/base";
|
import { BasePage, Notice } from "@/components/base";
|
||||||
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
|
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
|
||||||
import { openWebUrl } from "@/services/cmds";
|
import { openWebUrl } from "@/services/cmds";
|
||||||
import SettingVerge from "@/components/setting/setting-verge";
|
import SettingVergeBasic from "@/components/setting/setting-verge-basic";
|
||||||
|
import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced";
|
||||||
import SettingClash from "@/components/setting/setting-clash";
|
import SettingClash from "@/components/setting/setting-clash";
|
||||||
import SettingSystem from "@/components/setting/setting-system";
|
import SettingSystem from "@/components/setting/setting-system";
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from "@/services/states";
|
||||||
@@ -88,10 +96,19 @@ const SettingPage = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
|
marginBottom: 1.5,
|
||||||
backgroundColor: isDark ? "#282a36" : "#ffffff",
|
backgroundColor: isDark ? "#282a36" : "#ffffff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SettingVerge onError={onError} />
|
<SettingVergeBasic onError={onError} />
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: isDark ? "#282a36" : "#ffffff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingVergeAdvanced onError={onError} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
+13
-1
@@ -91,6 +91,10 @@ export async function patchClashConfig(payload: Partial<IConfigData>) {
|
|||||||
return invoke<void>("patch_clash_config", { payload });
|
return invoke<void>("patch_clash_config", { payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function patchClashMode(payload: String) {
|
||||||
|
return invoke<void>("patch_clash_mode", { payload });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getVergeConfig() {
|
export async function getVergeConfig() {
|
||||||
return invoke<IVergeConfig>("get_verge_config");
|
return invoke<IVergeConfig>("get_verge_config");
|
||||||
}
|
}
|
||||||
@@ -115,7 +119,7 @@ export async function getAutotemProxy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function changeClashCore(clashCore: string) {
|
export async function changeClashCore(clashCore: string) {
|
||||||
return invoke<any>("change_clash_core", { clashCore });
|
return invoke<string | null>("change_clash_core", { clashCore });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restartCore() {
|
export async function restartCore() {
|
||||||
@@ -237,3 +241,11 @@ export async function listWebDavBackup() {
|
|||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function scriptValidateNotice(status: string, msg: string) {
|
||||||
|
return invoke<void>("script_validate_notice", { status, msg });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateScriptFile(filePath: string) {
|
||||||
|
return invoke<boolean>("validate_script_file", { filePath });
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+3
@@ -120,6 +120,7 @@ interface IConnectionsItem {
|
|||||||
sourcePort: string;
|
sourcePort: string;
|
||||||
destinationPort: string;
|
destinationPort: string;
|
||||||
destinationIP?: string;
|
destinationIP?: string;
|
||||||
|
remoteDestination?: string;
|
||||||
process?: string;
|
process?: string;
|
||||||
processPath?: string;
|
processPath?: string;
|
||||||
};
|
};
|
||||||
@@ -708,9 +709,11 @@ interface IVergeConfig {
|
|||||||
tun_tray_icon?: boolean;
|
tun_tray_icon?: boolean;
|
||||||
enable_tray_speed?: boolean;
|
enable_tray_speed?: boolean;
|
||||||
enable_tun_mode?: boolean;
|
enable_tun_mode?: boolean;
|
||||||
|
enable_lite_mode?: boolean;
|
||||||
enable_auto_launch?: boolean;
|
enable_auto_launch?: boolean;
|
||||||
enable_silent_start?: boolean;
|
enable_silent_start?: boolean;
|
||||||
enable_system_proxy?: boolean;
|
enable_system_proxy?: boolean;
|
||||||
|
enable_global_hotkey?: boolean;
|
||||||
proxy_auto_config?: boolean;
|
proxy_auto_config?: boolean;
|
||||||
pac_file_content?: string;
|
pac_file_content?: string;
|
||||||
enable_random_port?: boolean;
|
enable_random_port?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user