mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
Compare commits
63
Commits
v1.5.10
...
dependencies
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3271c96531 | ||
|
|
05fa6b9aba | ||
|
|
7edd7f27cb | ||
|
|
4a6b12eda1 | ||
|
|
a354e1a0eb | ||
|
|
c2c419522a | ||
|
|
7a3cc7d242 | ||
|
|
bd6f02f6af | ||
|
|
bd33728fd7 | ||
|
|
8400ee8a3d | ||
|
|
80ee7aef8e | ||
|
|
41762be3a5 | ||
|
|
605dda7b76 | ||
|
|
8d6e3b5a58 | ||
|
|
53920fc368 | ||
|
|
96d10423d9 | ||
|
|
42be9b5b51 | ||
|
|
0add041516 | ||
|
|
dee76cac8d | ||
|
|
9646fab22c | ||
|
|
a54e9cb244 | ||
|
|
f8339c7a9a | ||
|
|
9e2c864117 | ||
|
|
e1ccd71455 | ||
|
|
0e82426ea1 | ||
|
|
6c7afd168a | ||
|
|
e014fdf3da | ||
|
|
2074da05c8 | ||
|
|
84cd87b70a | ||
|
|
75f87044f6 | ||
|
|
f71b51e64a | ||
|
|
9d741cdd63 | ||
|
|
56be17000a | ||
|
|
f6aacbc31d | ||
|
|
ad4b57c327 | ||
|
|
3794b2f0de | ||
|
|
e0e8412728 | ||
|
|
05b8175aad | ||
|
|
339c89dd1c | ||
|
|
402299e9c8 | ||
|
|
d0e8f450fd | ||
|
|
e97b1ccd7a | ||
|
|
68691b91ef | ||
|
|
213d417481 | ||
|
|
2ec841eb61 | ||
|
|
60b5c54be1 | ||
|
|
448412a191 | ||
|
|
e06327936e | ||
|
|
5f044e1aed | ||
|
|
d90fb6fc9b | ||
|
|
1c583d2ea9 | ||
|
|
ddab131102 | ||
|
|
3b06cf8a2a | ||
|
|
c5d7c29f3d | ||
|
|
26af860eac | ||
|
|
ec9852eb98 | ||
|
|
f2b8c6d0ca | ||
|
|
2cf7a048cf | ||
|
|
3dbb71a076 | ||
|
|
ddf6760543 | ||
|
|
a6fa323f33 | ||
|
|
2df8f1acdb | ||
|
|
74a3290abc |
@@ -8,13 +8,13 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
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.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/install/)
|
||||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
||||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||||
## Before submitting the issue, please make sure of the following checklist:
|
## 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.html) and [FAQ](https://clash-verge-rev.github.io/faq/install/)
|
||||||
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
|
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
|
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. 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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 9
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
|
|
||||||
### FAQ
|
### 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/install/)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
|
|
||||||
### FAQ
|
### 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/install/)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 9
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
@@ -6,4 +7,4 @@ dist-ssr
|
|||||||
update.json
|
update.json
|
||||||
scripts/_env.sh
|
scripts/_env.sh
|
||||||
.vscode
|
.vscode
|
||||||
.tool-version
|
.tool-versions
|
||||||
@@ -49,7 +49,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
|
|
||||||
### FAQ
|
### 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/install/)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+68
-1
@@ -1,4 +1,71 @@
|
|||||||
## 1.5.10
|
## 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
|
||||||
|
|
||||||
|
- Meta(mihomo)内核更新 1.18.2
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 升级图标无法点击的问题
|
||||||
|
- 卸载时检查安装目录是否为空
|
||||||
|
- 代理界面图标重合的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.10
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
|||||||
+29
-26
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "1.5.10",
|
"version": "1.6.1",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
@@ -19,45 +19,48 @@
|
|||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.5",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^5.15.5",
|
"@mui/icons-material": "^5.15.15",
|
||||||
"@mui/lab": "5.0.0-alpha.149",
|
"@mui/lab": "5.0.0-alpha.149",
|
||||||
"@mui/material": "^5.15.5",
|
"@mui/material": "^5.15.15",
|
||||||
"@mui/x-data-grid": "^6.18.7",
|
"@mui/x-data-grid": "^6.19.11",
|
||||||
"@tauri-apps/api": "^1.5.3",
|
"@tauri-apps/api": "^1.5.4",
|
||||||
"ahooks": "^3.7.8",
|
"@types/json-schema": "^7.0.15",
|
||||||
"axios": "^1.6.5",
|
"ahooks": "^3.7.11",
|
||||||
|
"axios": "^1.6.8",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
"i18next": "^23.7.16",
|
"i18next": "^23.11.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.34.1",
|
"meta-json-schema": "1.18.4-beta2",
|
||||||
"nanoid": "^5.0.4",
|
"monaco-editor": "^0.47.0",
|
||||||
"react": "^18.2.0",
|
"monaco-yaml": "^5.1.1",
|
||||||
"react-dom": "^18.2.0",
|
"nanoid": "^5.0.7",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-i18next": "^13.5.0",
|
"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-transition-group": "^4.4.5",
|
||||||
"react-virtuoso": "^4.6.2",
|
"react-virtuoso": "^4.7.10",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"snarkdown": "^2.0.0",
|
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"tar": "^6.2.0"
|
"tar": "^6.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@tauri-apps/cli": "^1.5.9",
|
"@tauri-apps/cli": "^1.5.12",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.12",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"https-proxy-agent": "^5.0.1",
|
"https-proxy-agent": "^5.0.1",
|
||||||
@@ -65,9 +68,9 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"pretty-quick": "^3.3.1",
|
"pretty-quick": "^3.3.1",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.75.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.2.10",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+3340
-1890
File diff suppressed because it is too large
Load Diff
+47
-13
@@ -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
|
* main
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
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({
|
resolveResource({
|
||||||
file: "clash-verge-service.exe",
|
file: "clash-verge-service" + ext,
|
||||||
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
|
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||||
});
|
});
|
||||||
const resolveInstall = () =>
|
};
|
||||||
|
|
||||||
|
const resolveInstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: "install-service.exe",
|
file: "install-service" + ext,
|
||||||
downloadURL: `${SERVICE_URL}/install-service.exe`,
|
downloadURL: `${SERVICE_URL}/install-service${ext}`,
|
||||||
});
|
});
|
||||||
const resolveUninstall = () =>
|
};
|
||||||
|
|
||||||
|
const resolveUninstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: "uninstall-service.exe",
|
file: "uninstall-service" + ext,
|
||||||
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
|
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resolveSetDnsScript = () =>
|
const resolveSetDnsScript = () =>
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: "set_dns.sh",
|
file: "set_dns.sh",
|
||||||
@@ -420,9 +446,9 @@ const tasks = [
|
|||||||
retry: 5,
|
retry: 5,
|
||||||
},
|
},
|
||||||
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
|
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
|
||||||
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
{ name: "service", func: resolveService, retry: 5 },
|
||||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
{ name: "install", func: resolveInstall, retry: 5 },
|
||||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
{ name: "uninstall", func: resolveUninstall, retry: 5 },
|
||||||
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
|
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
|
||||||
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
|
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
|
||||||
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
||||||
@@ -434,12 +460,20 @@ const tasks = [
|
|||||||
retry: 5,
|
retry: 5,
|
||||||
winOnly: true,
|
winOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "service_chmod",
|
||||||
|
func: resolveServicePermission,
|
||||||
|
retry: 1,
|
||||||
|
unixOnly: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runTask() {
|
async function runTask() {
|
||||||
const task = tasks.shift();
|
const task = tasks.shift();
|
||||||
if (!task) return;
|
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++) {
|
for (let i = 0; i < task.retry; i++) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Generated
+819
-696
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "1.5.10"
|
version = "1.6.1"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -37,7 +37,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
||||||
auto-launch = { git="https://github.com/zzzgydi/auto-launch", 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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
@@ -45,6 +45,7 @@ deelevate = "0.2.0"
|
|||||||
winreg = "0.52.0"
|
winreg = "0.52.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
users = "0.11.0"
|
||||||
#openssl
|
#openssl
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
+15
-25
@@ -65,6 +65,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
|||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
let _ = handle::Handle::update_systray_part();
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -299,9 +300,17 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
|
|||||||
if !icon_dir.exists() {
|
if !icon_dir.exists() {
|
||||||
let _ = std::fs::create_dir_all(&icon_dir);
|
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() {
|
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) {
|
match std::fs::copy(file_path, &dest_path) {
|
||||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
@@ -331,42 +340,23 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
|
|||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub mod service {
|
pub mod service {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::core::win_service;
|
use crate::core::service;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
|
pub async fn check_service() -> CmdResult<service::JsonResponse> {
|
||||||
wrap_err!(win_service::check_service().await)
|
wrap_err!(service::check_service().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_service() -> CmdResult {
|
pub async fn install_service() -> CmdResult {
|
||||||
wrap_err!(win_service::install_service().await)
|
wrap_err!(service::install_service().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn uninstall_service() -> CmdResult {
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
wrap_err!(win_service::uninstall_service().await)
|
wrap_err!(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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl IClashTemp {
|
|||||||
tun.insert("strict-route".into(), false.into());
|
tun.insert("strict-route".into(), false.into());
|
||||||
tun.insert("auto-detect-interface".into(), true.into());
|
tun.insert("auto-detect-interface".into(), true.into());
|
||||||
tun.insert("dns-hijack".into(), vec!["any:53"].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"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
map.insert("redir-port".into(), 7895.into());
|
map.insert("redir-port".into(), 7895.into());
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ pub struct PrfItem {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub option: Option<PrfOption>,
|
pub option: Option<PrfOption>,
|
||||||
|
|
||||||
|
/// profile web page url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub home: Option<String>,
|
||||||
|
|
||||||
/// the file data
|
/// the file data
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub file_data: Option<String>,
|
pub file_data: Option<String>,
|
||||||
@@ -161,6 +165,7 @@ impl PrfItem {
|
|||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
home: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
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 uid = help::get_uid("r");
|
||||||
let file = format!("{uid}.yaml");
|
let file = format!("{uid}.yaml");
|
||||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
@@ -317,6 +330,7 @@ impl PrfItem {
|
|||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option,
|
option,
|
||||||
|
home,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(data.into()),
|
file_data: Some(data.into()),
|
||||||
})
|
})
|
||||||
@@ -338,6 +352,7 @@ impl PrfItem {
|
|||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
home: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(tmpl::ITEM_MERGE.into()),
|
file_data: Some(tmpl::ITEM_MERGE.into()),
|
||||||
})
|
})
|
||||||
@@ -356,6 +371,7 @@ impl PrfItem {
|
|||||||
desc: Some(desc),
|
desc: Some(desc),
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
|
home: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ impl IProfiles {
|
|||||||
if each.uid == some_uid {
|
if each.uid == some_uid {
|
||||||
each.extra = item.extra;
|
each.extra = item.extra;
|
||||||
each.updated = item.updated;
|
each.updated = item.updated;
|
||||||
|
each.home = item.home;
|
||||||
// save the file data
|
// save the file data
|
||||||
// move the field value after save
|
// move the field value after save
|
||||||
if let Some(file_data) = item.file_data.take() {
|
if let Some(file_data) = item.file_data.take() {
|
||||||
|
|||||||
+21
-28
@@ -1,3 +1,4 @@
|
|||||||
|
use super::service;
|
||||||
use super::{clash_api, logger::Logger};
|
use super::{clash_api, logger::Logger};
|
||||||
use crate::log_err;
|
use crate::log_err;
|
||||||
use crate::{config::*, utils::dirs};
|
use crate::{config::*, utils::dirs};
|
||||||
@@ -93,10 +94,9 @@ impl CoreManager {
|
|||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
if *self.use_service_mode.lock() {
|
||||||
log::debug!(target: "app", "stop the core by service");
|
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;
|
should_kill = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,32 +105,27 @@ impl CoreManager {
|
|||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
// 服务模式
|
||||||
{
|
let enable = { Config::verge().latest().enable_service_mode };
|
||||||
use super::win_service;
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
// 服务模式
|
*self.use_service_mode.lock() = enable;
|
||||||
let enable = { Config::verge().latest().enable_service_mode };
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
|
|
||||||
*self.use_service_mode.lock() = enable;
|
if enable {
|
||||||
|
// 服务模式启动失败就直接运行sidecar
|
||||||
|
log::debug!(target: "app", "try to run core in service mode");
|
||||||
|
|
||||||
if enable {
|
match (|| async {
|
||||||
// 服务模式启动失败就直接运行sidecar
|
service::check_service().await?;
|
||||||
log::debug!(target: "app", "try to run core in service mode");
|
service::run_core_by_service(&config_path).await
|
||||||
|
})()
|
||||||
match (|| async {
|
.await
|
||||||
win_service::check_service().await?;
|
{
|
||||||
win_service::run_core_by_service(&config_path).await
|
Ok(_) => return Ok(()),
|
||||||
})()
|
Err(err) => {
|
||||||
.await
|
// 修改这个值,免得stop出错
|
||||||
{
|
*self.use_service_mode.lock() = false;
|
||||||
Ok(_) => return Ok(()),
|
log::error!(target: "app", "{err}");
|
||||||
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<()> {
|
pub fn recover_core(&'static self) -> Result<()> {
|
||||||
// 服务模式不管
|
// 服务模式不管
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
if *self.use_service_mode.lock() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -238,11 +232,10 @@ impl CoreManager {
|
|||||||
|
|
||||||
/// 停止核心运行
|
/// 停止核心运行
|
||||||
pub fn stop_core(&self) -> Result<()> {
|
pub fn stop_core(&self) -> Result<()> {
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
if *self.use_service_mode.lock() {
|
||||||
log::debug!(target: "app", "stop the core by service");
|
log::debug!(target: "app", "stop the core by service");
|
||||||
tauri::async_runtime::block_on(async move {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let output = {
|
let output = {
|
||||||
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
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() {
|
let sudo = match Command::new("which").arg("pkexec").output() {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub mod manager;
|
|||||||
pub mod sysopt;
|
pub mod sysopt;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
pub mod win_service;
|
pub mod service;
|
||||||
pub mod win_uwp;
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::core::*;
|
pub use self::core::*;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
#![cfg(target_os = "windows")]
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use deelevate::{PrivilegeLevel, Token};
|
|
||||||
use runas::Command as RunasCommand;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{env::current_exe, process::Command as StdCommand};
|
use std::{env::current_exe, process::Command as StdCommand};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
// Windows only
|
||||||
|
|
||||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
@@ -32,7 +29,13 @@ pub struct JsonResponse {
|
|||||||
|
|
||||||
/// Install the Clash Verge Service
|
/// Install the Clash Verge Service
|
||||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||||
|
///
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn install_service() -> Result<()> {
|
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 binary_path = dirs::service_path()?;
|
||||||
let install_path = binary_path.with_file_name("install-service.exe");
|
let install_path = binary_path.with_file_name("install-service.exe");
|
||||||
|
|
||||||
@@ -60,9 +63,69 @@ pub async fn install_service() -> Result<()> {
|
|||||||
Ok(())
|
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
|
/// Uninstall the Clash Verge Service
|
||||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn uninstall_service() -> Result<()> {
|
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 binary_path = dirs::service_path()?;
|
||||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||||
|
|
||||||
@@ -90,6 +153,63 @@ pub async fn uninstall_service() -> Result<()> {
|
|||||||
Ok(())
|
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
|
/// check the windows service status
|
||||||
pub async fn check_service() -> Result<JsonResponse> {
|
pub async fn check_service() -> Result<JsonResponse> {
|
||||||
let url = format!("{SERVICE_URL}/get_clash");
|
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 = { Config::verge().latest().clash_core.clone() };
|
||||||
let clash_core = clash_core.unwrap_or("clash".into());
|
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 = current_exe()?.with_file_name(clash_bin);
|
||||||
let bin_path = dirs::path_to_str(&bin_path)?;
|
let bin_path = dirs::path_to_str(&bin_path)?;
|
||||||
|
|
||||||
@@ -3,9 +3,10 @@ use anyhow::{anyhow, Result};
|
|||||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use std::env::current_exe;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use sysproxy::Sysproxy;
|
use sysproxy::Sysproxy;
|
||||||
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
|
use tauri::async_runtime::Mutex as TokioMutex;
|
||||||
|
|
||||||
pub struct Sysopt {
|
pub struct Sysopt {
|
||||||
/// current system proxy setting
|
/// current system proxy setting
|
||||||
@@ -149,7 +150,7 @@ impl Sysopt {
|
|||||||
/// init the auto launch
|
/// init the auto launch
|
||||||
pub fn init_launch(&self) -> Result<()> {
|
pub fn init_launch(&self) -> Result<()> {
|
||||||
let app_exe = current_exe()?;
|
let app_exe = current_exe()?;
|
||||||
let app_exe = dunce::canonicalize(app_exe)?;
|
// let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
let app_name = app_exe
|
let app_name = app_exe
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.and_then(|f| f.to_str())
|
.and_then(|f| f.to_str())
|
||||||
|
|||||||
+40
-13
@@ -182,9 +182,13 @@ impl Tray {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
|
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
|
||||||
if *sysproxy_tray_icon {
|
if *sysproxy_tray_icon {
|
||||||
let path = dirs::app_home_dir()?.join("icons").join("sysproxy.png");
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
if path.exists() {
|
let png_path = icon_dir_path.join("sysproxy.png");
|
||||||
icon = std::fs::read(path).unwrap();
|
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
|
icon
|
||||||
@@ -194,9 +198,13 @@ impl Tray {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
|
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
|
||||||
if *common_tray_icon {
|
if *common_tray_icon {
|
||||||
let path = dirs::app_home_dir()?.join("icons").join("common.png");
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
if path.exists() {
|
let png_path = icon_dir_path.join("common.png");
|
||||||
icon = std::fs::read(path).unwrap();
|
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
|
icon
|
||||||
@@ -208,9 +216,13 @@ impl Tray {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
|
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
|
||||||
if *tun_tray_icon {
|
if *tun_tray_icon {
|
||||||
let path = dirs::app_home_dir()?.join("icons").join("tun.png");
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
if path.exists() {
|
let png_path = icon_dir_path.join("tun.png");
|
||||||
icon = std::fs::read(path).unwrap();
|
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
|
indication_icon = icon
|
||||||
@@ -249,18 +261,30 @@ impl Tray {
|
|||||||
map
|
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(¤t_profile_uid);
|
||||||
|
current_profile_name = match ¤t_profile.unwrap().name {
|
||||||
|
Some(profile_name) => profile_name.to_string(),
|
||||||
|
None => current_profile_name,
|
||||||
|
};
|
||||||
|
};
|
||||||
let _ = tray.set_tooltip(&format!(
|
let _ = tray.set_tooltip(&format!(
|
||||||
"Clash Verge {version}\n{}: {}\n{}: {}",
|
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||||
t!("System Proxy", "系统代理"),
|
t!("System Proxy", "系统代理"),
|
||||||
switch_map[system_proxy],
|
switch_map[system_proxy],
|
||||||
t!("TUN Mode", "Tun 模式"),
|
t!("TUN Mode", "Tun 模式"),
|
||||||
switch_map[tun_mode]
|
switch_map[tun_mode],
|
||||||
|
t!("Curent Profile", "当前订阅"),
|
||||||
|
current_profile_name
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(())
|
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 = { Config::verge().latest().tray_event.clone() };
|
||||||
let tray_event = tray_event.unwrap_or("main_window".into());
|
let tray_event = tray_event.unwrap_or("main_window".into());
|
||||||
match tray_event.as_str() {
|
match tray_event.as_str() {
|
||||||
@@ -273,7 +297,10 @@ impl Tray {
|
|||||||
|
|
||||||
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||||
match event {
|
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() {
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||||
let mode = &mode[0..mode.len() - 5];
|
let mode = &mode[0..mode.len() - 5];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
function main(params) {
|
function main(config) {
|
||||||
if (params.mode === "script") {
|
if (config.mode === "script") {
|
||||||
params.mode = "rule";
|
config.mode = "rule";
|
||||||
}
|
}
|
||||||
return params;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
function main(params) {
|
function main(config) {
|
||||||
if (Array.isArray(params.proxies)) {
|
if (Array.isArray(config.proxies)) {
|
||||||
params.proxies.forEach((p, i) => {
|
config.proxies.forEach((p, i) => {
|
||||||
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
||||||
params.proxies[i].alpn = [p.alpn];
|
config.proxies[i].alpn = [p.alpn];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return params;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,19 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
let resource_dir = dirs::app_resources_dir().unwrap();
|
let resource_dir = dirs::app_resources_dir().unwrap();
|
||||||
let script = resource_dir.join("set_dns.sh");
|
let script = resource_dir.join("set_dns.sh");
|
||||||
let script = script.to_string_lossy();
|
let script = script.to_string_lossy();
|
||||||
match Command::new("bash").args([script]).output() {
|
match Command::new("bash")
|
||||||
Ok(_) => log::info!(target: "app", "set system dns successfully"),
|
.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) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "set system dns failed: {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 resource_dir = dirs::app_resources_dir().unwrap();
|
||||||
let script = resource_dir.join("unset_dns.sh");
|
let script = resource_dir.join("unset_dns.sh");
|
||||||
let script = script.to_string_lossy();
|
let script = script.to_string_lossy();
|
||||||
match Command::new("bash").args([script]).output() {
|
match Command::new("bash")
|
||||||
Ok(_) => log::info!(target: "app", "unset system dns successfully"),
|
.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) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "unset system dns failed: {err}");
|
log::error!(target: "app", "unset system dns failed: {err}");
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-15
@@ -185,22 +185,14 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|||||||
let tun_tray_icon = patch.tun_tray_icon;
|
let tun_tray_icon = patch.tun_tray_icon;
|
||||||
|
|
||||||
match {
|
match {
|
||||||
#[cfg(target_os = "windows")]
|
let service_mode = patch.enable_service_mode;
|
||||||
{
|
|
||||||
let service_mode = patch.enable_service_mode;
|
|
||||||
|
|
||||||
if service_mode.is_some() {
|
if service_mode.is_some() {
|
||||||
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
|
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
|
||||||
|
|
||||||
Config::generate()?;
|
Config::generate()?;
|
||||||
CoreManager::global().run_core().await?;
|
CoreManager::global().run_core().await?;
|
||||||
} else if tun_mode.is_some() {
|
} else if tun_mode.is_some() {
|
||||||
update_core_config().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
if tun_mode.is_some() {
|
|
||||||
update_core_config().await?;
|
update_core_config().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +299,7 @@ pub fn copy_clash_env(app_handle: &AppHandle) {
|
|||||||
|
|
||||||
let sh =
|
let sh =
|
||||||
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||||
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 ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||||
|
|
||||||
let mut cliboard = app_handle.clipboard_manager();
|
let mut cliboard = app_handle.clipboard_manager();
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ fn main() -> std::io::Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
|
||||||
crate::log_err!(init::init_config());
|
crate::log_err!(init::init_config());
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
|
|||||||
@@ -92,12 +92,16 @@ pub fn clash_pid_path() -> Result<PathBuf> {
|
|||||||
Ok(app_home_dir()?.join("clash.pid"))
|
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)]
|
#[cfg(windows)]
|
||||||
pub fn service_path() -> Result<PathBuf> {
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn service_log_file() -> Result<PathBuf> {
|
pub fn service_log_file() -> Result<PathBuf> {
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
|
|||||||
@@ -135,13 +135,12 @@ pub fn delete_log() -> Result<()> {
|
|||||||
for file in fs::read_dir(&log_dir)?.flatten() {
|
for file in fs::read_dir(&log_dir)?.flatten() {
|
||||||
let _ = process_file(file);
|
let _ = process_file(file);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
let service_log_dir = log_dir.join("service");
|
||||||
let service_log_dir = log_dir.join("service");
|
for file in fs::read_dir(&service_log_dir)?.flatten() {
|
||||||
for file in fs::read_dir(&service_log_dir)?.flatten() {
|
let _ = process_file(file);
|
||||||
let _ = process_file(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +304,7 @@ pub fn startup_script() -> Result<()> {
|
|||||||
shell = "powershell";
|
shell = "powershell";
|
||||||
}
|
}
|
||||||
if path.ends_with(".bat") {
|
if path.ends_with(".bat") {
|
||||||
shell = "cmd";
|
shell = "powershell";
|
||||||
}
|
}
|
||||||
if shell.is_empty() {
|
if shell.is_empty() {
|
||||||
return Err(anyhow::anyhow!("unsupported script: {path}"));
|
return Err(anyhow::anyhow!("unsupported script: {path}"));
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pub mod init;
|
|||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
|
pub mod unix_helper;
|
||||||
|
|||||||
+18
-19
@@ -1,44 +1,43 @@
|
|||||||
//! Some config file template
|
//! Some config file template
|
||||||
|
|
||||||
/// template for new a profile item
|
/// 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
|
/// enhanced profile
|
||||||
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
|
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
|
||||||
# The `Merge` format used to enhance profile
|
|
||||||
|
|
||||||
prepend-rules:
|
prepend-rules: []
|
||||||
|
|
||||||
prepend-rule-providers:
|
prepend-rule-providers: {}
|
||||||
|
|
||||||
prepend-proxies:
|
prepend-proxies: []
|
||||||
|
|
||||||
prepend-proxy-providers:
|
prepend-proxy-providers: {}
|
||||||
|
|
||||||
prepend-proxy-groups:
|
prepend-proxy-groups: []
|
||||||
|
|
||||||
append-rules:
|
append-rules: []
|
||||||
|
|
||||||
append-rule-providers:
|
append-rule-providers: {}
|
||||||
|
|
||||||
append-proxies:
|
append-proxies: []
|
||||||
|
|
||||||
append-proxy-providers:
|
append-proxy-providers: {}
|
||||||
|
|
||||||
append-proxy-groups:
|
append-proxy-groups: []
|
||||||
";
|
";
|
||||||
|
|
||||||
/// enhanced profile
|
/// enhanced profile
|
||||||
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
|
||||||
|
|
||||||
function main(params) {
|
function main(config) {
|
||||||
return params;
|
return config;
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "1.5.10"
|
"version": "1.6.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
@@ -65,6 +66,11 @@
|
|||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"all": true
|
"all": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"exists": true,
|
||||||
|
"readFile": true,
|
||||||
|
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"windows": [],
|
"windows": [],
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/tray-icon.png"
|
"iconPath": "icons/tray-icon.png"
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"targets": ["deb", "appimage", "updater"],
|
"targets": ["deb", "appimage", "updater"],
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["openssl"],
|
"depends": ["openssl"],
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/mac-tray-icon.png",
|
"iconPath": "icons/mac-tray-icon.png",
|
||||||
"iconAsTemplate": true
|
"iconAsTemplate": true
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"targets": ["app", "dmg", "updater"],
|
"targets": ["app", "dmg", "updater"],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"frameworks": [],
|
"frameworks": [],
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/tray-icon.png"
|
"iconPath": "icons/tray-icon.png"
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"targets": ["nsis", "updater"],
|
"targets": ["nsis", "updater"],
|
||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
|
|||||||
@@ -743,6 +743,30 @@ Function un.onInit
|
|||||||
|
|
||||||
!insertmacro MUI_UNGETLANGUAGE
|
!insertmacro MUI_UNGETLANGUAGE
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.isDirectoryEmpty
|
||||||
|
Exch $0
|
||||||
|
Push $1
|
||||||
|
Push $2
|
||||||
|
StrCpy $2 0
|
||||||
|
FindFirst $1 $2 "$0\*.*"
|
||||||
|
loop:
|
||||||
|
StrCmp $2 "" done
|
||||||
|
StrCmp $2 "." next
|
||||||
|
StrCmp $2 ".." next
|
||||||
|
StrCpy $0 0
|
||||||
|
goto done
|
||||||
|
next:
|
||||||
|
FindNext $1 $2
|
||||||
|
goto loop
|
||||||
|
done:
|
||||||
|
FindClose $1
|
||||||
|
StrCmp $2 "" 0 +2
|
||||||
|
StrCpy $0 1
|
||||||
|
Pop $2
|
||||||
|
Pop $1
|
||||||
|
Exch $0
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
Section Uninstall
|
Section Uninstall
|
||||||
!insertmacro CheckIfAppIsRunning
|
!insertmacro CheckIfAppIsRunning
|
||||||
@@ -756,7 +780,7 @@ Section Uninstall
|
|||||||
{{#each resources}}
|
{{#each resources}}
|
||||||
Delete "$INSTDIR\\{{this.[1]}}"
|
Delete "$INSTDIR\\{{this.[1]}}"
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
Delete "$INSTDIR\resources"
|
||||||
; Delete external binaries
|
; Delete external binaries
|
||||||
{{#each binaries}}
|
{{#each binaries}}
|
||||||
Delete "$INSTDIR\\{{this}}"
|
Delete "$INSTDIR\\{{this}}"
|
||||||
@@ -765,13 +789,14 @@ Section Uninstall
|
|||||||
; Delete uninstaller
|
; Delete uninstaller
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
${If} $DeleteAppDataCheckboxState == 1
|
; Remove InstallDir
|
||||||
|
Push "$INSTDIR"
|
||||||
|
Call un.isDirectoryEmpty
|
||||||
|
Pop $0
|
||||||
|
${If} $0 == 1
|
||||||
RMDir /R /REBOOTOK "$INSTDIR"
|
RMDir /R /REBOOTOK "$INSTDIR"
|
||||||
${Else}
|
${Else}
|
||||||
{{#each resources_ancestors}}
|
MessageBox MB_OK "Install Directory is not Empty, Please remove it manually."
|
||||||
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
|
|
||||||
{{/each}}
|
|
||||||
RMDir "$INSTDIR"
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
; Remove start menu shortcut
|
; Remove start menu shortcut
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
// $maxLogo: 100px;
|
// $maxLogo: 100px;
|
||||||
|
|
||||||
.the-logo {
|
.the-logo {
|
||||||
app-region: drag;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 0 58px;
|
flex: 1 0 58px;
|
||||||
// width: 100%;
|
// width: 100%;
|
||||||
@@ -62,7 +61,7 @@
|
|||||||
.the-newbtn {
|
.the-newbtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 0px;
|
top: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
@@ -81,7 +80,7 @@
|
|||||||
|
|
||||||
> div {
|
> div {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0px 10px;
|
padding: 0px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +102,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
.the-dragbar {
|
.the-dragbar {
|
||||||
|
margin-top: 5px;
|
||||||
app-region: drag;
|
app-region: drag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +121,17 @@
|
|||||||
.windows,
|
.windows,
|
||||||
.unknown {
|
.unknown {
|
||||||
&.layout {
|
&.layout {
|
||||||
.layout__left {
|
//.layout__left {
|
||||||
// padding-top: 24px;
|
// padding-top: 24px;
|
||||||
}
|
//}
|
||||||
|
|
||||||
.layout__left .the-logo {
|
.layout__left .the-logo {
|
||||||
flex: 1 0 58px;
|
flex: 1 0 58px;
|
||||||
padding-top: 40px;
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 20px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -3,8 +3,8 @@ import { forwardRef, useImperativeHandle, useState } from "react";
|
|||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, Button, Snackbar } from "@mui/material";
|
import { Box, Button, Snackbar } from "@mui/material";
|
||||||
import { deleteConnection } from "@/services/api";
|
import { deleteConnection } from "@/services/api";
|
||||||
import { truncateStr } from "@/utils/truncate-str";
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
export interface ConnectionDetailRef {
|
export interface ConnectionDetailRef {
|
||||||
open: (detail: IConnectionsItem) => void;
|
open: (detail: IConnectionsItem) => void;
|
||||||
@@ -69,7 +69,9 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
{ label: "Rule", value: rule },
|
{ label: "Rule", value: rule },
|
||||||
{
|
{
|
||||||
label: "Process",
|
label: "Process",
|
||||||
value: truncateStr(metadata.process || metadata.processPath),
|
value: `${metadata.process}${
|
||||||
|
metadata.processPath ? `(${metadata.processPath})` : ""
|
||||||
|
}`,
|
||||||
},
|
},
|
||||||
{ label: "Time", value: dayjs(data.start).fromNow() },
|
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||||
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||||
@@ -96,7 +98,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close
|
{t("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,16 +8,29 @@ import {
|
|||||||
PushPinOutlined,
|
PushPinOutlined,
|
||||||
PushPinRounded,
|
PushPinRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const LayoutControl = () => {
|
export const LayoutControl = () => {
|
||||||
const minWidth = 40;
|
const minWidth = 40;
|
||||||
|
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
const [isPined, setIsPined] = 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 (
|
return (
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
|
|||||||
@@ -29,21 +29,23 @@ export const LayoutTraffic = () => {
|
|||||||
// setup log ws during layout
|
// setup log ws during layout
|
||||||
useLogSetup();
|
useLogSetup();
|
||||||
|
|
||||||
const { connect, disconnect } = useWebsocket((event) => {
|
const trafficWs = useWebsocket(
|
||||||
const data = JSON.parse(event.data) as ITrafficItem;
|
(event) => {
|
||||||
trafficRef.current?.appendData(data);
|
const data = JSON.parse(event.data) as ITrafficItem;
|
||||||
setTraffic(data);
|
trafficRef.current?.appendData(data);
|
||||||
});
|
setTraffic(data);
|
||||||
|
},
|
||||||
|
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!clashInfo || !pageVisible) return;
|
if (!clashInfo || !pageVisible) return;
|
||||||
|
|
||||||
const { server = "", secret = "" } = clashInfo;
|
const { server = "", secret = "" } = clashInfo;
|
||||||
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
|
trafficWs.connect(
|
||||||
|
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
|
||||||
return () => {
|
);
|
||||||
disconnect();
|
return () => trafficWs.disconnect();
|
||||||
};
|
|
||||||
}, [clashInfo, pageVisible]);
|
}, [clashInfo, pageVisible]);
|
||||||
|
|
||||||
/* --------- meta memory information --------- */
|
/* --------- meta memory information --------- */
|
||||||
@@ -54,7 +56,7 @@ export const LayoutTraffic = () => {
|
|||||||
(event) => {
|
(event) => {
|
||||||
setMemory(JSON.parse(event.data));
|
setMemory(JSON.parse(event.data));
|
||||||
},
|
},
|
||||||
{ onError: () => setMemory({ inuse: 0 }) }
|
{ onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const ConfirmViewer = (props: Props) => {
|
|||||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle>{t(title)}</DialogTitle>
|
<DialogTitle>{t(title)}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent sx={{ width: "95%", pb: 1, userSelect: "text" }}>
|
<DialogContent sx={{ pb: 1, userSelect: "text" }}>
|
||||||
{t(message)}
|
{t(message)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,45 @@ import {
|
|||||||
import { atomThemeMode } from "@/services/states";
|
import { atomThemeMode } from "@/services/states";
|
||||||
import { readProfileFile, saveProfileFile } from "@/services/cmds";
|
import { readProfileFile, saveProfileFile } from "@/services/cmds";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
|
import * as monaco from "monaco-editor";
|
||||||
import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js";
|
|
||||||
import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js";
|
|
||||||
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
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 {
|
interface Props {
|
||||||
uid: string;
|
uid: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
mode: "yaml" | "javascript";
|
language: "yaml" | "javascript";
|
||||||
|
schema?: "clash" | "merge";
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorViewer = (props: Props) => {
|
// yaml worker
|
||||||
const { uid, open, mode, onClose, onChange } = props;
|
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 { t } = useTranslation();
|
||||||
const editorRef = useRef<any>();
|
const editorRef = useRef<any>();
|
||||||
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
@@ -41,13 +63,23 @@ export const EditorViewer = (props: Props) => {
|
|||||||
const dom = editorRef.current;
|
const dom = editorRef.current;
|
||||||
|
|
||||||
if (!dom) return;
|
if (!dom) return;
|
||||||
|
|
||||||
if (instanceRef.current) instanceRef.current.dispose();
|
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, {
|
instanceRef.current = editor.create(editorRef.current, {
|
||||||
value: data,
|
model: model,
|
||||||
language: mode,
|
language: language,
|
||||||
|
tabSize: ["yaml", "javascript"].includes(language) ? 2 : 4, // 根据语言类型设置缩进
|
||||||
theme: themeMode === "light" ? "vs" : "vs-dark",
|
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>
|
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||||
<DialogTitle>{t("Edit File")}</DialogTitle>
|
<DialogTitle>{t("Edit File")}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent sx={{ width: "95%", pb: 1, userSelect: "text" }}>
|
<DialogContent
|
||||||
<div style={{ width: "100%", height: "500px" }} ref={editorRef} />
|
sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
|
||||||
|
>
|
||||||
|
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { EditorViewer } from "./editor-viewer";
|
|||||||
import { ProfileBox } from "./profile-box";
|
import { ProfileBox } from "./profile-box";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { ConfirmViewer } from "./confirm-viewer";
|
import { ConfirmViewer } from "./confirm-viewer";
|
||||||
|
import { open } from "@tauri-apps/api/shell";
|
||||||
const round = keyframes`
|
const round = keyframes`
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
@@ -55,6 +55,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
// remote file mode
|
// remote file mode
|
||||||
const hasUrl = !!itemData.url;
|
const hasUrl = !!itemData.url;
|
||||||
const hasExtra = !!extra; // only subscription url has extra info
|
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 { upload = 0, download = 0, total = 0 } = extra ?? {};
|
||||||
const from = parseUrl(itemData.url);
|
const from = parseUrl(itemData.url);
|
||||||
@@ -95,6 +96,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
const onOpenHome = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
open(itemData.home ?? "");
|
||||||
|
};
|
||||||
|
|
||||||
const onEditInfo = () => {
|
const onEditInfo = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
onEdit();
|
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: "Select", handler: onForceSelect },
|
||||||
{ label: "Edit Info", handler: onEditInfo },
|
{ label: "Edit Info", handler: onEditInfo },
|
||||||
{ label: "Edit File", handler: onEditFile },
|
{ label: "Edit File", handler: onEditFile },
|
||||||
@@ -180,7 +188,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
const fileModeMenu = [
|
const fileModeMenu = [
|
||||||
{ label: "Select", handler: onForceSelect },
|
{ label: "Select", handler: onForceSelect },
|
||||||
{ label: "Edit Info", handler: onEditInfo },
|
{ label: "Edit Info", handler: onEditInfo },
|
||||||
@@ -378,7 +386,8 @@ export const ProfileItem = (props: Props) => {
|
|||||||
<EditorViewer
|
<EditorViewer
|
||||||
uid={uid}
|
uid={uid}
|
||||||
open={fileOpen}
|
open={fileOpen}
|
||||||
mode="yaml"
|
language="yaml"
|
||||||
|
schema="clash"
|
||||||
onClose={() => setFileOpen(false)}
|
onClose={() => setFileOpen(false)}
|
||||||
/>
|
/>
|
||||||
<ConfirmViewer
|
<ConfirmViewer
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ export const ProfileMore = (props: Props) => {
|
|||||||
<EditorViewer
|
<EditorViewer
|
||||||
uid={uid}
|
uid={uid}
|
||||||
open={fileOpen}
|
open={fileOpen}
|
||||||
mode={type === "merge" ? "yaml" : "javascript"}
|
language={type === "merge" ? "yaml" : "javascript"}
|
||||||
|
schema={type === "merge" ? "merge" : undefined}
|
||||||
onClose={() => setFileOpen(false)}
|
onClose={() => setFileOpen(false)}
|
||||||
/>
|
/>
|
||||||
<ConfirmViewer
|
<ConfirmViewer
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
}
|
}
|
||||||
if (form.option?.update_interval) {
|
if (form.option?.update_interval) {
|
||||||
form.option.update_interval = +form.option.update_interval;
|
form.option.update_interval = +form.option.update_interval;
|
||||||
|
} else {
|
||||||
|
delete form.option?.update_interval;
|
||||||
}
|
}
|
||||||
if (form.option?.user_agent === "") {
|
if (form.option?.user_agent === "") {
|
||||||
delete form.option.user_agent;
|
delete form.option.user_agent;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
providerHealthCheck,
|
providerHealthCheck,
|
||||||
updateProxy,
|
updateProxy,
|
||||||
deleteConnection,
|
deleteConnection,
|
||||||
|
getGroupProxyDelays,
|
||||||
} from "@/services/api";
|
} from "@/services/api";
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { useProfiles } from "@/hooks/use-profiles";
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { BaseEmpty } from "../base";
|
import { BaseEmpty } from "../base";
|
||||||
@@ -33,7 +33,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
// 切换分组的节点代理
|
// 切换分组的节点代理
|
||||||
const handleChangeProxy = useLockFn(
|
const handleChangeProxy = useLockFn(
|
||||||
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
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;
|
const { name, now } = group;
|
||||||
await updateProxy(name, proxy.name);
|
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);
|
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();
|
onProxies();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const ProxyHead = (props: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("Proxy detail")}
|
title={showType ? t("Proxy basic") : t("Proxy detail")}
|
||||||
onClick={() => onHeadState({ showType: !showType })}
|
onClick={() => onHeadState({ showType: !showType })}
|
||||||
>
|
>
|
||||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import delayManager from "@/services/delay";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
group: IProxyGroupItem;
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
showType?: boolean;
|
showType?: boolean;
|
||||||
@@ -16,7 +16,7 @@ interface Props {
|
|||||||
|
|
||||||
// 多列布局
|
// 多列布局
|
||||||
export const ProxyItemMini = (props: Props) => {
|
export const ProxyItemMini = (props: Props) => {
|
||||||
const { groupName, proxy, selected, showType = true, onClick } = props;
|
const { group, proxy, selected, showType = true, onClick } = props;
|
||||||
|
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
@@ -25,21 +25,21 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, groupName);
|
delayManager.removeListener(proxy.name, group.name);
|
||||||
};
|
};
|
||||||
}, [proxy.name, groupName]);
|
}, [proxy.name, group.name]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, groupName));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [proxy]);
|
}, [proxy]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,6 +65,13 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
||||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
||||||
"&:hover .the-icon": { display: "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": {
|
"&.Mui-selected": {
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-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
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
component="div"
|
component="div"
|
||||||
@@ -147,14 +157,12 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ ml: 0.5, color: "primary.main" }}>
|
<Box sx={{ ml: 0.5, color: "primary.main" }}>
|
||||||
{delay === -2 && (
|
{delay === -2 && (
|
||||||
<Widget>
|
<Widget>
|
||||||
<BaseLoading />
|
<BaseLoading />
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!proxy.provider && delay !== -2 && (
|
{!proxy.provider && delay !== -2 && (
|
||||||
// provider的节点不支持检测
|
// provider的节点不支持检测
|
||||||
<Widget
|
<Widget
|
||||||
@@ -193,7 +201,6 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
{delayManager.formatDelay(delay, timeout)}
|
{delayManager.formatDelay(delay, timeout)}
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{delay !== -2 && delay <= 0 && selected && (
|
{delay !== -2 && delay <= 0 && selected && (
|
||||||
// 展示已选择的icon
|
// 展示已选择的icon
|
||||||
<CheckCircleOutlineRounded
|
<CheckCircleOutlineRounded
|
||||||
@@ -202,6 +209,13 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{group.fixed && group.fixed === proxy.name && (
|
||||||
|
// 展示fixed状态
|
||||||
|
<span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
|
||||||
|
📌
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import delayManager from "@/services/delay";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
group: IProxyGroupItem;
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
showType?: boolean;
|
showType?: boolean;
|
||||||
@@ -44,7 +44,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProxyItem = (props: Props) => {
|
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 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
@@ -52,21 +52,21 @@ export const ProxyItem = (props: Props) => {
|
|||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, groupName);
|
delayManager.removeListener(proxy.name, group.name);
|
||||||
};
|
};
|
||||||
}, [proxy.name, groupName]);
|
}, [proxy.name, group.name]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, groupName));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [proxy]);
|
}, [proxy]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
|
|
||||||
async function initIconCachePath() {
|
async function initIconCachePath() {
|
||||||
if (group.icon && group.icon.trim().startsWith("http")) {
|
if (group.icon && group.icon.trim().startsWith("http")) {
|
||||||
const fileName = getFileName(group.icon);
|
const fileName =
|
||||||
|
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
|
||||||
const iconPath = await downloadIconCache(group.icon, fileName);
|
const iconPath = await downloadIconCache(group.icon, fileName);
|
||||||
setIconCachePath(convertFileSrc(iconPath));
|
setIconCachePath(convertFileSrc(iconPath));
|
||||||
}
|
}
|
||||||
@@ -141,7 +142,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
if (type === 2 && !group.hidden) {
|
if (type === 2 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
groupName={group.name}
|
group={group}
|
||||||
proxy={proxy!}
|
proxy={proxy!}
|
||||||
selected={group.now === proxy?.name}
|
selected={group.now === proxy?.name}
|
||||||
showType={headState?.showType}
|
showType={headState?.showType}
|
||||||
@@ -185,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
{proxyCol?.map((proxy) => (
|
{proxyCol?.map((proxy) => (
|
||||||
<ProxyItemMini
|
<ProxyItemMini
|
||||||
key={item.key + proxy.name}
|
key={item.key + proxy.name}
|
||||||
groupName={group.name}
|
group={group}
|
||||||
proxy={proxy!}
|
proxy={proxy!}
|
||||||
selected={group.now === proxy.name}
|
selected={group.now === proxy.name}
|
||||||
showType={headState?.showType}
|
showType={headState?.showType}
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRecoilValue } from "recoil";
|
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 { atomThemeMode } from "@/services/states";
|
||||||
import { getRuntimeYaml } from "@/services/cmds";
|
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 { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
|
||||||
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
|
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, {
|
instanceRef.current = editor.create(editorRef.current, {
|
||||||
value: data ?? "# Error\n",
|
value: data ?? "# Error\n",
|
||||||
language: "yaml",
|
language: "yaml",
|
||||||
|
tabSize: 2,
|
||||||
theme: themeMode === "light" ? "vs" : "vs-dark",
|
theme: themeMode === "light" ? "vs" : "vs-dark",
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
|
||||||
readOnly: true,
|
mouseWheelZoom: true, // Ctrl+滚轮调节缩放
|
||||||
|
readOnly: true, // 只读
|
||||||
|
readOnlyMessage: { value: t("ReadOnlyMessage") },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -57,20 +67,22 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="xl" fullWidth>
|
||||||
open={open}
|
<DialogTitle>
|
||||||
title={
|
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
|
||||||
<>
|
</DialogTitle>
|
||||||
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
|
|
||||||
</>
|
<DialogContent
|
||||||
}
|
sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
|
||||||
contentSx={{ width: 520, pb: 1, userSelect: "text" }}
|
>
|
||||||
cancelBtn={t("Back")}
|
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
|
||||||
disableOk
|
</DialogContent>
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onCancel={() => setOpen(false)}
|
<DialogActions>
|
||||||
>
|
<Button onClick={() => setOpen(false)} variant="outlined">
|
||||||
<div style={{ width: "100%", height: "420px" }} ref={editorRef} />
|
{t("Back")}
|
||||||
</BaseDialog>
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary="External Controller" />
|
<ListItemText primary={t("External Controller")} />
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -58,13 +58,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<ListItemText primary="Core Secret" />
|
<ListItemText primary={t("Core Secret")} />
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
sx={{ width: 175 }}
|
sx={{ width: 175 }}
|
||||||
value={secret}
|
value={secret}
|
||||||
placeholder="Recommended"
|
placeholder={t("Recommended")}
|
||||||
onChange={(e) => setSecret(e.target.value)}
|
onChange={(e) => setSecret(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { open as openDialog } from "@tauri-apps/api/dialog";
|
|||||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||||
import { copyIconFile, getAppDir } from "@/services/cmds";
|
import { copyIconFile, getAppDir } from "@/services/cmds";
|
||||||
import { join } from "@tauri-apps/api/path";
|
import { join } from "@tauri-apps/api/path";
|
||||||
|
import { exists } from "@tauri-apps/api/fs";
|
||||||
|
|
||||||
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -26,12 +27,27 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
async function initIconPath() {
|
async function initIconPath() {
|
||||||
const appDir = await getAppDir();
|
const appDir = await getAppDir();
|
||||||
const icon_dir = await join(appDir, "icons");
|
const icon_dir = await join(appDir, "icons");
|
||||||
const common_icon = await join(icon_dir, "common.png");
|
const common_icon_png = await join(icon_dir, "common.png");
|
||||||
const sysproxy_icon = await join(icon_dir, "sysproxy.png");
|
const common_icon_ico = await join(icon_dir, "common.ico");
|
||||||
const tun_icon = await join(icon_dir, "tun.png");
|
const sysproxy_icon_png = await join(icon_dir, "sysproxy.png");
|
||||||
setCommonIcon(common_icon);
|
const sysproxy_icon_ico = await join(icon_dir, "sysproxy.ico");
|
||||||
setSysproxyIcon(sysproxy_icon);
|
const tun_icon_png = await join(icon_dir, "tun.png");
|
||||||
setTunIcon(tun_icon);
|
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, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -140,12 +156,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Tray Icon Image",
|
name: "Tray Icon Image",
|
||||||
extensions: ["png"],
|
extensions: ["png", "ico"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (path?.length) {
|
if (path?.length) {
|
||||||
await copyIconFile(`${path}`, "common.png");
|
await copyIconFile(`${path}`, "common");
|
||||||
|
await initIconPath();
|
||||||
onChangeData({ common_tray_icon: true });
|
onChangeData({ common_tray_icon: true });
|
||||||
patchVerge({ common_tray_icon: true });
|
patchVerge({ common_tray_icon: true });
|
||||||
}
|
}
|
||||||
@@ -184,12 +201,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Tray Icon Image",
|
name: "Tray Icon Image",
|
||||||
extensions: ["png"],
|
extensions: ["png", "ico"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (path?.length) {
|
if (path?.length) {
|
||||||
await copyIconFile(`${path}`, "sysproxy.png");
|
await copyIconFile(`${path}`, "sysproxy");
|
||||||
|
await initIconPath();
|
||||||
onChangeData({ sysproxy_tray_icon: true });
|
onChangeData({ sysproxy_tray_icon: true });
|
||||||
patchVerge({ sysproxy_tray_icon: true });
|
patchVerge({ sysproxy_tray_icon: true });
|
||||||
}
|
}
|
||||||
@@ -226,12 +244,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Tray Icon Image",
|
name: "Tray Icon Image",
|
||||||
extensions: ["png"],
|
extensions: ["png", "ico"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (path?.length) {
|
if (path?.length) {
|
||||||
await copyIconFile(`${path}`, "tun.png");
|
await copyIconFile(`${path}`, "tun");
|
||||||
|
await initIconPath();
|
||||||
onChangeData({ tun_tray_icon: true });
|
onChangeData({ tun_tray_icon: true });
|
||||||
patchVerge({ tun_tray_icon: true });
|
patchVerge({ tun_tray_icon: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,12 +86,15 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
|
|||||||
disableFooter
|
disableFooter
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<Typography>Current State: {state}</Typography>
|
<Typography>
|
||||||
|
{t("Current State")}: {t(state)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{(state === "unknown" || state === "uninstall") && (
|
{(state === "unknown" || state === "uninstall") && (
|
||||||
<Typography>
|
<Typography>
|
||||||
Information: Please make sure that the Clash Verge Service is
|
{t(
|
||||||
installed and enabled
|
"Information: Please make sure that the Clash Verge Service is installed and enabled"
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -102,19 +105,19 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
|
|||||||
>
|
>
|
||||||
{state === "uninstall" && enable && (
|
{state === "uninstall" && enable && (
|
||||||
<Button variant="contained" onClick={onDisable}>
|
<Button variant="contained" onClick={onDisable}>
|
||||||
Disable Service Mode
|
{t("Disable Service Mode")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state === "uninstall" && (
|
{state === "uninstall" && (
|
||||||
<Button variant="contained" onClick={onInstall}>
|
<Button variant="contained" onClick={onInstall}>
|
||||||
Install
|
{t("Install")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(state === "active" || state === "installed") && (
|
{(state === "active" || state === "installed") && (
|
||||||
<Button variant="outlined" onClick={onUninstall}>
|
<Button variant="outlined" onClick={onUninstall}>
|
||||||
Uninstall
|
{t("Uninstall")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
rows={3}
|
rows={3}
|
||||||
sx={{ width: 280 }}
|
sx={{ width: 280 }}
|
||||||
value={value.bypass}
|
value={value.bypass}
|
||||||
|
placeholder={sysproxy?.bypass || `-`}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setValue((v) => ({ ...v, bypass: e.target.value }))
|
setValue((v) => ({ ...v, bypass: e.target.value }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
|
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
|
||||||
import { StackModeSwitch } from "./stack-mode-switch";
|
import { StackModeSwitch } from "./stack-mode-switch";
|
||||||
|
import { enhanceProfiles } from "@/services/cmds";
|
||||||
|
|
||||||
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -27,7 +28,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
autoDetectInterface: true,
|
autoDetectInterface: true,
|
||||||
dnsHijack: ["any:53"],
|
dnsHijack: ["any:53"],
|
||||||
strictRoute: false,
|
strictRoute: false,
|
||||||
mtu: 9000,
|
mtu: 1500,
|
||||||
});
|
});
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -40,7 +41,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
|
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
|
||||||
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
|
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
|
||||||
strictRoute: clash?.tun["strict-route"] ?? false,
|
strictRoute: clash?.tun["strict-route"] ?? false,
|
||||||
mtu: clash?.tun.mtu ?? 9000,
|
mtu: clash?.tun.mtu ?? 1500,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
@@ -50,12 +51,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
try {
|
try {
|
||||||
let tun = {
|
let tun = {
|
||||||
stack: values.stack,
|
stack: values.stack,
|
||||||
device: values.device,
|
device: values.device === "" ? "Meta" : values.device,
|
||||||
"auto-route": values.autoRoute,
|
"auto-route": values.autoRoute,
|
||||||
"auto-detect-interface": values.autoDetectInterface,
|
"auto-detect-interface": values.autoDetectInterface,
|
||||||
"dns-hijack": values.dnsHijack,
|
"dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack,
|
||||||
"strict-route": values.strictRoute,
|
"strict-route": values.strictRoute,
|
||||||
mtu: values.mtu,
|
mtu: values.mtu ?? 1500,
|
||||||
};
|
};
|
||||||
await patchClash({ tun });
|
await patchClash({ tun });
|
||||||
await mutateClash(
|
await mutateClash(
|
||||||
@@ -65,6 +66,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}),
|
}),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
await enhanceProfiles();
|
||||||
|
Notice.success("Refresh clash config", 1000);
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(err.message || err.toString(), 3000);
|
||||||
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
Notice.error(err.message || err.toString());
|
||||||
@@ -88,7 +95,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
"auto-detect-interface": true,
|
"auto-detect-interface": true,
|
||||||
"dns-hijack": ["any:53"],
|
"dns-hijack": ["any:53"],
|
||||||
"strict-route": false,
|
"strict-route": false,
|
||||||
mtu: 9000,
|
mtu: 1500,
|
||||||
};
|
};
|
||||||
setValues({
|
setValues({
|
||||||
stack: "gvisor",
|
stack: "gvisor",
|
||||||
@@ -97,7 +104,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
autoDetectInterface: true,
|
autoDetectInterface: true,
|
||||||
dnsHijack: ["any:53"],
|
dnsHijack: ["any:53"],
|
||||||
strictRoute: false,
|
strictRoute: false,
|
||||||
mtu: 9000,
|
mtu: 1500,
|
||||||
});
|
});
|
||||||
await patchClash({ tun });
|
await patchClash({ tun });
|
||||||
await mutateClash(
|
await mutateClash(
|
||||||
@@ -208,7 +215,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
sx={{ width: 250 }}
|
sx={{ width: 250 }}
|
||||||
value={values.mtu}
|
value={values.mtu}
|
||||||
placeholder="9000"
|
placeholder="1500"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setValues((v) => ({
|
setValues((v) => ({
|
||||||
...v,
|
...v,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import snarkdown from "snarkdown";
|
|
||||||
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
|
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, LinearProgress, styled } from "@mui/material";
|
import { Box, LinearProgress } from "@mui/material";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { relaunch } from "@tauri-apps/api/process";
|
import { relaunch } from "@tauri-apps/api/process";
|
||||||
@@ -11,10 +10,8 @@ import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
|||||||
import { atomUpdateState } from "@/services/states";
|
import { atomUpdateState } from "@/services/states";
|
||||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { portableFlag } from "@/pages/_layout";
|
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;
|
let eventListener: UnlistenFn | null = null;
|
||||||
|
|
||||||
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
@@ -38,12 +35,11 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// markdown parser
|
const markdownContent = useMemo(() => {
|
||||||
const parseContent = useMemo(() => {
|
|
||||||
if (!updateInfo?.manifest?.body) {
|
if (!updateInfo?.manifest?.body) {
|
||||||
return "New Version is available";
|
return "New Version is available";
|
||||||
}
|
}
|
||||||
return snarkdown(updateInfo?.manifest?.body);
|
return updateInfo?.manifest?.body;
|
||||||
}, [updateInfo]);
|
}, [updateInfo]);
|
||||||
|
|
||||||
const onUpdate = useLockFn(async () => {
|
const onUpdate = useLockFn(async () => {
|
||||||
@@ -87,10 +83,22 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={onUpdate}
|
onOk={onUpdate}
|
||||||
>
|
>
|
||||||
<UpdateLog
|
<Box sx={{ height: "calc(100% - 10px)", overflow: "auto" }}>
|
||||||
dangerouslySetInnerHTML={{ __html: parseContent }}
|
<ReactMarkdown
|
||||||
sx={{ height: "calc(100% - 10px)", overflow: "auto" }}
|
components={{
|
||||||
/>
|
a: ({ node, ...props }) => {
|
||||||
|
const { children } = props;
|
||||||
|
return (
|
||||||
|
<a {...props} target="_blank">
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{markdownContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
{updateState && (
|
{updateState && (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="buffer"
|
variant="buffer"
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export const WebUIItem = (props: Props) => {
|
|||||||
title={value}
|
title={value}
|
||||||
color={value ? "text.primary" : "text.secondary"}
|
color={value ? "text.primary" : "text.secondary"}
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
"> span": {
|
"> span": {
|
||||||
color: palette.primary.main,
|
color: palette.primary.main,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,23 +17,17 @@ interface Props {
|
|||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWIN = getSystem() === "windows";
|
|
||||||
|
|
||||||
const SettingSystem = ({ onError }: Props) => {
|
const SettingSystem = ({ onError }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
|
|
||||||
// service mode
|
// service mode
|
||||||
const { data: serviceStatus } = useSWR(
|
const { data: serviceStatus } = useSWR("checkService", checkService, {
|
||||||
isWIN ? "checkService" : null,
|
revalidateIfStale: false,
|
||||||
checkService,
|
shouldRetryOnError: false,
|
||||||
{
|
focusThrottleInterval: 36e5, // 1 hour
|
||||||
revalidateIfStale: false,
|
});
|
||||||
shouldRetryOnError: false,
|
|
||||||
focusThrottleInterval: 36e5, // 1 hour
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const serviceRef = useRef<DialogRef>(null);
|
const serviceRef = useRef<DialogRef>(null);
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
@@ -56,20 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
<SettingList title={t("System Setting")}>
|
<SettingList title={t("System Setting")}>
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
<SysproxyViewer ref={sysproxyRef} />
|
||||||
<TunViewer ref={tunRef} />
|
<TunViewer ref={tunRef} />
|
||||||
{isWIN && (
|
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
|
||||||
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t("Tun Mode")}
|
label={t("Tun Mode")}
|
||||||
extra={
|
extra={
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Tooltip title={t("Tun Mode Info")} placement="top">
|
||||||
title={
|
|
||||||
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<IconButton color="inherit" size="small">
|
<IconButton color="inherit" size="small">
|
||||||
<InfoRounded
|
<InfoRounded
|
||||||
fontSize="inherit"
|
fontSize="inherit"
|
||||||
@@ -102,39 +89,37 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{isWIN && (
|
<SettingItem
|
||||||
<SettingItem
|
label={t("Service Mode")}
|
||||||
label={t("Service Mode")}
|
extra={
|
||||||
extra={
|
<IconButton
|
||||||
<IconButton
|
color="inherit"
|
||||||
color="inherit"
|
size="small"
|
||||||
size="small"
|
onClick={() => serviceRef.current?.open()}
|
||||||
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 })}
|
|
||||||
>
|
>
|
||||||
<Switch
|
<PrivacyTipRounded
|
||||||
edge="end"
|
fontSize="inherit"
|
||||||
disabled={
|
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||||
serviceStatus !== "active" && serviceStatus !== "installed"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</GuardState>
|
</IconButton>
|
||||||
</SettingItem>
|
}
|
||||||
)}
|
>
|
||||||
|
<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
|
<SettingItem
|
||||||
label={t("System Proxy")}
|
label={t("System Proxy")}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { LayoutViewer } from "./mods/layout-viewer";
|
|||||||
import { UpdateViewer } from "./mods/update-viewer";
|
import { UpdateViewer } from "./mods/update-viewer";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
import { routers } from "@/pages/_routers";
|
import { routers } from "@/pages/_routers";
|
||||||
import { appWindow } from "@tauri-apps/api/window";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,7 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
<MenuItem value="zh">中文</MenuItem>
|
<MenuItem value="zh">中文</MenuItem>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">English</MenuItem>
|
||||||
<MenuItem value="ru">Русский</MenuItem>
|
<MenuItem value="ru">Русский</MenuItem>
|
||||||
|
<MenuItem value="fa">فارسی</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
@@ -156,8 +157,8 @@ const SettingVerge = ({ onError }: Props) => {
|
|||||||
onGuard={(e) => patchVerge({ start_page: e })}
|
onGuard={(e) => patchVerge({ start_page: e })}
|
||||||
>
|
>
|
||||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||||
{routers.map((page: { label: string; link: string }) => {
|
{routers.map((page: { label: string; path: string }) => {
|
||||||
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
|
return <MenuItem value={page.path}>{t(page.label)}</MenuItem>;
|
||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</GuardState>
|
</GuardState>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Notice } from "@/components/base";
|
|||||||
import { TestBox } from "./test-box";
|
import { TestBox } from "./test-box";
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
|
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
|
||||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -48,7 +48,7 @@ export const TestItem = (props: Props) => {
|
|||||||
|
|
||||||
async function initIconCachePath() {
|
async function initIconCachePath() {
|
||||||
if (icon && icon.trim().startsWith("http")) {
|
if (icon && icon.trim().startsWith("http")) {
|
||||||
const fileName = getFileName(icon);
|
const fileName = uid + "-" + getFileName(icon);
|
||||||
const iconPath = await downloadIconCache(icon, fileName);
|
const iconPath = await downloadIconCache(icon, fileName);
|
||||||
setIconCachePath(convertFileSrc(iconPath));
|
setIconCachePath(convertFileSrc(iconPath));
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,6 @@ export const TestItem = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TestBox
|
<TestBox
|
||||||
onClick={onEditTest}
|
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
setPosition({ top: clientY, left: clientX });
|
setPosition({ top: clientY, left: clientX });
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export type WsMsgFn = (event: MessageEvent<any>) => void;
|
|||||||
export interface WsOptions {
|
export interface WsOptions {
|
||||||
errorCount?: number; // default is 5
|
errorCount?: number; // default is 5
|
||||||
retryInterval?: number; // default is 2500
|
retryInterval?: number; // default is 2500
|
||||||
onError?: () => void;
|
onError?: (event: Event) => void;
|
||||||
|
onClose?: (event: CloseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
||||||
@@ -33,17 +34,23 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
|||||||
const ws = new WebSocket(url);
|
const ws = new WebSocket(url);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.addEventListener("message", onMessage);
|
ws.addEventListener("message", (event) => {
|
||||||
ws.addEventListener("error", () => {
|
errorCount = 0; // reset counter
|
||||||
|
onMessage(event);
|
||||||
|
});
|
||||||
|
ws.addEventListener("error", (event) => {
|
||||||
errorCount -= 1;
|
errorCount -= 1;
|
||||||
|
|
||||||
if (errorCount >= 0) {
|
if (errorCount >= 0) {
|
||||||
timerRef.current = setTimeout(connectHelper, 2500);
|
timerRef.current = setTimeout(connectHelper, 2500);
|
||||||
} else {
|
} else {
|
||||||
disconnect();
|
disconnect();
|
||||||
options?.onError?.();
|
options?.onError?.(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ws.addEventListener("close", (event) => {
|
||||||
|
options?.onClose?.(event);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
connectHelper();
|
connectHelper();
|
||||||
|
|||||||
+35
-6
@@ -18,6 +18,8 @@
|
|||||||
"direct": "direct",
|
"direct": "direct",
|
||||||
"script": "script",
|
"script": "script",
|
||||||
|
|
||||||
|
"Create Test": "Create Test",
|
||||||
|
"Edit Test": "Edit Test",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Icon": "Icon",
|
"Icon": "Icon",
|
||||||
"Test URL": "Test URL",
|
"Test URL": "Test URL",
|
||||||
@@ -29,7 +31,9 @@
|
|||||||
"New": "New",
|
"New": "New",
|
||||||
"Create Profile": "Create Profile",
|
"Create Profile": "Create Profile",
|
||||||
"Choose File": "Choose File",
|
"Choose File": "Choose File",
|
||||||
|
"Close": "Close",
|
||||||
"Close All": "Close All",
|
"Close All": "Close All",
|
||||||
|
"Home": "Home",
|
||||||
"Select": "Select",
|
"Select": "Select",
|
||||||
"Edit Info": "Edit Info",
|
"Edit Info": "Edit Info",
|
||||||
"Edit File": "Edit File",
|
"Edit File": "Edit File",
|
||||||
@@ -45,6 +49,8 @@
|
|||||||
"Update All Profiles": "Update All Profiles",
|
"Update All Profiles": "Update All Profiles",
|
||||||
"View Runtime Config": "View Runtime Config",
|
"View Runtime Config": "View Runtime Config",
|
||||||
"Reactivate Profiles": "Reactivate Profiles",
|
"Reactivate Profiles": "Reactivate Profiles",
|
||||||
|
"Confirm deletion": "Confirm deletion",
|
||||||
|
"This operation is not reversible": "This operation is not reversible",
|
||||||
|
|
||||||
"Location": "Location",
|
"Location": "Location",
|
||||||
"Delay check": "Delay check",
|
"Delay check": "Delay check",
|
||||||
@@ -52,6 +58,7 @@
|
|||||||
"Sort by delay": "Sort by delay",
|
"Sort by delay": "Sort by delay",
|
||||||
"Sort by name": "Sort by name",
|
"Sort by name": "Sort by name",
|
||||||
"Delay check URL": "Delay check URL",
|
"Delay check URL": "Delay check URL",
|
||||||
|
"Proxy basic": "Proxy basic",
|
||||||
"Proxy detail": "Proxy detail",
|
"Proxy detail": "Proxy detail",
|
||||||
"Filter": "Filter",
|
"Filter": "Filter",
|
||||||
"Filter conditions": "Filter conditions",
|
"Filter conditions": "Filter conditions",
|
||||||
@@ -80,6 +87,9 @@
|
|||||||
"Random Port": "Random Port",
|
"Random Port": "Random Port",
|
||||||
"After restart to take effect": "After restart to take effect",
|
"After restart to take effect": "After restart to take effect",
|
||||||
"External": "External",
|
"External": "External",
|
||||||
|
"External Controller": "External Controller",
|
||||||
|
"Core Secret": "Core Secret",
|
||||||
|
"Recommended": "Recommended",
|
||||||
"Clash Core": "Clash Core",
|
"Clash Core": "Clash Core",
|
||||||
"Grant": "Grant",
|
"Grant": "Grant",
|
||||||
"Tun mode requires": "Tun mode requires",
|
"Tun mode requires": "Tun mode requires",
|
||||||
@@ -94,10 +104,10 @@
|
|||||||
"Proxy Guard": "Proxy Guard",
|
"Proxy Guard": "Proxy Guard",
|
||||||
"Guard Duration": "Guard Duration",
|
"Guard Duration": "Guard Duration",
|
||||||
"Proxy Bypass": "Proxy Bypass",
|
"Proxy Bypass": "Proxy Bypass",
|
||||||
|
"Current System Proxy": "Current System Proxy",
|
||||||
"Enable status": "Enable status",
|
"Enable status": "Enable status",
|
||||||
"Server Addr": "Server Addr",
|
"Server Addr": "Server Addr",
|
||||||
"Bypass": "Bypass",
|
"Bypass": "Bypass",
|
||||||
"Current System Proxy": "Current System Proxy",
|
|
||||||
"Theme Mode": "Theme Mode",
|
"Theme Mode": "Theme Mode",
|
||||||
"Tray Click Event": "Tray Click Event",
|
"Tray Click Event": "Tray Click Event",
|
||||||
"Copy Env Type": "Copy Env Type",
|
"Copy Env Type": "Copy Env Type",
|
||||||
@@ -112,6 +122,9 @@
|
|||||||
"Traffic Graph": "Traffic Graph",
|
"Traffic Graph": "Traffic Graph",
|
||||||
"Memory Usage": "Memory Usage",
|
"Memory Usage": "Memory Usage",
|
||||||
"Proxy Group Icon": "Proxy Group Icon",
|
"Proxy Group Icon": "Proxy Group Icon",
|
||||||
|
"Menu Icon": "Menu Icon",
|
||||||
|
"Monochrome": "Monochrome",
|
||||||
|
"Colorful": "Colorful",
|
||||||
"Common Tray Icon": "Common Tray Icon",
|
"Common Tray Icon": "Common Tray Icon",
|
||||||
"System Proxy Tray Icon": "System Proxy Tray Icon",
|
"System Proxy Tray Icon": "System Proxy Tray Icon",
|
||||||
"Tun Tray Icon": "Tun Tray Icon",
|
"Tun Tray Icon": "Tun Tray Icon",
|
||||||
@@ -120,6 +133,7 @@
|
|||||||
"Open Core Dir": "Open Core Dir",
|
"Open Core Dir": "Open Core Dir",
|
||||||
"Open Logs Dir": "Open Logs Dir",
|
"Open Logs Dir": "Open Logs Dir",
|
||||||
"Check for Updates": "Check for Updates",
|
"Check for Updates": "Check for Updates",
|
||||||
|
"Open Dev Tools": "Open Dev Tools",
|
||||||
"Verge Version": "Verge Version",
|
"Verge Version": "Verge Version",
|
||||||
"theme.light": "Light",
|
"theme.light": "Light",
|
||||||
"theme.dark": "Dark",
|
"theme.dark": "Dark",
|
||||||
@@ -127,6 +141,7 @@
|
|||||||
"Clash Field": "Clash Field",
|
"Clash Field": "Clash Field",
|
||||||
"Runtime Config": "Runtime Config",
|
"Runtime Config": "Runtime Config",
|
||||||
"ReadOnly": "ReadOnly",
|
"ReadOnly": "ReadOnly",
|
||||||
|
"ReadOnlyMessage": "Cannot edit in read-only editor",
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Upgrade": "Upgrade",
|
"Upgrade": "Upgrade",
|
||||||
|
|
||||||
@@ -134,6 +149,7 @@
|
|||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Exit": "Exit",
|
"Exit": "Exit",
|
||||||
|
"Confirm": "Confirm",
|
||||||
|
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
"Download Speed": "Download Speed",
|
"Download Speed": "Download Speed",
|
||||||
@@ -148,11 +164,11 @@
|
|||||||
|
|
||||||
"App Log Level": "App Log Level",
|
"App Log Level": "App Log Level",
|
||||||
"Auto Close Connections": "Auto Close Connections",
|
"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",
|
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
||||||
"Proxy Layout Column": "Proxy Layout Column",
|
"Proxy Layout Column": "Proxy Layout Column",
|
||||||
"Default Latency Test": "Default Latency Test",
|
"Default Latency Test": "Default Latency Test",
|
||||||
"Defaule Latency Timeout": "Defaule Latency Timeout",
|
"Default Latency Timeout": "Default Latency Timeout",
|
||||||
|
|
||||||
"Auto Log Clean": "Auto Log Clean",
|
"Auto Log Clean": "Auto Log Clean",
|
||||||
"Never Clean": "Never Clean",
|
"Never Clean": "Never Clean",
|
||||||
@@ -167,9 +183,22 @@
|
|||||||
"Auto Detect Interface": "Auto Detect Interface",
|
"Auto Detect Interface": "Auto Detect Interface",
|
||||||
"DNS Hijack": "DNS Hijack",
|
"DNS Hijack": "DNS Hijack",
|
||||||
"MTU": "Max Transmission Unit",
|
"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",
|
"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": "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",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+75
-3
@@ -11,13 +11,15 @@
|
|||||||
"Logs": "Логи",
|
"Logs": "Логи",
|
||||||
"Clear": "Очистить",
|
"Clear": "Очистить",
|
||||||
"Proxies": "Прокси",
|
"Proxies": "Прокси",
|
||||||
"Test": "Тест",
|
|
||||||
"Proxy Groups": "Группы прокси",
|
"Proxy Groups": "Группы прокси",
|
||||||
|
"Test": "Тест",
|
||||||
"rule": "правила",
|
"rule": "правила",
|
||||||
"global": "глобальный",
|
"global": "глобальный",
|
||||||
"direct": "прямой",
|
"direct": "прямой",
|
||||||
"script": "скриптовый",
|
"script": "скриптовый",
|
||||||
|
|
||||||
|
"Create Test": "Создать тест",
|
||||||
|
"Edit Test": "Редактировать тест",
|
||||||
"Edit": "Редактировать",
|
"Edit": "Редактировать",
|
||||||
"Icon": "Икона",
|
"Icon": "Икона",
|
||||||
"Test URL": "Тестовый URL",
|
"Test URL": "Тестовый URL",
|
||||||
@@ -29,7 +31,9 @@
|
|||||||
"New": "Новый",
|
"New": "Новый",
|
||||||
"Create Profile": "Создать профиль",
|
"Create Profile": "Создать профиль",
|
||||||
"Choose File": "Выбрать файл",
|
"Choose File": "Выбрать файл",
|
||||||
|
"Close": "Закрыть",
|
||||||
"Close All": "Закрыть всё",
|
"Close All": "Закрыть всё",
|
||||||
|
"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": "Подтвердите удаление",
|
||||||
|
"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": "URL проверки задержки",
|
"Delay check URL": "URL проверки задержки",
|
||||||
|
"Proxy basic": "Резюме о прокси",
|
||||||
"Proxy detail": "Подробности о прокси",
|
"Proxy detail": "Подробности о прокси",
|
||||||
"Filter": "Фильтр",
|
"Filter": "Фильтр",
|
||||||
"Filter conditions": "Условия фильтрации",
|
"Filter conditions": "Условия фильтрации",
|
||||||
@@ -79,7 +86,13 @@
|
|||||||
"Port Config": "Настройка порта",
|
"Port Config": "Настройка порта",
|
||||||
"Random Port": "Случайный порт",
|
"Random Port": "Случайный порт",
|
||||||
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
||||||
|
"External": "Внешний",
|
||||||
|
"External Controller": "Адрес прослушивания внешнего контроллера",
|
||||||
|
"Core Secret": "Секрет",
|
||||||
|
"Recommended": "Рекомендуется",
|
||||||
"Clash Core": "Ядро Clash",
|
"Clash Core": "Ядро Clash",
|
||||||
|
"Grant": "Предоставить",
|
||||||
|
"Tun mode requires": "Требуется Режим туннеля",
|
||||||
"Tun Mode": "Режим туннеля",
|
"Tun Mode": "Режим туннеля",
|
||||||
"Service Mode": "Режим сервиса",
|
"Service Mode": "Режим сервиса",
|
||||||
"Auto Launch": "Автозапуск",
|
"Auto Launch": "Автозапуск",
|
||||||
@@ -92,20 +105,35 @@
|
|||||||
"Guard Duration": "Период защиты",
|
"Guard Duration": "Период защиты",
|
||||||
"Proxy Bypass": "Игнорирование прокси",
|
"Proxy Bypass": "Игнорирование прокси",
|
||||||
"Current System Proxy": "Текущий системный прокси",
|
"Current System Proxy": "Текущий системный прокси",
|
||||||
|
"Enable status": "Статус включения",
|
||||||
|
"Server Addr": "Адрес сервера",
|
||||||
|
"Bypass": "Игнорирование",
|
||||||
"Theme Mode": "Режим темы",
|
"Theme Mode": "Режим темы",
|
||||||
"Tray Click Event": "Событие щелчка в лотке",
|
"Tray Click Event": "Событие щелчка в лотке",
|
||||||
"Start Page": "Главная страница",
|
|
||||||
"Copy Env Type": "Скопировать тип Env",
|
"Copy Env Type": "Скопировать тип Env",
|
||||||
|
"Start Page": "Главная страница",
|
||||||
"Startup Script": "Скрипт запуска",
|
"Startup Script": "Скрипт запуска",
|
||||||
"Browse": "Просмотреть",
|
"Browse": "Просмотреть",
|
||||||
"Show Main Window": "Показать главное окно",
|
"Show Main Window": "Показать главное окно",
|
||||||
"Theme Setting": "Настройка темы",
|
"Theme Setting": "Настройка темы",
|
||||||
|
"Layout Setting": "Настройка раскладки",
|
||||||
|
"Miscellaneous": "Настройка различные",
|
||||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||||
"Traffic Graph": "График трафика",
|
"Traffic Graph": "График трафика",
|
||||||
|
"Memory Usage": "Использование памяти",
|
||||||
|
"Proxy Group Icon": "Иконка Группы прокси",
|
||||||
|
"Menu Icon": "Иконка меню",
|
||||||
|
"Monochrome": "Монохромный",
|
||||||
|
"Colorful": "Полноцветный",
|
||||||
|
"Common Tray Icon": "Общий значок в лотке",
|
||||||
|
"System Proxy Tray Icon": "Значок системного прокси в лотке",
|
||||||
|
"Tun Tray Icon": "Значок туннеля в лотке",
|
||||||
"Language": "Язык",
|
"Language": "Язык",
|
||||||
"Open App Dir": "Открыть папку приложения",
|
"Open App Dir": "Открыть папку приложения",
|
||||||
"Open Core Dir": "Открыть папку ядра",
|
"Open Core Dir": "Открыть папку ядра",
|
||||||
"Open Logs Dir": "Открыть папку логов",
|
"Open Logs Dir": "Открыть папку логов",
|
||||||
|
"Check for Updates": "Проверить обновления",
|
||||||
|
"Open Dev Tools": "Открыть инструменты разработчика",
|
||||||
"Verge Version": "Версия Verge",
|
"Verge Version": "Версия Verge",
|
||||||
"theme.light": "Светлая",
|
"theme.light": "Светлая",
|
||||||
"theme.dark": "Тёмная",
|
"theme.dark": "Тёмная",
|
||||||
@@ -113,6 +141,7 @@
|
|||||||
"Clash Field": "Используемые настройки Clash",
|
"Clash Field": "Используемые настройки Clash",
|
||||||
"Runtime Config": "Используемый конфиг",
|
"Runtime Config": "Используемый конфиг",
|
||||||
"ReadOnly": "Только для чтения",
|
"ReadOnly": "Только для чтения",
|
||||||
|
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
|
||||||
"Restart": "Перезапуск",
|
"Restart": "Перезапуск",
|
||||||
"Upgrade": "Обновлять",
|
"Upgrade": "Обновлять",
|
||||||
|
|
||||||
@@ -120,6 +149,11 @@
|
|||||||
"Save": "Сохранить",
|
"Save": "Сохранить",
|
||||||
"Cancel": "Отмена",
|
"Cancel": "Отмена",
|
||||||
"Exit": "Выход",
|
"Exit": "Выход",
|
||||||
|
"Confirm": "Подтвердить",
|
||||||
|
|
||||||
|
"Default": "По умолчанию",
|
||||||
|
"Download Speed": "Скорость загрузки",
|
||||||
|
"Upload Speed": "Скорость загрузки",
|
||||||
|
|
||||||
"open_or_close_dashboard": "Open/Close Dashboard",
|
"open_or_close_dashboard": "Open/Close Dashboard",
|
||||||
"clash_mode_rule": "Режим правил",
|
"clash_mode_rule": "Режим правил",
|
||||||
@@ -128,5 +162,43 @@
|
|||||||
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
||||||
"toggle_tun_mode": "Включить/Отключить режим туннеля",
|
"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": "Использование регулярных выражений"
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -18,6 +18,8 @@
|
|||||||
"direct": "直连",
|
"direct": "直连",
|
||||||
"script": "脚本",
|
"script": "脚本",
|
||||||
|
|
||||||
|
"Create 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 All": "关闭全部",
|
"Close All": "关闭全部",
|
||||||
|
"Home": "首页",
|
||||||
"Select": "使用",
|
"Select": "使用",
|
||||||
"Edit Info": "编辑信息",
|
"Edit Info": "编辑信息",
|
||||||
"Edit File": "编辑文件",
|
"Edit File": "编辑文件",
|
||||||
@@ -54,6 +58,7 @@
|
|||||||
"Sort by delay": "按延迟排序",
|
"Sort by delay": "按延迟排序",
|
||||||
"Sort by name": "按名称排序",
|
"Sort by name": "按名称排序",
|
||||||
"Delay check URL": "延迟测试链接",
|
"Delay check URL": "延迟测试链接",
|
||||||
|
"Proxy basic": "隐藏节点细节",
|
||||||
"Proxy detail": "展示节点细节",
|
"Proxy detail": "展示节点细节",
|
||||||
"Filter": "过滤节点",
|
"Filter": "过滤节点",
|
||||||
"Filter conditions": "过滤条件",
|
"Filter conditions": "过滤条件",
|
||||||
@@ -82,6 +87,9 @@
|
|||||||
"Random Port": "随机端口",
|
"Random Port": "随机端口",
|
||||||
"After restart to take effect": "重启后生效",
|
"After restart to take effect": "重启后生效",
|
||||||
"External": "外部控制",
|
"External": "外部控制",
|
||||||
|
"External Controller": "外部控制器监听地址",
|
||||||
|
"Core Secret": "API访问密钥",
|
||||||
|
"Recommended": "建议设置",
|
||||||
"Clash Core": "Clash 内核",
|
"Clash Core": "Clash 内核",
|
||||||
"Grant": "授权",
|
"Grant": "授权",
|
||||||
"Tun mode requires": "如需启用TUN模式需要授权",
|
"Tun mode requires": "如需启用TUN模式需要授权",
|
||||||
@@ -133,6 +141,7 @@
|
|||||||
"Clash Field": "Clash 字段",
|
"Clash Field": "Clash 字段",
|
||||||
"Runtime Config": "当前配置",
|
"Runtime Config": "当前配置",
|
||||||
"ReadOnly": "只读",
|
"ReadOnly": "只读",
|
||||||
|
"ReadOnlyMessage": "无法在只读模式下编辑",
|
||||||
"Restart": "重启内核",
|
"Restart": "重启内核",
|
||||||
"Upgrade": "升级内核",
|
"Upgrade": "升级内核",
|
||||||
|
|
||||||
@@ -176,8 +185,20 @@
|
|||||||
"MTU": "最大传输单元",
|
"MTU": "最大传输单元",
|
||||||
"Reset to Default": "重置为默认值",
|
"Reset to Default": "重置为默认值",
|
||||||
|
|
||||||
|
"Current State": "当前状态",
|
||||||
|
"pending": "等待中",
|
||||||
|
"installed": "已安装",
|
||||||
|
"uninstall": "未安装",
|
||||||
|
"active": "已激活",
|
||||||
|
"unknown": "未知",
|
||||||
|
"Disable Service Mode": "禁用服务模式",
|
||||||
|
"Install": "安装",
|
||||||
|
"Uninstall": "卸载",
|
||||||
|
|
||||||
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
|
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
|
||||||
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限,使用前请先开启服务模式",
|
"Tun Mode Info": "Tun模式需要授予内核相关权限,使用前请先开启服务模式",
|
||||||
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限,使用前请先在内核设置中给内核授权",
|
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用",
|
||||||
"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": "使用正则表达式"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
createRoot(container).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
|
|||||||
+9
-18
@@ -4,9 +4,8 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
|||||||
import { SWRConfig, mutate } from "swr";
|
import { SWRConfig, mutate } from "swr";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Route, Routes, useLocation } from "react-router-dom";
|
import { useLocation, useRoutes } from "react-router-dom";
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
import { List, Paper, ThemeProvider } from "@mui/material";
|
||||||
import { alpha, List, Paper, ThemeProvider } from "@mui/material";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { appWindow } from "@tauri-apps/api/window";
|
import { appWindow } from "@tauri-apps/api/window";
|
||||||
import { routers } from "./_routers";
|
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 LogoSvg_dark from "@/assets/image/logo_dark.svg?react";
|
||||||
import { atomThemeMode } from "@/services/states";
|
import { atomThemeMode } from "@/services/states";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { BaseErrorBoundary, Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import { LayoutItem } from "@/components/layout/layout-item";
|
import { LayoutItem } from "@/components/layout/layout-item";
|
||||||
import { LayoutControl } from "@/components/layout/layout-control";
|
import { LayoutControl } from "@/components/layout/layout-control";
|
||||||
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
||||||
@@ -27,6 +26,8 @@ import "dayjs/locale/ru";
|
|||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import { getPortableFlag } from "@/services/cmds";
|
import { getPortableFlag } from "@/services/cmds";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import React from "react";
|
||||||
|
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
||||||
export let portableFlag = false;
|
export let portableFlag = false;
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -43,6 +44,8 @@ const Layout = () => {
|
|||||||
const { language, start_page } = verge || {};
|
const { language, start_page } = verge || {};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const routersEles = useRoutes(routers);
|
||||||
|
if (!routersEles) return null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
@@ -142,7 +145,7 @@ const Layout = () => {
|
|||||||
{routers.map((router) => (
|
{routers.map((router) => (
|
||||||
<LayoutItem
|
<LayoutItem
|
||||||
key={router.label}
|
key={router.label}
|
||||||
to={router.link}
|
to={router.path}
|
||||||
icon={router.icon}
|
icon={router.icon}
|
||||||
>
|
>
|
||||||
{t(router.label)}
|
{t(router.label)}
|
||||||
@@ -173,19 +176,7 @@ const Layout = () => {
|
|||||||
timeout={300}
|
timeout={300}
|
||||||
classNames="page"
|
classNames="page"
|
||||||
>
|
>
|
||||||
<Routes>
|
{React.cloneElement(routersEles, { key: location.pathname })}
|
||||||
{routers.map(({ label, link, ele: Ele }) => (
|
|
||||||
<Route
|
|
||||||
key={label}
|
|
||||||
path={link}
|
|
||||||
element={
|
|
||||||
<BaseErrorBoundary key={label}>
|
|
||||||
<Ele />
|
|
||||||
</BaseErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Routes>
|
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+21
-15
@@ -5,6 +5,7 @@ import ProfilesPage from "./profiles";
|
|||||||
import SettingsPage from "./settings";
|
import SettingsPage from "./settings";
|
||||||
import ConnectionsPage from "./connections";
|
import ConnectionsPage from "./connections";
|
||||||
import RulesPage from "./rules";
|
import RulesPage from "./rules";
|
||||||
|
import { BaseErrorBoundary } from "@/components/base";
|
||||||
|
|
||||||
import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react";
|
import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react";
|
||||||
import ProfilesSvg from "@/assets/image/itemicon/profiles.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 = [
|
export const routers = [
|
||||||
{
|
{
|
||||||
label: "Label-Proxies",
|
label: "Label-Proxies",
|
||||||
link: "/",
|
path: "/",
|
||||||
icon: [<WifiRoundedIcon />, <ProxiesSvg />],
|
icon: [<WifiRoundedIcon />, <ProxiesSvg />],
|
||||||
ele: ProxiesPage,
|
element: <ProxiesPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Profiles",
|
label: "Label-Profiles",
|
||||||
link: "/profile",
|
path: "/profile",
|
||||||
icon: [<DnsRoundedIcon />, <ProfilesSvg />],
|
icon: [<DnsRoundedIcon />, <ProfilesSvg />],
|
||||||
ele: ProfilesPage,
|
element: <ProfilesPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Connections",
|
label: "Label-Connections",
|
||||||
link: "/connections",
|
path: "/connections",
|
||||||
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />],
|
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />],
|
||||||
ele: ConnectionsPage,
|
element: <ConnectionsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Rules",
|
label: "Label-Rules",
|
||||||
link: "/rules",
|
path: "/rules",
|
||||||
icon: [<ForkRightRoundedIcon />, <RulesSvg />],
|
icon: [<ForkRightRoundedIcon />, <RulesSvg />],
|
||||||
ele: RulesPage,
|
element: <RulesPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Logs",
|
label: "Label-Logs",
|
||||||
link: "/logs",
|
path: "/logs",
|
||||||
icon: [<SubjectRoundedIcon />, <LogsSvg />],
|
icon: [<SubjectRoundedIcon />, <LogsSvg />],
|
||||||
ele: LogsPage,
|
element: <LogsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Test",
|
label: "Label-Test",
|
||||||
link: "/test",
|
path: "/test",
|
||||||
icon: [<WifiTetheringRoundedIcon />, <TestSvg />],
|
icon: [<WifiTetheringRoundedIcon />, <TestSvg />],
|
||||||
ele: TestPage,
|
element: <TestPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Label-Settings",
|
label: "Label-Settings",
|
||||||
link: "/settings",
|
path: "/settings",
|
||||||
icon: [<SettingsRoundedIcon />, <SettingsSvg />],
|
icon: [<SettingsRoundedIcon />, <SettingsSvg />],
|
||||||
ele: SettingsPage,
|
element: <SettingsPage />,
|
||||||
},
|
},
|
||||||
];
|
].map((router) => ({
|
||||||
|
...router,
|
||||||
|
element: (
|
||||||
|
<BaseErrorBoundary key={router.label}>{router.element}</BaseErrorBoundary>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import {
|
import { Box, Button, IconButton, MenuItem, Select } from "@mui/material";
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -26,6 +18,7 @@ import {
|
|||||||
} from "@/components/connection/connection-detail";
|
} from "@/components/connection/connection-detail";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||||
|
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
|
||||||
|
|
||||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||||
|
|
||||||
@@ -45,7 +38,12 @@ const ConnectionsPage = () => {
|
|||||||
const isTableLayout = setting.layout === "table";
|
const isTableLayout = setting.layout === "table";
|
||||||
|
|
||||||
const orderOpts: Record<string, OrderFunc> = {
|
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!),
|
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||||
"Download Speed": (list) =>
|
"Download Speed": (list) =>
|
||||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||||
@@ -185,17 +183,9 @@ const ConnectionsPage = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<BaseStyledTextField
|
||||||
hiddenLabel
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck="false"
|
|
||||||
variant="outlined"
|
|
||||||
placeholder={t("Filter conditions")}
|
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
+60
-22
@@ -5,9 +5,9 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
IconButton,
|
IconButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
|
||||||
Select,
|
Select,
|
||||||
TextField,
|
SelectProps,
|
||||||
|
styled,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -19,6 +19,25 @@ import { atomEnableLog, atomLogData } from "@/services/states";
|
|||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import LogItem from "@/components/log/log-item";
|
import LogItem from "@/components/log/log-item";
|
||||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
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 LogPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -28,8 +47,27 @@ const LogPage = () => {
|
|||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [logState, setLogState] = useState("all");
|
const [logState, setLogState] = useState("all");
|
||||||
const [filterText, setFilterText] = useState("");
|
const [filterText, setFilterText] = useState("");
|
||||||
|
const [useRegexSearch, setUseRegexSearch] = useState(true);
|
||||||
|
const [hasInputError, setInputError] = useState(false);
|
||||||
|
const [inputHelperText, setInputHelperText] = useState("");
|
||||||
const filterLogs = useMemo(() => {
|
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 logData.filter((data) => {
|
||||||
return (
|
return (
|
||||||
data.payload.includes(filterText) &&
|
data.payload.includes(filterText) &&
|
||||||
@@ -77,35 +115,35 @@ const LogPage = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select
|
<StyledSelect
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
value={logState}
|
value={logState}
|
||||||
onChange={(e) => setLogState(e.target.value)}
|
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="all">ALL</MenuItem>
|
||||||
<MenuItem value="inf">INFO</MenuItem>
|
<MenuItem value="inf">INFO</MenuItem>
|
||||||
<MenuItem value="warn">WARN</MenuItem>
|
<MenuItem value="warn">WARN</MenuItem>
|
||||||
<MenuItem value="err">ERROR</MenuItem>
|
<MenuItem value="err">ERROR</MenuItem>
|
||||||
</Select>
|
</StyledSelect>
|
||||||
|
|
||||||
<TextField
|
<BaseStyledTextField
|
||||||
hiddenLabel
|
error={hasInputError}
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck="false"
|
|
||||||
variant="outlined"
|
|
||||||
placeholder={t("Filter conditions")}
|
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
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>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
+36
-17
@@ -1,16 +1,8 @@
|
|||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useSetRecoilState } from "recoil";
|
import { useSetRecoilState } from "recoil";
|
||||||
import {
|
import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material";
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Divider,
|
|
||||||
} from "@mui/material";
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -41,6 +33,7 @@ import {
|
|||||||
deleteProfile,
|
deleteProfile,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
reorderProfile,
|
reorderProfile,
|
||||||
|
createProfile,
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { atomLoadingCache } from "@/services/states";
|
import { atomLoadingCache } from "@/services/states";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
@@ -56,6 +49,9 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
|||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash-es";
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { atomThemeMode } from "@/services/states";
|
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 ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -70,6 +66,35 @@ const ProfilePage = () => {
|
|||||||
coordinateGetter: sortableKeyboardCoordinates,
|
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 {
|
const {
|
||||||
profiles = {},
|
profiles = {},
|
||||||
activateSelected,
|
activateSelected,
|
||||||
@@ -299,16 +324,10 @@ const ProfilePage = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<BaseStyledTextField
|
||||||
hiddenLabel
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
value={url}
|
value={url}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoComplete="off"
|
|
||||||
spellCheck="false"
|
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
|
||||||
placeholder={t("Profile URL")}
|
placeholder={t("Profile URL")}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
sx: { pr: 1 },
|
sx: { pr: 1 },
|
||||||
|
|||||||
+3
-10
@@ -2,12 +2,13 @@ import useSWR from "swr";
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Box, TextField } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { getRules } from "@/services/api";
|
import { getRules } from "@/services/api";
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import RuleItem from "@/components/rule/rule-item";
|
import RuleItem from "@/components/rule/rule-item";
|
||||||
import { ProviderButton } from "@/components/rule/provider-button";
|
import { ProviderButton } from "@/components/rule/provider-button";
|
||||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||||
|
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
|
||||||
|
|
||||||
const RulesPage = () => {
|
const RulesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -41,17 +42,9 @@ const RulesPage = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<BaseStyledTextField
|
||||||
hiddenLabel
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
autoComplete="off"
|
|
||||||
variant="outlined"
|
|
||||||
spellCheck="false"
|
|
||||||
placeholder={t("Filter conditions")}
|
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -75,9 +75,13 @@ export const getRules = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Get Proxy delay
|
/// Get Proxy delay
|
||||||
export const getProxyDelay = async (name: string, url?: string) => {
|
export const getProxyDelay = async (
|
||||||
|
name: string,
|
||||||
|
url?: string,
|
||||||
|
timeout?: number
|
||||||
|
) => {
|
||||||
const params = {
|
const params = {
|
||||||
timeout: 10000,
|
timeout: timeout || 10000,
|
||||||
url: url || "http://1.1.1.1",
|
url: url || "http://1.1.1.1",
|
||||||
};
|
};
|
||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
@@ -237,3 +241,21 @@ export const closeAllConnections = async () => {
|
|||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
await instance.delete<any, any>(`/connections`);
|
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>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export async function exitApp() {
|
|||||||
|
|
||||||
export async function copyIconFile(
|
export async function copyIconFile(
|
||||||
path: string,
|
path: string,
|
||||||
name: "common.png" | "sysproxy.png" | "tun.png"
|
name: "common" | "sysproxy" | "tun"
|
||||||
) {
|
) {
|
||||||
return invoke<void>("copy_icon_file", { path, name });
|
return invoke<void>("copy_icon_file", { path, name });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import en from "@/locales/en.json";
|
import en from "@/locales/en.json";
|
||||||
import ru from "@/locales/ru.json";
|
import ru from "@/locales/ru.json";
|
||||||
import zh from "@/locales/zh.json";
|
import zh from "@/locales/zh.json";
|
||||||
|
import fa from "@/locales/fa.json";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
ru: { translation: ru },
|
ru: { translation: ru },
|
||||||
zh: { translation: zh },
|
zh: { translation: zh },
|
||||||
|
fa: { translation: fa },
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources,
|
resources,
|
||||||
lng: "en",
|
lng: "en",
|
||||||
|
fallbackLng: "en",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+2
@@ -64,6 +64,7 @@ interface IProxyItem {
|
|||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
provider?: string; // 记录是否来自provider
|
provider?: string; // 记录是否来自provider
|
||||||
|
fixed?: string; // 记录固定(优先)的节点
|
||||||
}
|
}
|
||||||
|
|
||||||
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
||||||
@@ -168,6 +169,7 @@ interface IProfileItem {
|
|||||||
expire: number;
|
expire: number;
|
||||||
};
|
};
|
||||||
option?: IProfileOption;
|
option?: IProfileOption;
|
||||||
|
home?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProfileOption {
|
interface IProfileOption {
|
||||||
|
|||||||
+10
-2
@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import monaco from "vite-plugin-monaco-editor";
|
import monacoEditor from "vite-plugin-monaco-editor";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -11,7 +11,15 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
svgr(),
|
svgr(),
|
||||||
react(),
|
react(),
|
||||||
monaco({ languageWorkers: ["editorWorkerService", "typescript"] }),
|
monacoEditor({
|
||||||
|
languageWorkers: ["editorWorkerService", "typescript"],
|
||||||
|
customWorkers: [
|
||||||
|
{
|
||||||
|
label: "yaml",
|
||||||
|
entry: "monaco-yaml/yaml.worker",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
outDir: "../dist",
|
outDir: "../dist",
|
||||||
|
|||||||
Reference in New Issue
Block a user