Compare commits

...

72 Commits

89 changed files with 6006 additions and 3210 deletions

View File

@@ -8,13 +8,13 @@ body:
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq.html)
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题以便他人快速检索
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
5. 请 **务必** 按照模板规范详细描述问题否则issue将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) and [FAQ](https://clash-verge-rev.github.io/faq.html)
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group

View File

@@ -8,13 +8,13 @@ body:
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 确认软件不存在类似的功能
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题以便他人快速检索
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题否则issue将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) to confirm that the software does not have similar functions
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented

View File

@@ -10,6 +10,12 @@ rustup target add "$INPUT_TARGET"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
elif [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
dpkg --add-architecture i386
apt-get update
apt-get install -y libstdc++6:i386 libgdk-pixbuf2.0-dev:i386 libatomic1:i386 gcc-multilib g++-multilib libwebkit2gtk-4.0-dev:i386 libssl-dev:i386 libgtk-3-dev:i386 librsvg2-dev:i386 patchelf:i386 libayatana-appindicator3-dev:i386
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64
apt-get update

View File

@@ -18,6 +18,8 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
@@ -49,7 +51,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Pnpm install and check
@@ -77,8 +79,6 @@ jobs:
run: pnpm portable ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
alpha-for-linux:
strategy:
@@ -87,6 +87,8 @@ jobs:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
@@ -111,52 +113,113 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- run: |
cat > release.txt << 'EOF'
### 我应该下载哪个版本?
- Windows x86_64架构: x64-setup.exe (不支持win7)
- Windows arm64架构: arm64-setup.exe
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
- Linux x64架构: amd64.AppImage/amd64.deb
- Linux arm64架构: arm64.deb
- Linux armv7架构: armhf.deb
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
### FAQ
- [https://clash-verge-rev.github.io/faq.html](https://clash-verge-rev.github.io/faq.html)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
alpha-for-fixed-webview2:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- os: windows-latest
target: i686-pc-windows-msvc
arch: x86
- os: windows-latest
target: aarch64-pc-windows-msvc
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
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'
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'
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'
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux]
needs: [alpha, alpha-for-linux, alpha-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -175,27 +238,41 @@ jobs:
- run: |
cat > release.txt << 'EOF'
### 我应该下载哪个版本?
## 我应该下载哪个版本?
- Windows x86_64架构: x64-setup.exe (不支持win7)
- Windows arm64架构: arm64-setup.exe
### MacOS (提示文件损坏或开发者无法验证请查看下面FAQ)
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
- Linux x64架构: amd64.AppImage/amd64.deb
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.AppImage/amd64.deb
- Linux 32位: i386.deb
- Linux arm64架构: arm64.deb
- Linux armv7架构: armhf.deb
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
### Windows (Win7 用户请查看下面FAQ手动替换内核使用)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- 32位: x86-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版(不推荐使用,无法自动更新)
- 64位: x64_portable.zip
- 32位: x86_portable.zip
- arm64架构: arm64_portable.zip
#### 内置Webview2版(体积较大仅在企业版系统或Win7无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- 32位: x86_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
### FAQ
- [https://clash-verge-rev.github.io/faq.html](https://clash-verge-rev.github.io/faq.html)
- [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"

View File

@@ -15,6 +15,8 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
@@ -46,7 +48,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Pnpm install and check
@@ -74,8 +76,6 @@ jobs:
run: pnpm portable ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
release-for-linux:
strategy:
@@ -84,6 +84,8 @@ jobs:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
@@ -109,7 +111,7 @@ jobs:
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
@@ -118,7 +120,7 @@ jobs:
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
@@ -126,22 +128,104 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
release-for-fixed-webview2:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- os: windows-latest
target: i686-pc-windows-msvc
arch: x86
- os: windows-latest
target: aarch64-pc-windows-msvc
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
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'
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'
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'
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update:
runs-on: ubuntu-latest
needs: [release, release-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Pnpm install
@@ -151,3 +235,29 @@ jobs:
run: pnpm updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-for-fixed-webview2:
runs-on: ubuntu-latest
needs: [release-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
run: pnpm i
- name: Release updater file
run: pnpm updater-fixed-webview2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -10,14 +10,14 @@ jobs:
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Pnpm install
@@ -27,3 +27,28 @@ jobs:
run: pnpm updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-for-fixed-webview2:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
run: pnpm i
- name: Release updater file
run: pnpm updater-fixed-webview2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
.pnpm-store
.DS_Store
dist
dist-ssr
@@ -6,4 +7,4 @@ dist-ssr
update.json
scripts/_env.sh
.vscode
.tool-version
.tool-versions

View File

@@ -49,7 +49,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
### FAQ
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq.html)
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
## Development

View File

@@ -1,3 +1,74 @@
## v1.6.2
### Features
- 支持本地文件拖拽导入
- 重新支持 32 位 CPU
- 新增内置 Webview2 版本
- 优化 Merge 逻辑,支持深度合并
- 删除 Merge 配置中的 append/prepend-provider 字段
- 支持更新稳定版内核
### Bugs Fixes
- MacOS DNS 还原失败
- CMD 环境变量格式错误
- Linux 下与 N 卡的兼容性问题
- 修改 Tun 设置不立即生效
## v1.6.1
### Features
- 鼠标悬浮显示当前订阅的名称 [#938](https://github.com/clash-verge-rev/clash-verge-rev/pull/938)
- 日志过滤支持正则表达式 [#959](https://github.com/clash-verge-rev/clash-verge-rev/pull/959)
- 更新 Clash 内核到 1.18.4
### Bugs Fixes
- 修复 Linux KDE 环境下系统代理无法开启的问题
- 窗口最大化图标调整 [#924](https://github.com/clash-verge-rev/clash-verge-rev/pull/924)
- 修改 MacOS 托盘点击行为(左键菜单,右键点击事件)
- 修复 MacOS 服务模式安装失败的问题
---
## v1.6.0
### Features
- Meta(mihomo)内核回退 1.18.1(当前新版内核 hy2 协议有 bug等修复后更新
- 多处界面细节调整 [#724](https://github.com/clash-verge-rev/clash-verge-rev/pull/724) [#799](https://github.com/clash-verge-rev/clash-verge-rev/pull/799) [#900](https://github.com/clash-verge-rev/clash-verge-rev/pull/900) [#901](https://github.com/clash-verge-rev/clash-verge-rev/pull/901)
- Linux 下新增服务模式
- 新增订阅卡片右键可以打开机场首页
- url-test 支持手动选择、节点组 fixed 节点使用角标展示 [#840](https://github.com/clash-verge-rev/clash-verge-rev/pull/840)
- Clash 配置、Merge 配置提供 JSON Schema 语法支持、连接界面调整 [#887](https://github.com/clash-verge-rev/clash-verge-rev/pull/887)
- 修改 Merge 配置文件默认内容 [#889](https://github.com/clash-verge-rev/clash-verge-rev/pull/889)
- 修改 tun 模式默认 mtu 为 1500老版本升级需在 tun 模式设置下“重置为默认值”。
- 使用 npm 安装 meta-json-schema [#895](https://github.com/clash-verge-rev/clash-verge-rev/pull/895)
- 更新部分翻译 [#904](https://github.com/clash-verge-rev/clash-verge-rev/pull/904)
- 支持 ico 格式的任务栏图标
### Bugs Fixes
- 修复 Linux KDE 环境下系统代理无法开启的问题
- 修复延迟检测动画问题
- 窗口最大化图标调整 [#816](https://github.com/clash-verge-rev/clash-verge-rev/pull/816)
- 修复 Windows 某些情况下无法安装服务模式 [#822](https://github.com/clash-verge-rev/clash-verge-rev/pull/822)
- UI 细节修复 [#821](https://github.com/clash-verge-rev/clash-verge-rev/pull/821)
- 修复使用默认编辑器打开配置文件
- 修复内核文件在特定目录也可以更新的问题 [#857](https://github.com/clash-verge-rev/clash-verge-rev/pull/857)
- 修复服务模式的安装目录问题
- 修复删除配置文件的“更新间隔”出现的问题 [#907](https://github.com/clash-verge-rev/clash-verge-rev/issues/907)
### 已知问题(历史遗留问题,暂未找到有效解决方案)
- MacOS M 芯片下服务模式无法安装;临时解决方案:在内核 ⚙️ 下,手动授权,再打开 tun 模式。
- MacOS 下如果删除过网络配置,会导致无法正常打开系统代理;临时解决方案:使用浏览器代理插件或手动配置系统代理。
- Window 拨号连接下无法正确识别并打开系统代理;临时解决方案:使用浏览器代理插件或使用 tun 模式。
---
## v1.5.11
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.5.11",
"version": "1.6.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
@@ -12,52 +12,57 @@
"web:serve": "vite preview",
"check": "node scripts/check.mjs",
"updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"prepare": "husky install"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.15.5",
"@mui/icons-material": "^5.15.16",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.5",
"@mui/x-data-grid": "^6.18.7",
"@tauri-apps/api": "^1.5.3",
"ahooks": "^3.7.8",
"axios": "^1.6.5",
"@mui/material": "^5.15.16",
"@mui/x-data-grid": "^6.19.11",
"@tauri-apps/api": "^1.5.4",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.7.11",
"axios": "^1.6.8",
"dayjs": "1.11.5",
"i18next": "^23.7.16",
"i18next": "^23.11.3",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1",
"nanoid": "^5.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"meta-json-schema": "1.18.4-beta4",
"monaco-editor": "^0.47.0",
"monaco-yaml": "^5.1.1",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.49.3",
"react-hook-form": "^7.51.4",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.21.2",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.23.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.6.2",
"react-virtuoso": "^4.7.10",
"recoil": "^0.7.7",
"snarkdown": "^2.0.0",
"swr": "^1.3.0",
"tar": "^6.2.0"
"tar": "^6.2.1"
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.9",
"@tauri-apps/cli": "^1.5.13",
"@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.6",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.10",
"adm-zip": "^0.5.12",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1",
@@ -65,9 +70,9 @@
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"sass": "^1.70.0",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"sass": "^1.77.0",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},

5277
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -101,7 +101,7 @@ async function getLatestAlphaVersion() {
/* ======= clash meta stable ======= */
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
const META_URL_PREFIX = `https://github.com/clash-verge-rev/mihomo/releases/download`;
let META_VERSION;
const META_MAP = {
@@ -353,27 +353,53 @@ const resolvePlugin = async () => {
}
};
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service",
"install-service",
"uninstall-service",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (let f of serviceExecutables) {
const targetPath = path.join(resDir, f);
if (await fs.pathExists(targetPath)) {
execSync(`chmod 755 ${targetPath}`);
console.log(`[INFO]: "${targetPath}" chmod finished`);
}
}
};
/**
* main
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () =>
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "clash-verge-service.exe",
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
file: "clash-verge-service" + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
const resolveInstall = () =>
};
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "install-service.exe",
downloadURL: `${SERVICE_URL}/install-service.exe`,
file: "install-service" + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
});
const resolveUninstall = () =>
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "uninstall-service.exe",
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
file: "uninstall-service" + ext,
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
};
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
@@ -420,9 +446,9 @@ const tasks = [
retry: 5,
},
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, retry: 5 },
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 },
@@ -434,12 +460,20 @@ const tasks = [
retry: 5,
winOnly: true,
},
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && process.platform !== "win32") return runTask();
if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
for (let i = 0; i < task.retry; i++) {
try {

View File

@@ -0,0 +1,100 @@
import fs from "fs-extra";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "x86",
"aarch64-pc-windows-msvc": "arm64",
};
const PROCESS_MAP = {
x64: "x64",
ia32: "x86",
arm64: "arm64",
};
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
/// Script for ci
/// 打包绿色版/便携版 (only Windows)
async function resolvePortable() {
if (process.platform !== "win32") return;
const releaseDir = target
? `./src-tauri/target/${target}/release`
: `./src-tauri/target/release`;
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
);
zip.addLocalFolder(configDir, ".config");
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
const { version } = packageJson;
const zipFile = `Clash.Verge_${version}_${arch}_fixed_webview2_portable.zip`;
zip.writeZip(zipFile);
console.log("[INFO]: create portable zip successfully");
// push release assets
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
console.log("[INFO]: upload to ", tag);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag,
});
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
});
}
console.log(release.name);
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: release.id,
name: zipFile,
data: zip.toBuffer(),
});
}
resolvePortable().catch(console.error);

View File

@@ -9,9 +9,16 @@ const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "x86",
"aarch64-pc-windows-msvc": "arm64",
};
const PROCESS_MAP = {
x64: "x64",
ia32: "x86",
arm64: "arm64",
};
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
/// Script for ci
/// 打包绿色版/便携版 (only Windows)
async function resolvePortable() {
@@ -41,7 +48,7 @@ async function resolvePortable() {
const packageJson = require("../package.json");
const { version } = packageJson;
const zipFile = `Clash.Verge_${version}_${ARCH_MAP[target]}_portable.zip`;
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`;
zip.writeZip(zipFile);
console.log("[INFO]: create portable zip successfully");

View File

@@ -0,0 +1,154 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update-fixed-webview2.json";
const UPDATE_JSON_PROXY = "update-fixed-webview2-proxy.json";
/// generate update.json
/// upload to update tag's release asset
async function resolveUpdater() {
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});
// get the latest publish tag
const tag = tags.find((t) => t.name.startsWith("v"));
console.log(tag);
console.log();
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name), // use updatelog.md
pub_date: new Date().toISOString(),
platforms: {
"windows-x86_64": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
"windows-x86": { signature: "", url: "" },
},
};
const promises = latestRelease.assets.map(async (asset) => {
const { name, browser_download_url } = asset;
// win64 url
if (name.endsWith("x64_fixed_webview2-setup.nsis.zip")) {
updateData.platforms["windows-x86_64"].url = browser_download_url;
}
// win64 signature
if (name.endsWith("x64_fixed_webview2-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
}
// win arm url
if (name.endsWith("arm64_fixed_webview2-setup.nsis.zip")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
}
// win arm signature
if (name.endsWith("arm64_fixed_webview2-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig;
}
});
await Promise.allSettled(promises);
console.log(updateData);
// maybe should test the signature as well
// delete the null field
Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`);
delete updateData.platforms[key];
}
});
// 生成一个代理github的更新文件
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
const updateDataNew = JSON.parse(JSON.stringify(updateData));
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
"https://mirror.ghproxy.com/" + value.url;
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
}
});
// update the update.json
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: UPDATE_TAG_NAME,
});
// delete the old assets
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
});
}
if (asset.name === UPDATE_JSON_PROXY) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
}
}
// upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_FILE,
data: JSON.stringify(updateData, null, 2),
});
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_PROXY,
data: JSON.stringify(updateDataNew, null, 2),
});
}
// get the signature file content
async function getSignature(url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
return response.text();
}
resolveUpdater().catch(console.error);

View File

@@ -45,10 +45,12 @@ async function resolveUpdater() {
"darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
"linux-x86": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
"windows-x86": { signature: "", url: "" },
},
};
@@ -67,6 +69,16 @@ async function resolveUpdater() {
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x64-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
}
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
@@ -105,6 +117,7 @@ async function resolveUpdater() {
if (name.endsWith("amd64.AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url;
updateData.platforms["linux-x86"].url = browser_download_url;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
@@ -114,6 +127,7 @@ async function resolveUpdater() {
const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig;
updateData.platforms["linux-x86"].url = browser_download_url;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;

1515
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.5.11"
version = "1.6.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -37,7 +37,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { version = "1.6", features = [ "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
tauri = { version = "1.6", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -45,6 +45,7 @@ deelevate = "0.2.0"
winreg = "0.52.0"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
#openssl
[features]

View File

@@ -65,6 +65,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part();
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
Ok(())
@@ -299,9 +300,17 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
}
let dest_path = icon_dir.join(name);
let ext = match file_path.extension() {
Some(e) => e.to_string_lossy().to_string(),
None => "ico".to_string(),
};
let png_dest_path = icon_dir.join(format!("{name}.png"));
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
let dest_path = icon_dir.join(format!("{name}.{ext}"));
if file_path.exists() {
std::fs::remove_file(png_dest_path).unwrap_or_default();
std::fs::remove_file(ico_dest_path).unwrap_or_default();
match std::fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
@@ -331,42 +340,23 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
std::process::exit(0);
}
#[cfg(windows)]
pub mod service {
use super::*;
use crate::core::win_service;
use crate::core::service;
#[tauri::command]
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
wrap_err!(win_service::check_service().await)
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
wrap_err!(win_service::install_service().await)
wrap_err!(service::install_service().await)
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
wrap_err!(win_service::uninstall_service().await)
}
}
#[cfg(not(windows))]
pub mod service {
use super::*;
#[tauri::command]
pub async fn check_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
Ok(())
wrap_err!(service::uninstall_service().await)
}
}

View File

@@ -38,7 +38,7 @@ impl IClashTemp {
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 9000.into());
tun.insert("mtu".into(), 1500.into());
#[cfg(not(target_os = "windows"))]
map.insert("redir-port".into(), 7895.into());
#[cfg(target_os = "linux")]

View File

@@ -46,6 +46,10 @@ pub struct PrfItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<PrfOption>,
/// profile web page url
#[serde(skip_serializing_if = "Option::is_none")]
pub home: Option<String>,
/// the file data
#[serde(skip)]
pub file_data: Option<String>,
@@ -161,6 +165,7 @@ impl PrfItem {
selected: None,
extra: None,
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
})
@@ -291,6 +296,14 @@ impl PrfItem {
},
};
let home = match header.get("profile-web-page-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
Some(str_value.to_string())
},
None => None,
};
let uid = help::get_uid("r");
let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
@@ -317,6 +330,7 @@ impl PrfItem {
selected: None,
extra,
option,
home,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()),
})
@@ -338,6 +352,7 @@ impl PrfItem {
selected: None,
extra: None,
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
@@ -356,6 +371,7 @@ impl PrfItem {
desc: Some(desc),
file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,

View File

@@ -211,7 +211,7 @@ impl IProfiles {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home;
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {

View File

@@ -1,3 +1,4 @@
use super::service;
use super::{clash_api, logger::Logger};
use crate::log_err;
use crate::{config::*, utils::dirs};
@@ -93,10 +94,9 @@ impl CoreManager {
None => false,
};
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(super::win_service::stop_core_by_service().await);
log_err!(service::stop_core_by_service().await);
should_kill = true;
}
@@ -105,32 +105,27 @@ impl CoreManager {
sleep(Duration::from_millis(500)).await;
}
#[cfg(target_os = "windows")]
{
use super::win_service;
// 服务模式
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
// 服务模式
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
*self.use_service_mode.lock() = enable;
*self.use_service_mode.lock() = enable;
if enable {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
if enable {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
match (|| async {
win_service::check_service().await?;
win_service::run_core_by_service(&config_path).await
})()
.await
{
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
match (|| async {
service::check_service().await?;
service::run_core_by_service(&config_path).await
})()
.await
{
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
}
}
@@ -205,7 +200,6 @@ impl CoreManager {
/// 重启内核
pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() {
return Ok(());
}
@@ -238,11 +232,10 @@ impl CoreManager {
/// 停止核心运行
pub fn stop_core(&self) -> Result<()> {
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move {
log_err!(super::win_service::stop_core_by_service().await);
log_err!(service::stop_core_by_service().await);
});
return Ok(());
}

View File

@@ -22,7 +22,7 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
#[cfg(target_os = "linux")]
let output = {
let path = path.replace(' ', "\\ "); // 避免路径中有空格
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
let shell = format!("setcap cap_net_bind_service,cap_net_admin,cap_dac_override=+ep {path}");
let sudo = match Command::new("which").arg("pkexec").output() {
Ok(output) => {

View File

@@ -7,7 +7,7 @@ pub mod manager;
pub mod sysopt;
pub mod timer;
pub mod tray;
pub mod win_service;
pub mod service;
pub mod win_uwp;
pub use self::core::*;

View File

@@ -1,18 +1,15 @@
#![cfg(target_os = "windows")]
use crate::config::Config;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211";
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -32,7 +29,13 @@ pub struct JsonResponse {
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe");
@@ -60,9 +63,69 @@ pub async fn install_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(installer_path).status()?,
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(installer_path)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
@@ -90,6 +153,63 @@ pub async fn uninstall_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstaller_path).status()?,
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(uninstaller_path)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// check the windows service status
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
@@ -119,7 +239,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let clash_bin = format!("{clash_core}.exe");
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
let bin_path = current_exe()?.with_file_name(clash_bin);
let bin_path = dirs::path_to_str(&bin_path)?;

View File

@@ -3,9 +3,10 @@ use anyhow::{anyhow, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::env::current_exe;
use std::sync::Arc;
use sysproxy::Sysproxy;
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
use tauri::async_runtime::Mutex as TokioMutex;
pub struct Sysopt {
/// current system proxy setting
@@ -149,7 +150,7 @@ impl Sysopt {
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
// let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())

View File

@@ -182,9 +182,13 @@ impl Tray {
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
if *sysproxy_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("sysproxy.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
@@ -194,9 +198,13 @@ impl Tray {
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
if *common_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("common.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
@@ -208,9 +216,13 @@ impl Tray {
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
if *tun_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("tun.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
indication_icon = icon
@@ -249,18 +261,30 @@ impl Tray {
map
};
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}",
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"),
switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"),
switch_map[tun_mode]
switch_map[tun_mode],
t!("Curent Profile", "当前订阅"),
current_profile_name
));
Ok(())
}
pub fn on_left_click(app_handle: &AppHandle) {
pub fn on_click(app_handle: &AppHandle) {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = tray_event.unwrap_or("main_window".into());
match tray_event.as_str() {
@@ -273,7 +297,10 @@ impl Tray {
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::LeftClick { .. } => Tray::on_left_click(app_handle),
#[cfg(not(target_os = "macos"))]
SystemTrayEvent::LeftClick { .. } => Tray::on_click(app_handle),
#[cfg(target_os = "macos")]
SystemTrayEvent::RightClick { .. } => Tray::on_click(app_handle),
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];

View File

@@ -1,6 +1,6 @@
function main(params) {
if (params.mode === "script") {
params.mode = "rule";
function main(config) {
if (config.mode === "script") {
config.mode = "rule";
}
return params;
return config;
}

View File

@@ -1,10 +1,10 @@
function main(params) {
if (Array.isArray(params.proxies)) {
params.proxies.forEach((p, i) => {
function main(config) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") {
params.proxies[i].alpn = [p.alpn];
config.proxies[i].alpn = [p.alpn];
}
});
}
return params;
return config;
}

View File

@@ -1,64 +1,38 @@
use super::{use_filter, use_lowercase};
use serde_yaml::{self, Mapping, Sequence, Value};
const MERGE_FIELDS: [&str; 10] = [
const MERGE_FIELDS: [&str; 6] = [
"prepend-rules",
"append-rules",
"prepend-rule-providers",
"append-rule-providers",
"prepend-proxies",
"append-proxies",
"prepend-proxy-providers",
"append-proxy-providers",
"prepend-proxy-groups",
"append-proxy-groups",
];
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段
use_lowercase(merge.clone())
.into_iter()
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
.for_each(|(key, value)| {
config.insert(key, value);
});
fn deep_merge(a: &mut Value, b: &Value) {
match (a, b) {
(&mut Value::Mapping(ref mut a), &Value::Mapping(ref b)) => {
for (k, v) in b {
deep_merge(a.entry(k.clone()).or_insert(Value::Null), v);
}
}
(a, b) => *a = b.clone(),
}
}
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config);
let mut merge_without_append = use_lowercase(merge.clone());
for key in MERGE_FIELDS {
merge_without_append.remove(key).unwrap_or_default();
}
deep_merge(&mut config, &Value::from(merge_without_append));
let mut config = config.as_mapping().unwrap().clone();
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
let merge = use_filter(merge, &merge_list.collect());
["rule-providers", "proxy-providers"]
.iter()
.for_each(|key_str| {
let key_val = Value::from(key_str.to_string());
let mut map = Mapping::default();
map = config.get(&key_val).map_or(map.clone(), |val| {
val.as_mapping().map_or(map, |v| v.clone())
});
let pre_key = Value::from(format!("prepend-{key_str}"));
let post_key = Value::from(format!("append-{key_str}"));
if let Some(pre_val) = merge.get(&pre_key) {
if pre_val.is_mapping() {
let mut pre_val = pre_val.as_mapping().unwrap().clone();
pre_val.extend(map);
map = pre_val;
}
}
if let Some(post_val) = merge.get(&post_key) {
if post_val.is_mapping() {
map.extend(post_val.as_mapping().unwrap().clone());
}
}
if !map.is_empty() {
config.insert(key_val, Value::from(map));
}
});
["rules", "proxies", "proxy-groups"]
.iter()
.for_each(|key_str| {

View File

@@ -42,8 +42,19 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("set_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash").args([script]).output() {
Ok(_) => log::info!(target: "app", "set system dns successfully"),
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
@@ -59,8 +70,19 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("unset_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash").args([script]).output() {
Ok(_) => log::info!(target: "app", "unset system dns successfully"),
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}

View File

@@ -185,22 +185,14 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tun_tray_icon = patch.tun_tray_icon;
match {
#[cfg(target_os = "windows")]
{
let service_mode = patch.enable_service_mode;
let service_mode = patch.enable_service_mode;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate()?;
CoreManager::global().run_core().await?;
} else if tun_mode.is_some() {
update_core_config().await?;
}
}
#[cfg(not(target_os = "windows"))]
if tun_mode.is_some() {
Config::generate()?;
CoreManager::global().run_core().await?;
} else if tun_mode.is_some() {
update_core_config().await?;
}
@@ -307,7 +299,7 @@ pub fn copy_clash_env(app_handle: &AppHandle) {
let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut cliboard = app_handle.clipboard_manager();

View File

@@ -20,6 +20,9 @@ fn main() -> std::io::Result<()> {
return Ok(());
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
crate::log_err!(init::init_config());
#[allow(unused_mut)]

View File

@@ -92,12 +92,16 @@ pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid"))
}
#[cfg(not(target_os = "windows"))]
pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service"))
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
}
#[cfg(windows)]
pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local;

View File

@@ -135,13 +135,12 @@ pub fn delete_log() -> Result<()> {
for file in fs::read_dir(&log_dir)?.flatten() {
let _ = process_file(file);
}
#[cfg(target_os = "windows")]
{
let service_log_dir = log_dir.join("service");
for file in fs::read_dir(&service_log_dir)?.flatten() {
let _ = process_file(file);
}
let service_log_dir = log_dir.join("service");
for file in fs::read_dir(&service_log_dir)?.flatten() {
let _ = process_file(file);
}
Ok(())
}
@@ -305,7 +304,7 @@ pub fn startup_script() -> Result<()> {
shell = "powershell";
}
if path.ends_with(".bat") {
shell = "cmd";
shell = "powershell";
}
if shell.is_empty() {
return Err(anyhow::anyhow!("unsupported script: {path}"));

View File

@@ -4,3 +4,4 @@ pub mod init;
pub mod resolve;
pub mod server;
pub mod tmpl;
pub mod unix_helper;

View File

@@ -1,44 +1,35 @@
//! Some config file template
/// template for new a profile item
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge
proxies:
proxies: []
proxy-groups:
proxy-groups: []
rules:
rules: []
";
/// enhanced profile
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
# The `Merge` format used to enhance profile
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
prepend-rules:
prepend-rules: []
prepend-rule-providers:
prepend-proxies: []
prepend-proxies:
prepend-proxy-groups: []
prepend-proxy-providers:
append-rules: []
prepend-proxy-groups:
append-proxies: []
append-rules:
append-rule-providers:
append-proxies:
append-proxy-providers:
append-proxy-groups:
append-proxy-groups: []
";
/// enhanced profile
pub const ITEM_SCRIPT: &str = "// Define the `main` function
pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
function main(params) {
return params;
function main(config) {
return config;
}
";

View File

@@ -0,0 +1,14 @@
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> &'static str {
use std::process::Command;
match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
}
}

View File

@@ -1,7 +1,8 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Clash Verge",
"version": "1.5.11"
"version": "1.6.2"
},
"build": {
"distDir": "../dist",
@@ -65,6 +66,11 @@
},
"path": {
"all": true
},
"fs": {
"exists": true,
"readFile": true,
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"]
}
},
"windows": [],

View File

@@ -1,9 +1,11 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["deb", "appimage", "updater"],
"deb": {
"depends": ["openssl"],

View File

@@ -1,10 +1,12 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/mac-tray-icon.png",
"iconAsTemplate": true
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["app", "dmg", "updater"],
"macOS": {
"frameworks": [],

View File

@@ -1,9 +1,11 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,

View File

@@ -0,0 +1,38 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.arm64/"
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE",
"installMode": "perMachine",
"template": "./template/installer.nsi"
}
}
},
"updater": {
"active": true,
"dialog": false,
"endpoints": [
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x64/"
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE",
"installMode": "perMachine",
"template": "./template/installer.nsi"
}
}
},
"updater": {
"active": true,
"dialog": false,
"endpoints": [
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x86/"
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE",
"installMode": "perMachine",
"template": "./template/installer.nsi"
}
}
},
"updater": {
"active": true,
"dialog": false,
"endpoints": [
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
}
}
}

View File

@@ -33,6 +33,7 @@
// width: 100%;
display: flex;
height: 100%;
padding: 0px 20px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
@@ -79,7 +80,7 @@
> div {
margin: 0 auto;
padding: 0px 10px;
padding: 0px 20px;
}
}
}
@@ -120,9 +121,9 @@
.windows,
.unknown {
&.layout {
.layout__left {
// padding-top: 24px;
}
//.layout__left {
// padding-top: 24px;
//}
.layout__left .the-logo {
flex: 1 0 58px;

View File

@@ -0,0 +1,24 @@
import { TextField, type TextFieldProps, styled } from "@mui/material";
import { useTranslation } from "react-i18next";
export const BaseStyledTextField = styled((props: TextFieldProps) => {
const { t } = useTranslation();
return (
<TextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
sx={{ input: { py: 0.65, px: 1.25 } }}
{...props}
/>
);
})(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
},
}));

View File

@@ -3,8 +3,8 @@ import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Snackbar } from "@mui/material";
import { deleteConnection } from "@/services/api";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
@@ -69,7 +69,9 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{ label: "Rule", value: rule },
{
label: "Process",
value: truncateStr(metadata.process || metadata.processPath),
value: `${metadata.process}${
metadata.processPath ? `(${metadata.processPath})` : ""
}`,
},
{ label: "Time", value: dayjs(data.start).fromNow() },
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
@@ -96,7 +98,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
onClose?.();
}}
>
Close
{t("Close")}
</Button>
</Box>
</Box>

View File

@@ -8,16 +8,29 @@ import {
PushPinOutlined,
PushPinRounded,
} from "@mui/icons-material";
import { useState } from "react";
import { useEffect, useState } from "react";
export const LayoutControl = () => {
const minWidth = 40;
const [isMaximized, setIsMaximized] = useState(false);
const [isPined, setIsPined] = useState(false);
appWindow.isMaximized().then((isMaximized) => {
setIsMaximized(() => isMaximized);
});
useEffect(() => {
const unlistenResized = appWindow.onResized(() => {
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
});
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
return () => {
unlistenResized.then((fn) => fn());
};
}, []);
return (
<ButtonGroup

View File

@@ -29,21 +29,23 @@ export const LayoutTraffic = () => {
// setup log ws during layout
useLogSetup();
const { connect, disconnect } = useWebsocket((event) => {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
setTraffic(data);
});
const trafficWs = useWebsocket(
(event) => {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
setTraffic(data);
},
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
);
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
return () => {
disconnect();
};
trafficWs.connect(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
);
return () => trafficWs.disconnect();
}, [clashInfo, pageVisible]);
/* --------- meta memory information --------- */
@@ -54,7 +56,7 @@ export const LayoutTraffic = () => {
(event) => {
setMemory(JSON.parse(event.data));
},
{ onError: () => setMemory({ inuse: 0 }) }
{ onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
);
useEffect(() => {

View File

@@ -29,7 +29,7 @@ export const ConfirmViewer = (props: Props) => {
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>{t(title)}</DialogTitle>
<DialogContent sx={{ width: "95%", pb: 1, userSelect: "text" }}>
<DialogContent sx={{ pb: 1, userSelect: "text" }}>
{t(message)}
</DialogContent>

View File

@@ -12,23 +12,45 @@ import {
import { atomThemeMode } from "@/services/states";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice } from "@/components/base";
import { nanoid } from "nanoid";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js";
import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js";
import * as monaco from "monaco-editor";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import { configureMonacoYaml } from "monaco-yaml";
import { type JSONSchema7 } from "json-schema";
import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json";
interface Props {
uid: string;
open: boolean;
mode: "yaml" | "javascript";
language: "yaml" | "javascript";
schema?: "clash" | "merge";
onClose: () => void;
onChange?: () => void;
}
export const EditorViewer = (props: Props) => {
const { uid, open, mode, onClose, onChange } = props;
// yaml worker
configureMonacoYaml(monaco, {
validate: true,
enableSchemaRequest: true,
schemas: [
{
uri: "http://example.com/meta-json-schema.json",
fileMatch: ["**/*.clash.yaml"],
schema: metaSchema as JSONSchema7,
},
{
uri: "http://example.com/clash-verge-merge-json-schema.json",
fileMatch: ["**/*.merge.yaml"],
schema: mergeSchema as JSONSchema7,
},
],
});
export const EditorViewer = (props: Props) => {
const { uid, open, language, schema, onClose, onChange } = props;
const { t } = useTranslation();
const editorRef = useRef<any>();
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
@@ -41,13 +63,23 @@ export const EditorViewer = (props: Props) => {
const dom = editorRef.current;
if (!dom) return;
if (instanceRef.current) instanceRef.current.dispose();
const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
const model = monaco.editor.createModel(data, language, uri);
instanceRef.current = editor.create(editorRef.current, {
value: data,
language: mode,
model: model,
language: language,
tabSize: ["yaml", "javascript"].includes(language) ? 2 : 4, // 根据语言类型设置缩进
theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false },
minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
mouseWheelZoom: true, // Ctrl+滚轮调节缩放
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
});
});
@@ -77,8 +109,10 @@ export const EditorViewer = (props: Props) => {
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{t("Edit File")}</DialogTitle>
<DialogContent sx={{ width: "95%", pb: 1, userSelect: "text" }}>
<div style={{ width: "100%", height: "500px" }} ref={editorRef} />
<DialogContent
sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
>
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
</DialogContent>
<DialogActions>

View File

@@ -24,7 +24,7 @@ import { EditorViewer } from "./editor-viewer";
import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic";
import { ConfirmViewer } from "./confirm-viewer";
import { open } from "@tauri-apps/api/shell";
const round = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@@ -55,6 +55,7 @@ export const ProfileItem = (props: Props) => {
// remote file mode
const hasUrl = !!itemData.url;
const hasExtra = !!extra; // only subscription url has extra info
const hasHome = !!itemData.home; // only subscription url has home page
const { upload = 0, download = 0, total = 0 } = extra ?? {};
const from = parseUrl(itemData.url);
@@ -95,6 +96,11 @@ export const ProfileItem = (props: Props) => {
const [fileOpen, setFileOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const onOpenHome = () => {
setAnchorEl(null);
open(itemData.home ?? "");
};
const onEditInfo = () => {
setAnchorEl(null);
onEdit();
@@ -166,7 +172,9 @@ export const ProfileItem = (props: Props) => {
}
});
const urlModeMenu = [
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : []
).concat([
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
@@ -180,7 +188,7 @@ export const ProfileItem = (props: Props) => {
setConfirmOpen(true);
},
},
];
]);
const fileModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
@@ -378,7 +386,8 @@ export const ProfileItem = (props: Props) => {
<EditorViewer
uid={uid}
open={fileOpen}
mode="yaml"
language="yaml"
schema="clash"
onClose={() => setFileOpen(false)}
/>
<ConfirmViewer

View File

@@ -235,7 +235,8 @@ export const ProfileMore = (props: Props) => {
<EditorViewer
uid={uid}
open={fileOpen}
mode={type === "merge" ? "yaml" : "javascript"}
language={type === "merge" ? "yaml" : "javascript"}
schema={type === "merge" ? "merge" : undefined}
onClose={() => setFileOpen(false)}
/>
<ConfirmViewer

View File

@@ -97,6 +97,8 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
}
if (form.option?.update_interval) {
form.option.update_interval = +form.option.update_interval;
} else {
delete form.option?.update_interval;
}
if (form.option?.user_agent === "") {
delete form.option.user_agent;

View File

@@ -6,8 +6,8 @@ import {
providerHealthCheck,
updateProxy,
deleteConnection,
getGroupProxyDelays,
} from "@/services/api";
import { Box } from "@mui/material";
import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge";
import { BaseEmpty } from "../base";
@@ -33,7 +33,7 @@ export const ProxyGroups = (props: Props) => {
// 切换分组的节点代理
const handleChangeProxy = useLockFn(
async (group: IProxyGroupItem, proxy: IProxyItem) => {
if (group.type !== "Selector" && group.type !== "Fallback") return;
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
const { name, now } = group;
await updateProxy(name, proxy.name);
@@ -85,7 +85,11 @@ export const ProxyGroups = (props: Props) => {
}
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
await delayManager.checkListDelay(names, groupName, timeout);
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果)
]);
onProxies();
});

View File

@@ -109,7 +109,7 @@ export const ProxyHead = (props: Props) => {
<IconButton
size="small"
color="inherit"
title={t("Proxy detail")}
title={showType ? t("Proxy basic") : t("Proxy detail")}
onClick={() => onHeadState({ showType: !showType })}
>
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}

View File

@@ -7,7 +7,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
group: IProxyGroupItem;
proxy: IProxyItem;
selected: boolean;
showType?: boolean;
@@ -16,7 +16,7 @@ interface Props {
// 多列布局
export const ProxyItemMini = (props: Props) => {
const { groupName, proxy, selected, showType = true, onClick } = props;
const { group, proxy, selected, showType = true, onClick } = props;
// -1/<=0 为 不显示
// -2 为 loading
@@ -25,21 +25,21 @@ export const ProxyItemMini = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, groupName]);
}, [proxy.name, group.name]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName));
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
});
return (
@@ -65,6 +65,13 @@ export const ProxyItemMini = (props: Props) => {
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
"&:hover .the-icon": { display: "none" },
"& .the-pin, & .the-unpin": {
position: "absolute",
fontSize: "12px",
top: "-5px",
right: "-5px",
},
"& .the-unpin": { filter: "grayscale(1)" },
"&.Mui-selected": {
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
@@ -79,7 +86,10 @@ export const ProxyItemMini = (props: Props) => {
},
]}
>
<Box title={proxy.name} sx={{ overflow: "hidden" }}>
<Box
title={`${proxy.name}\n${proxy.now ?? ""}`}
sx={{ overflow: "hidden" }}
>
<Typography
variant="body2"
component="div"
@@ -147,14 +157,12 @@ export const ProxyItemMini = (props: Props) => {
</Box>
)}
</Box>
<Box sx={{ ml: 0.5, color: "primary.main" }}>
{delay === -2 && (
<Widget>
<BaseLoading />
</Widget>
)}
{!proxy.provider && delay !== -2 && (
// provider的节点不支持检测
<Widget
@@ -193,7 +201,6 @@ export const ProxyItemMini = (props: Props) => {
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
{delay !== -2 && delay <= 0 && selected && (
// 展示已选择的icon
<CheckCircleOutlineRounded
@@ -202,6 +209,13 @@ export const ProxyItemMini = (props: Props) => {
/>
)}
</Box>
{group.fixed && group.fixed === proxy.name && (
// 展示fixed状态
<span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
📌
</span>
)}
</ListItemButton>
);
};

View File

@@ -17,7 +17,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
group: IProxyGroupItem;
proxy: IProxyItem;
selected: boolean;
showType?: boolean;
@@ -44,7 +44,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
}));
export const ProxyItem = (props: Props) => {
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
const { group, proxy, selected, showType = true, sx, onClick } = props;
// -1/<=0 为 不显示
// -2 为 loading
@@ -52,21 +52,21 @@ export const ProxyItem = (props: Props) => {
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, groupName]);
}, [proxy.name, group.name]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName));
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
});
return (

View File

@@ -142,7 +142,7 @@ export const ProxyRender = (props: RenderProps) => {
if (type === 2 && !group.hidden) {
return (
<ProxyItem
groupName={group.name}
group={group}
proxy={proxy!}
selected={group.now === proxy?.name}
showType={headState?.showType}
@@ -186,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
{proxyCol?.map((proxy) => (
<ProxyItemMini
key={item.key + proxy.name}
groupName={group.name}
group={group}
proxy={proxy!}
selected={group.now === proxy.name}
showType={headState?.showType}

View File

@@ -97,19 +97,17 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<Box display="flex" justifyContent="space-between">
{t("Clash Core")}
<Box>
{clash_core !== "clash-meta" && (
<LoadingButton
variant="contained"
size="small"
startIcon={<SwitchAccessShortcut />}
loadingPosition="start"
loading={upgrading}
sx={{ marginRight: "8px" }}
onClick={onUpgrade}
>
{t("Upgrade")}
</LoadingButton>
)}
<LoadingButton
variant="contained"
size="small"
startIcon={<SwitchAccessShortcut />}
loadingPosition="start"
loading={upgrading}
sx={{ marginRight: "8px" }}
onClick={onUpgrade}
>
{t("Upgrade")}
</LoadingButton>
<Button
variant="contained"
size="small"

View File

@@ -7,10 +7,17 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
import { useRecoilValue } from "recoil";
import { Chip } from "@mui/material";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Chip,
} from "@mui/material";
import { atomThemeMode } from "@/services/states";
import { getRuntimeYaml } from "@/services/cmds";
import { BaseDialog, DialogRef } from "@/components/base";
import { DialogRef } from "@/components/base";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
@@ -47,9 +54,12 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
instanceRef.current = editor.create(editorRef.current, {
value: data ?? "# Error\n",
language: "yaml",
tabSize: 2,
theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false },
readOnly: true,
minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
mouseWheelZoom: true, // Ctrl+滚轮调节缩放
readOnly: true, // 只读
readOnlyMessage: { value: t("ReadOnlyMessage") },
});
});
},
@@ -57,20 +67,22 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
}));
return (
<BaseDialog
open={open}
title={
<>
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
</>
}
contentSx={{ width: 520, pb: 1, userSelect: "text" }}
cancelBtn={t("Back")}
disableOk
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<div style={{ width: "100%", height: "420px" }} ref={editorRef} />
</BaseDialog>
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="xl" fullWidth>
<DialogTitle>
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
</DialogTitle>
<DialogContent
sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
>
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} variant="outlined">
{t("Back")}
</Button>
</DialogActions>
</Dialog>
);
});

View File

@@ -46,7 +46,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="External Controller" />
<ListItemText primary={t("External Controller")} />
<TextField
size="small"
autoComplete="off"
@@ -58,14 +58,16 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Core Secret" />
<ListItemText primary={t("Core Secret")} />
<TextField
size="small"
autoComplete="off"
sx={{ width: 175 }}
value={secret}
placeholder="Recommended"
onChange={(e) => setSecret(e.target.value)}
placeholder={t("Recommended")}
onChange={(e) =>
setSecret(e.target.value?.replace(/[^\x00-\x7F]/g, ""))
}
/>
</ListItem>
</List>

View File

@@ -9,6 +9,7 @@ import { open as openDialog } from "@tauri-apps/api/dialog";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { copyIconFile, getAppDir } from "@/services/cmds";
import { join } from "@tauri-apps/api/path";
import { exists } from "@tauri-apps/api/fs";
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -26,12 +27,27 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
async function initIconPath() {
const appDir = await getAppDir();
const icon_dir = await join(appDir, "icons");
const common_icon = await join(icon_dir, "common.png");
const sysproxy_icon = await join(icon_dir, "sysproxy.png");
const tun_icon = await join(icon_dir, "tun.png");
setCommonIcon(common_icon);
setSysproxyIcon(sysproxy_icon);
setTunIcon(tun_icon);
const common_icon_png = await join(icon_dir, "common.png");
const common_icon_ico = await join(icon_dir, "common.ico");
const sysproxy_icon_png = await join(icon_dir, "sysproxy.png");
const sysproxy_icon_ico = await join(icon_dir, "sysproxy.ico");
const tun_icon_png = await join(icon_dir, "tun.png");
const tun_icon_ico = await join(icon_dir, "tun.ico");
if (await exists(common_icon_ico)) {
setCommonIcon(common_icon_ico);
} else {
setCommonIcon(common_icon_png);
}
if (await exists(sysproxy_icon_ico)) {
setSysproxyIcon(sysproxy_icon_ico);
} else {
setSysproxyIcon(sysproxy_icon_png);
}
if (await exists(tun_icon_ico)) {
setTunIcon(tun_icon_ico);
} else {
setTunIcon(tun_icon_png);
}
}
useImperativeHandle(ref, () => ({
@@ -140,12 +156,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
extensions: ["png", "ico"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "common.png");
await copyIconFile(`${path}`, "common");
await initIconPath();
onChangeData({ common_tray_icon: true });
patchVerge({ common_tray_icon: true });
}
@@ -184,12 +201,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
extensions: ["png", "ico"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "sysproxy.png");
await copyIconFile(`${path}`, "sysproxy");
await initIconPath();
onChangeData({ sysproxy_tray_icon: true });
patchVerge({ sysproxy_tray_icon: true });
}
@@ -226,12 +244,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
extensions: ["png", "ico"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "tun.png");
await copyIconFile(`${path}`, "tun");
await initIconPath();
onChangeData({ tun_tray_icon: true });
patchVerge({ tun_tray_icon: true });
}

View File

@@ -86,12 +86,15 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
disableFooter
onClose={() => setOpen(false)}
>
<Typography>Current State: {state}</Typography>
<Typography>
{t("Current State")}: {t(state)}
</Typography>
{(state === "unknown" || state === "uninstall") && (
<Typography>
Information: Please make sure that the Clash Verge Service is
installed and enabled
{t(
"Information: Please make sure that the Clash Verge Service is installed and enabled"
)}
</Typography>
)}
@@ -102,19 +105,19 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
>
{state === "uninstall" && enable && (
<Button variant="contained" onClick={onDisable}>
Disable Service Mode
{t("Disable Service Mode")}
</Button>
)}
{state === "uninstall" && (
<Button variant="contained" onClick={onInstall}>
Install
{t("Install")}
</Button>
)}
{(state === "active" || state === "installed") && (
<Button variant="outlined" onClick={onUninstall}>
Uninstall
{t("Uninstall")}
</Button>
)}
</Stack>

View File

@@ -128,6 +128,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
rows={3}
sx={{ width: 280 }}
value={value.bypass}
placeholder={sysproxy?.bypass || `-`}
onChange={(e) =>
setValue((v) => ({ ...v, bypass: e.target.value }))
}

View File

@@ -13,6 +13,7 @@ import {
import { useClash } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { StackModeSwitch } from "./stack-mode-switch";
import { enhanceProfiles } from "@/services/cmds";
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -27,7 +28,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
mtu: 1500,
});
useImperativeHandle(ref, () => ({
@@ -40,7 +41,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
strictRoute: clash?.tun["strict-route"] ?? false,
mtu: clash?.tun.mtu ?? 9000,
mtu: clash?.tun.mtu ?? 1500,
});
},
close: () => setOpen(false),
@@ -50,12 +51,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
try {
let tun = {
stack: values.stack,
device: values.device,
device: values.device === "" ? "Meta" : values.device,
"auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack,
"dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack,
"strict-route": values.strictRoute,
mtu: values.mtu,
mtu: values.mtu ?? 1500,
};
await patchClash({ tun });
await mutateClash(
@@ -65,6 +66,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
}),
false
);
try {
await enhanceProfiles();
Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
}
setOpen(false);
} catch (err: any) {
Notice.error(err.message || err.toString());
@@ -88,7 +95,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
"auto-detect-interface": true,
"dns-hijack": ["any:53"],
"strict-route": false,
mtu: 9000,
mtu: 1500,
};
setValues({
stack: "gvisor",
@@ -97,7 +104,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
mtu: 1500,
});
await patchClash({ tun });
await mutateClash(
@@ -208,7 +215,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false"
sx={{ width: 250 }}
value={values.mtu}
placeholder="9000"
placeholder="1500"
onChange={(e) =>
setValues((v) => ({
...v,

View File

@@ -1,8 +1,7 @@
import useSWR from "swr";
import snarkdown from "snarkdown";
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
import { useLockFn } from "ahooks";
import { Box, LinearProgress, styled } from "@mui/material";
import { Box, LinearProgress } from "@mui/material";
import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next";
import { relaunch } from "@tauri-apps/api/process";
@@ -11,10 +10,8 @@ import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
import { portableFlag } from "@/pages/_layout";
import ReactMarkdown from "react-markdown";
const UpdateLog = styled(Box)(() => ({
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
}));
let eventListener: UnlistenFn | null = null;
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
@@ -38,12 +35,11 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false),
}));
// markdown parser
const parseContent = useMemo(() => {
const markdownContent = useMemo(() => {
if (!updateInfo?.manifest?.body) {
return "New Version is available";
}
return snarkdown(updateInfo?.manifest?.body);
return updateInfo?.manifest?.body;
}, [updateInfo]);
const onUpdate = useLockFn(async () => {
@@ -87,10 +83,22 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)}
onOk={onUpdate}
>
<UpdateLog
dangerouslySetInnerHTML={{ __html: parseContent }}
sx={{ height: "calc(100% - 10px)", overflow: "auto" }}
/>
<Box sx={{ height: "calc(100% - 10px)", overflow: "auto" }}>
<ReactMarkdown
components={{
a: ({ node, ...props }) => {
const { children } = props;
return (
<a {...props} target="_blank">
{children}
</a>
);
},
}}
>
{markdownContent}
</ReactMarkdown>
</Box>
{updateState && (
<LinearProgress
variant="buffer"

View File

@@ -90,6 +90,8 @@ export const WebUIItem = (props: Props) => {
title={value}
color={value ? "text.primary" : "text.secondary"}
sx={({ palette }) => ({
overflow: "hidden",
textOverflow: "ellipsis",
"> span": {
color: palette.primary.main,
},

View File

@@ -17,23 +17,17 @@ interface Props {
onError?: (err: Error) => void;
}
const isWIN = getSystem() === "windows";
const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge();
// service mode
const { data: serviceStatus } = useSWR(
isWIN ? "checkService" : null,
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const { data: serviceStatus } = useSWR("checkService", checkService, {
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
});
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null);
@@ -56,20 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
{isWIN && (
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<SettingItem
label={t("Tun Mode")}
extra={
<>
<Tooltip
title={
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
}
placement="top"
>
<Tooltip title={t("Tun Mode Info")} placement="top">
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
@@ -102,39 +89,37 @@ const SettingSystem = ({ onError }: Props) => {
</GuardState>
</SettingItem>
{isWIN && (
<SettingItem
label={t("Service Mode")}
extra={
<IconButton
color="inherit"
size="small"
onClick={() => serviceRef.current?.open()}
>
<PrivacyTipRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
<SettingItem
label={t("Service Mode")}
extra={
<IconButton
color="inherit"
size="small"
onClick={() => serviceRef.current?.open()}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
<PrivacyTipRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</GuardState>
</SettingItem>
)}
</IconButton>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
/>
</GuardState>
</SettingItem>
<SettingItem
label={t("System Proxy")}

View File

@@ -157,8 +157,8 @@ const SettingVerge = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ start_page: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; link: string }) => {
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
{routers.map((page: { label: string; path: string }) => {
return <MenuItem value={page.path}>{t(page.label)}</MenuItem>;
})}
</Select>
</GuardState>

View File

@@ -104,7 +104,6 @@ export const TestItem = (props: Props) => {
}}
>
<TestBox
onClick={onEditTest}
onContextMenu={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });

View File

@@ -5,7 +5,8 @@ export type WsMsgFn = (event: MessageEvent<any>) => void;
export interface WsOptions {
errorCount?: number; // default is 5
retryInterval?: number; // default is 2500
onError?: () => void;
onError?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
}
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
@@ -33,17 +34,23 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.addEventListener("message", onMessage);
ws.addEventListener("error", () => {
ws.addEventListener("message", (event) => {
errorCount = 0; // reset counter
onMessage(event);
});
ws.addEventListener("error", (event) => {
errorCount -= 1;
if (errorCount >= 0) {
timerRef.current = setTimeout(connectHelper, 2500);
} else {
disconnect();
options?.onError?.();
options?.onError?.(event);
}
});
ws.addEventListener("close", (event) => {
options?.onClose?.(event);
});
};
connectHelper();

View File

@@ -18,6 +18,8 @@
"direct": "direct",
"script": "script",
"Create Test": "Create Test",
"Edit Test": "Edit Test",
"Edit": "Edit",
"Icon": "Icon",
"Test URL": "Test URL",
@@ -29,7 +31,9 @@
"New": "New",
"Create Profile": "Create Profile",
"Choose File": "Choose File",
"Close": "Close",
"Close All": "Close All",
"Home": "Home",
"Select": "Select",
"Edit Info": "Edit Info",
"Edit File": "Edit File",
@@ -45,6 +49,8 @@
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles",
"Confirm deletion": "Confirm deletion",
"This operation is not reversible": "This operation is not reversible",
"Location": "Location",
"Delay check": "Delay check",
@@ -52,6 +58,7 @@
"Sort by delay": "Sort by delay",
"Sort by name": "Sort by name",
"Delay check URL": "Delay check URL",
"Proxy basic": "Proxy basic",
"Proxy detail": "Proxy detail",
"Filter": "Filter",
"Filter conditions": "Filter conditions",
@@ -80,6 +87,9 @@
"Random Port": "Random Port",
"After restart to take effect": "After restart to take effect",
"External": "External",
"External Controller": "External Controller",
"Core Secret": "Core Secret",
"Recommended": "Recommended",
"Clash Core": "Clash Core",
"Grant": "Grant",
"Tun mode requires": "Tun mode requires",
@@ -94,10 +104,10 @@
"Proxy Guard": "Proxy Guard",
"Guard Duration": "Guard Duration",
"Proxy Bypass": "Proxy Bypass",
"Current System Proxy": "Current System Proxy",
"Enable status": "Enable status",
"Server Addr": "Server Addr",
"Bypass": "Bypass",
"Current System Proxy": "Current System Proxy",
"Theme Mode": "Theme Mode",
"Tray Click Event": "Tray Click Event",
"Copy Env Type": "Copy Env Type",
@@ -112,6 +122,9 @@
"Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Proxy Group Icon": "Proxy Group Icon",
"Menu Icon": "Menu Icon",
"Monochrome": "Monochrome",
"Colorful": "Colorful",
"Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon",
@@ -120,6 +133,7 @@
"Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
"Open Dev Tools": "Open Dev Tools",
"Verge Version": "Verge Version",
"theme.light": "Light",
"theme.dark": "Dark",
@@ -127,6 +141,7 @@
"Clash Field": "Clash Field",
"Runtime Config": "Runtime Config",
"ReadOnly": "ReadOnly",
"ReadOnlyMessage": "Cannot edit in read-only editor",
"Restart": "Restart",
"Upgrade": "Upgrade",
@@ -134,6 +149,7 @@
"Save": "Save",
"Cancel": "Cancel",
"Exit": "Exit",
"Confirm": "Confirm",
"Default": "Default",
"Download Speed": "Download Speed",
@@ -148,11 +164,11 @@
"App Log Level": "App Log Level",
"Auto Close Connections": "Auto Close Connections",
"Enable Clash Fields Filter": "Enable Clash Fields Filter",
"Auto Check Update": "Auto Check Update",
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Proxy Layout Column": "Proxy Layout Column",
"Default Latency Test": "Default Latency Test",
"Defaule Latency Timeout": "Defaule Latency Timeout",
"Default Latency Timeout": "Default Latency Timeout",
"Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean",
@@ -167,9 +183,22 @@
"Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit",
"Reset to Default": "Reset to Default",
"Current State": "Current State",
"pending": "pending",
"installed": "installed",
"uninstall": "uninstalled",
"active": "active",
"unknown": "unknown",
"Disable Service Mode": "Disable Service Mode",
"Install": "Install",
"Uninstall": "Uninstall",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode"
"Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled",
"Use Regular Expression": "Use Regular Expression"
}

View File

@@ -11,13 +11,15 @@
"Logs": "Логи",
"Clear": "Очистить",
"Proxies": "Прокси",
"Test": "Тест",
"Proxy Groups": "Группы прокси",
"Test": "Тест",
"rule": "правила",
"global": "глобальный",
"direct": "прямой",
"script": "скриптовый",
"Create Test": "Создать тест",
"Edit Test": "Редактировать тест",
"Edit": "Редактировать",
"Icon": "Икона",
"Test URL": "Тестовый URL",
@@ -29,7 +31,9 @@
"New": "Новый",
"Create Profile": "Создать профиль",
"Choose File": "Выбрать файл",
"Close": "Закрыть",
"Close All": "Закрыть всё",
"Home": "Главная",
"Select": "Выбрать",
"Edit Info": "Изменить информацию",
"Edit File": "Изменить файл",
@@ -45,6 +49,8 @@
"Update All Profiles": "Обновить все профили",
"View Runtime Config": "Просмотреть используемый конфиг",
"Reactivate Profiles": "Реактивировать профили",
"Confirm deletion": "Подтвердите удаление",
"This operation is not reversible": "Эта операция необратима",
"Location": "Местоположение",
"Delay check": "Проверка задержки",
@@ -52,6 +58,7 @@
"Sort by delay": "Сортировать по задержке",
"Sort by name": "Сортировать по названию",
"Delay check URL": "URL проверки задержки",
"Proxy basic": "Резюме о прокси",
"Proxy detail": "Подробности о прокси",
"Filter": "Фильтр",
"Filter conditions": "Условия фильтрации",
@@ -79,7 +86,13 @@
"Port Config": "Настройка порта",
"Random Port": "Случайный порт",
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
"External": "Внешний",
"External Controller": "Адрес прослушивания внешнего контроллера",
"Core Secret": "Секрет",
"Recommended": "Рекомендуется",
"Clash Core": "Ядро Clash",
"Grant": "Предоставить",
"Tun mode requires": "Требуется Режим туннеля",
"Tun Mode": "Режим туннеля",
"Service Mode": "Режим сервиса",
"Auto Launch": "Автозапуск",
@@ -92,20 +105,35 @@
"Guard Duration": "Период защиты",
"Proxy Bypass": "Игнорирование прокси",
"Current System Proxy": "Текущий системный прокси",
"Enable status": "Статус включения",
"Server Addr": "Адрес сервера",
"Bypass": "Игнорирование",
"Theme Mode": "Режим темы",
"Tray Click Event": "Событие щелчка в лотке",
"Start Page": "Главная страница",
"Copy Env Type": "Скопировать тип Env",
"Start Page": "Главная страница",
"Startup Script": "Скрипт запуска",
"Browse": "Просмотреть",
"Show Main Window": "Показать главное окно",
"Theme Setting": "Настройка темы",
"Layout Setting": "Настройка раскладки",
"Miscellaneous": "Настройка различные",
"Hotkey Setting": "Настройка клавиатурных сокращений",
"Traffic Graph": "График трафика",
"Memory Usage": "Использование памяти",
"Proxy Group Icon": "Иконка Группы прокси",
"Menu Icon": "Иконка меню",
"Monochrome": "Монохромный",
"Colorful": "Полноцветный",
"Common Tray Icon": "Общий значок в лотке",
"System Proxy Tray Icon": "Значок системного прокси в лотке",
"Tun Tray Icon": "Значок туннеля в лотке",
"Language": "Язык",
"Open App Dir": "Открыть папку приложения",
"Open Core Dir": "Открыть папку ядра",
"Open Logs Dir": "Открыть папку логов",
"Check for Updates": "Проверить обновления",
"Open Dev Tools": "Открыть инструменты разработчика",
"Verge Version": "Версия Verge",
"theme.light": "Светлая",
"theme.dark": "Тёмная",
@@ -113,6 +141,7 @@
"Clash Field": "Используемые настройки Clash",
"Runtime Config": "Используемый конфиг",
"ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Restart": "Перезапуск",
"Upgrade": "Обновлять",
@@ -120,6 +149,11 @@
"Save": "Сохранить",
"Cancel": "Отмена",
"Exit": "Выход",
"Confirm": "Подтвердить",
"Default": "По умолчанию",
"Download Speed": "Скорость загрузки",
"Upload Speed": "Скорость загрузки",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Режим правил",
@@ -128,5 +162,43 @@
"toggle_system_proxy": "Включить/Отключить системный прокси",
"toggle_tun_mode": "Включить/Отключить режим туннеля",
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную"
"App Log Level": "Уровень журнала приложения",
"Auto Close Connections": "Автоматическое закрытие соединений",
"Auto Check Update": "Автоматическая проверка обновлений",
"Enable Builtin Enhanced": "Включить встроенные улучшения",
"Proxy Layout Column": "Количество столбцов в макете прокси",
"Default Latency Test": "Ссылка на тестирование задержки по умолчанию",
"Default Latency Timeout": "Таймаут задержки по умолчанию",
"Auto Log Clean": "Автоматическая очистка журналов",
"Never Clean": "Никогда не очищать",
"Retain 7 Days": "Сохранять 7 дней",
"Retain 30 Days": "Сохранять 30 дней",
"Retain 90 Days": "Сохранять 90 дней",
"Stack": "Стек",
"Device": "Имя устройства",
"Auto Route": "Автоматическая маршрутизация",
"Strict Route": "Строгий маршрут",
"Auto Detect Interface": "Автоопределение интерфейса",
"DNS Hijack": "DNS-перехват",
"MTU": "Максимальная единица передачи",
"Reset to Default": "Сбросить настройки по умолчанию",
"Current State": "Текущее состояние",
"pending": "Ожидающий",
"installed": "Установленный",
"uninstall": "Не установленный",
"active": "Активированный",
"unknown": "неизвестный",
"Disable Service Mode": "Отключить режим обслуживания",
"Install": "Установить",
"Uninstall": "Удалить",
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
"Tun Mode Info": "Режим туннеля требует предоставления разрешений, связанных с ядром. Пожалуйста, включите сервисный режим перед его использованием",
"System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен",
"Use Regular Expression": "Использование регулярных выражений"
}

View File

@@ -18,6 +18,8 @@
"direct": "直连",
"script": "脚本",
"Create Test": "新建测试",
"Edit Test": "编辑测试",
"Edit": "编辑",
"Icon": "图标",
"Test URL": "测试地址",
@@ -29,7 +31,9 @@
"New": "新建",
"Create Profile": "新建订阅",
"Choose File": "选择文件",
"Close": "关闭",
"Close All": "关闭全部",
"Home": "首页",
"Select": "使用",
"Edit Info": "编辑信息",
"Edit File": "编辑文件",
@@ -54,6 +58,7 @@
"Sort by delay": "按延迟排序",
"Sort by name": "按名称排序",
"Delay check URL": "延迟测试链接",
"Proxy basic": "隐藏节点细节",
"Proxy detail": "展示节点细节",
"Filter": "过滤节点",
"Filter conditions": "过滤条件",
@@ -82,6 +87,9 @@
"Random Port": "随机端口",
"After restart to take effect": "重启后生效",
"External": "外部控制",
"External Controller": "外部控制器监听地址",
"Core Secret": "API访问密钥",
"Recommended": "建议设置",
"Clash Core": "Clash 内核",
"Grant": "授权",
"Tun mode requires": "如需启用TUN模式需要授权",
@@ -133,6 +141,7 @@
"Clash Field": "Clash 字段",
"Runtime Config": "当前配置",
"ReadOnly": "只读",
"ReadOnlyMessage": "无法在只读模式下编辑",
"Restart": "重启内核",
"Upgrade": "升级内核",
@@ -176,8 +185,20 @@
"MTU": "最大传输单元",
"Reset to Default": "重置为默认值",
"Current State": "当前状态",
"pending": "等待中",
"installed": "已安装",
"uninstall": "未安装",
"active": "已激活",
"unknown": "未知",
"Disable Service Mode": "禁用服务模式",
"Install": "安装",
"Uninstall": "卸载",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限使用前请先开启服务模式",
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限使用前请先在内核设置中给内核授权",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用"
"Tun Mode Info": "Tun模式需要授予内核相关权限使用前请先开启服务模式",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "提示信息: 请确保Clash Verge Service已安装并启用",
"Use Regular Expression": "使用正则表达式"
}

View File

@@ -24,6 +24,19 @@ if (!container) {
);
}
document.addEventListener("keydown", (event) => {
// Disable WebView keyboard shortcuts
if (["F5", "F7"].includes(event.key)) {
event.preventDefault();
}
if (
(event.ctrlKey || event.metaKey) &&
["F", "H", "P", "R", "U"].includes(event.key.toUpperCase())
) {
event.preventDefault();
}
});
createRoot(container).render(
<React.StrictMode>
<RecoilRoot>

View File

@@ -4,9 +4,8 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Route, Routes, useLocation } from "react-router-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { alpha, List, Paper, ThemeProvider } from "@mui/material";
import { useLocation, useRoutes } from "react-router-dom";
import { List, Paper, ThemeProvider } from "@mui/material";
import { listen } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window";
import { routers } from "./_routers";
@@ -16,7 +15,7 @@ import LogoSvg from "@/assets/image/logo.svg?react";
import LogoSvg_dark from "@/assets/image/logo_dark.svg?react";
import { atomThemeMode } from "@/services/states";
import { useRecoilState } from "recoil";
import { BaseErrorBoundary, Notice } from "@/components/base";
import { Notice } from "@/components/base";
import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutControl } from "@/components/layout/layout-control";
import { LayoutTraffic } from "@/components/layout/layout-traffic";
@@ -27,6 +26,8 @@ import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
import { getPortableFlag } from "@/services/cmds";
import { useNavigate } from "react-router-dom";
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
export let portableFlag = false;
dayjs.extend(relativeTime);
@@ -43,6 +44,8 @@ const Layout = () => {
const { language, start_page } = verge || {};
const navigate = useNavigate();
const location = useLocation();
const routersEles = useRoutes(routers);
if (!routersEles) return null;
useEffect(() => {
window.addEventListener("keydown", (e) => {
@@ -142,7 +145,7 @@ const Layout = () => {
{routers.map((router) => (
<LayoutItem
key={router.label}
to={router.link}
to={router.path}
icon={router.icon}
>
{t(router.label)}
@@ -173,19 +176,7 @@ const Layout = () => {
timeout={300}
classNames="page"
>
<Routes>
{routers.map(({ label, link, ele: Ele }) => (
<Route
key={label}
path={link}
element={
<BaseErrorBoundary key={label}>
<Ele />
</BaseErrorBoundary>
}
/>
))}
</Routes>
{React.cloneElement(routersEles, { key: location.pathname })}
</CSSTransition>
</TransitionGroup>
</div>

View File

@@ -5,6 +5,7 @@ import ProfilesPage from "./profiles";
import SettingsPage from "./settings";
import ConnectionsPage from "./connections";
import RulesPage from "./rules";
import { BaseErrorBoundary } from "@/components/base";
import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react";
import ProfilesSvg from "@/assets/image/itemicon/profiles.svg?react";
@@ -25,44 +26,49 @@ import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded";
export const routers = [
{
label: "Label-Proxies",
link: "/",
path: "/",
icon: [<WifiRoundedIcon />, <ProxiesSvg />],
ele: ProxiesPage,
element: <ProxiesPage />,
},
{
label: "Label-Profiles",
link: "/profile",
path: "/profile",
icon: [<DnsRoundedIcon />, <ProfilesSvg />],
ele: ProfilesPage,
element: <ProfilesPage />,
},
{
label: "Label-Connections",
link: "/connections",
path: "/connections",
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />],
ele: ConnectionsPage,
element: <ConnectionsPage />,
},
{
label: "Label-Rules",
link: "/rules",
path: "/rules",
icon: [<ForkRightRoundedIcon />, <RulesSvg />],
ele: RulesPage,
element: <RulesPage />,
},
{
label: "Label-Logs",
link: "/logs",
path: "/logs",
icon: [<SubjectRoundedIcon />, <LogsSvg />],
ele: LogsPage,
element: <LogsPage />,
},
{
label: "Label-Test",
link: "/test",
path: "/test",
icon: [<WifiTetheringRoundedIcon />, <TestSvg />],
ele: TestPage,
element: <TestPage />,
},
{
label: "Label-Settings",
link: "/settings",
path: "/settings",
icon: [<SettingsRoundedIcon />, <SettingsSvg />],
ele: SettingsPage,
element: <SettingsPage />,
},
];
].map((router) => ({
...router,
element: (
<BaseErrorBoundary key={router.label}>{router.element}</BaseErrorBoundary>
),
}));

View File

@@ -1,14 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import {
Box,
Button,
IconButton,
MenuItem,
Paper,
Select,
TextField,
} from "@mui/material";
import { Box, Button, IconButton, MenuItem, Select } from "@mui/material";
import { useRecoilState } from "recoil";
import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next";
@@ -26,6 +18,7 @@ import {
} from "@/components/connection/connection-detail";
import parseTraffic from "@/utils/parse-traffic";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
@@ -45,7 +38,12 @@ const ConnectionsPage = () => {
const isTableLayout = setting.layout === "table";
const orderOpts: Record<string, OrderFunc> = {
Default: (list) => list,
Default: (list) =>
list.sort(
(a, b) =>
new Date(b.start || "0").getTime()! -
new Date(a.start || "0").getTime()!
),
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
"Download Speed": (list) =>
list.sort((a, b) => b.curDownload! - a.curDownload!),
@@ -185,17 +183,9 @@ const ConnectionsPage = () => {
</Select>
)}
<TextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
spellCheck="false"
variant="outlined"
placeholder={t("Filter conditions")}
<BaseStyledTextField
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
/>
</Box>

View File

@@ -5,9 +5,9 @@ import {
Button,
IconButton,
MenuItem,
Paper,
Select,
TextField,
SelectProps,
styled,
} from "@mui/material";
import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next";
@@ -19,6 +19,25 @@ import { atomEnableLog, atomLogData } from "@/services/states";
import { BaseEmpty, BasePage } from "@/components/base";
import LogItem from "@/components/log/log-item";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const StyledSelect = styled((props: SelectProps<string>) => {
return (
<Select
size="small"
autoComplete="off"
sx={{
width: 120,
height: 33.375,
mr: 1,
'[role="button"]': { py: 0.65 },
}}
{...props}
/>
);
})(({ theme }) => ({
background: theme.palette.mode === "light" ? "#fff" : undefined,
}));
const LogPage = () => {
const { t } = useTranslation();
@@ -28,8 +47,27 @@ const LogPage = () => {
const isDark = theme.palette.mode === "dark";
const [logState, setLogState] = useState("all");
const [filterText, setFilterText] = useState("");
const [useRegexSearch, setUseRegexSearch] = useState(true);
const [hasInputError, setInputError] = useState(false);
const [inputHelperText, setInputHelperText] = useState("");
const filterLogs = useMemo(() => {
setInputHelperText("");
setInputError(false);
if (useRegexSearch) {
try {
const regex = new RegExp(filterText);
return logData.filter((data) => {
return (
regex.test(data.payload) &&
(logState === "all" ? true : data.type.includes(logState))
);
});
} catch (err: any) {
setInputHelperText(err.message.substring(0, 60));
setInputError(true);
return logData;
}
}
return logData.filter((data) => {
return (
data.payload.includes(filterText) &&
@@ -77,35 +115,35 @@ const LogPage = () => {
alignItems: "center",
}}
>
<Select
size="small"
autoComplete="off"
<StyledSelect
value={logState}
onChange={(e) => setLogState(e.target.value)}
sx={{
width: 120,
height: 33.375,
mr: 1,
'[role="button"]': { py: 0.65 },
}}
>
<MenuItem value="all">ALL</MenuItem>
<MenuItem value="inf">INFO</MenuItem>
<MenuItem value="warn">WARN</MenuItem>
<MenuItem value="err">ERROR</MenuItem>
</Select>
</StyledSelect>
<TextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
spellCheck="false"
variant="outlined"
placeholder={t("Filter conditions")}
<BaseStyledTextField
error={hasInputError}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
helperText={inputHelperText}
placeholder={t("Filter conditions")}
InputProps={{
sx: { pr: 1 },
endAdornment: (
<IconButton
sx={{ fontWeight: "800", height: "100%", padding: "0" }}
color={useRegexSearch ? "primary" : "default"}
title={t("Use Regular Expression")}
onClick={() => setUseRegexSearch(!useRegexSearch)}
>
.*
</IconButton>
),
}}
/>
</Box>

View File

@@ -1,16 +1,8 @@
import useSWR, { mutate } from "swr";
import { useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { useSetRecoilState } from "recoil";
import {
Box,
Button,
Grid,
IconButton,
Stack,
TextField,
Divider,
} from "@mui/material";
import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material";
import {
DndContext,
closestCenter,
@@ -41,6 +33,7 @@ import {
deleteProfile,
updateProfile,
reorderProfile,
createProfile,
} from "@/services/cmds";
import { atomLoadingCache } from "@/services/states";
import { closeAllConnections } from "@/services/api";
@@ -56,6 +49,9 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
import { useRecoilState } from "recoil";
import { atomThemeMode } from "@/services/states";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
import { listen } from "@tauri-apps/api/event";
import { readTextFile } from "@tauri-apps/api/fs";
const ProfilePage = () => {
const { t } = useTranslation();
@@ -70,6 +66,35 @@ const ProfilePage = () => {
coordinateGetter: sortableKeyboardCoordinates,
})
);
useEffect(() => {
const unlisten = listen("tauri://file-drop", async (event) => {
const fileList = event.payload as string[];
for (let file of fileList) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error("Only support YAML files.");
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
const {
profiles = {},
activateSelected,
@@ -299,16 +324,10 @@ const ProfilePage = () => {
alignItems: "center",
}}
>
<TextField
hiddenLabel
fullWidth
size="small"
<BaseStyledTextField
value={url}
variant="outlined"
autoComplete="off"
spellCheck="false"
onChange={(e) => setUrl(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
placeholder={t("Profile URL")}
InputProps={{
sx: { pr: 1 },

View File

@@ -2,12 +2,13 @@ import useSWR from "swr";
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
import { Box, TextField } from "@mui/material";
import { Box } from "@mui/material";
import { getRules } from "@/services/api";
import { BaseEmpty, BasePage } from "@/components/base";
import RuleItem from "@/components/rule/rule-item";
import { ProviderButton } from "@/components/rule/provider-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const RulesPage = () => {
const { t } = useTranslation();
@@ -41,17 +42,9 @@ const RulesPage = () => {
alignItems: "center",
}}
>
<TextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
<BaseStyledTextField
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
/>
</Box>

View File

@@ -75,9 +75,13 @@ export const getRules = async () => {
};
/// Get Proxy delay
export const getProxyDelay = async (name: string, url?: string) => {
export const getProxyDelay = async (
name: string,
url?: string,
timeout?: number
) => {
const params = {
timeout: 10000,
timeout: timeout || 10000,
url: url || "http://1.1.1.1",
};
const instance = await getAxios();
@@ -237,3 +241,21 @@ export const closeAllConnections = async () => {
const instance = await getAxios();
await instance.delete<any, any>(`/connections`);
};
// Get Group Proxy Delays
export const getGroupProxyDelays = async (
groupName: string,
url?: string,
timeout?: number
) => {
const params = {
timeout: timeout || 10000,
url: url || "http://1.1.1.1",
};
const instance = await getAxios();
const result = await instance.get(
`/group/${encodeURIComponent(groupName)}/delay`,
{ params }
);
return result as any as Record<string, number>;
};

View File

@@ -223,7 +223,7 @@ export async function exitApp() {
export async function copyIconFile(
path: string,
name: "common.png" | "sysproxy.png" | "tun.png"
name: "common" | "sysproxy" | "tun"
) {
return invoke<void>("copy_icon_file", { path, name });
}

View File

@@ -64,6 +64,7 @@ interface IProxyItem {
hidden?: boolean;
icon?: string;
provider?: string; // 记录是否来自provider
fixed?: string; // 记录固定(优先)的节点
}
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
@@ -168,6 +169,7 @@ interface IProfileItem {
expire: number;
};
option?: IProfileOption;
home?: string;
}
interface IProfileOption {

View File

@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
import path from "path";
import svgr from "vite-plugin-svgr";
import react from "@vitejs/plugin-react";
import monaco from "vite-plugin-monaco-editor";
import monacoEditor from "vite-plugin-monaco-editor";
// https://vitejs.dev/config/
export default defineConfig({
@@ -11,7 +11,15 @@ export default defineConfig({
plugins: [
svgr(),
react(),
monaco({ languageWorkers: ["editorWorkerService", "typescript"] }),
monacoEditor({
languageWorkers: ["editorWorkerService", "typescript"],
customWorkers: [
{
label: "yaml",
entry: "monaco-yaml/yaml.worker",
},
],
}),
],
build: {
outDir: "../dist",