Compare commits
40 Commits
1
.github/build-for-linux/build.sh
vendored
1
.github/build-for-linux/build.sh
vendored
@@ -1,4 +1,3 @@
|
||||
# pnpm install --resolution-only
|
||||
pnpm install
|
||||
pnpm check $INPUT_TARGET
|
||||
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
|
||||
|
||||
4
.github/build-for-linux/entrypoint.sh
vendored
4
.github/build-for-linux/entrypoint.sh
vendored
@@ -17,8 +17,6 @@ elif [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
|
||||
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||
export PKG_CONFIG_SYSROOT_DIR=/
|
||||
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
||||
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
|
||||
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
|
||||
dpkg --add-architecture arm64
|
||||
apt-get update
|
||||
apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
|
||||
@@ -29,8 +27,6 @@ elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
||||
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
|
||||
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
|
||||
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
|
||||
dpkg --add-architecture armhf
|
||||
apt-get update
|
||||
apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
|
||||
|
||||
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
|
||||
|
||||
### Setup for Windows Users
|
||||
|
||||
If you're a Windows user, you may need to perform some additional steps:
|
||||
|
||||
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||
- The gnu `patch` tool should be installed
|
||||
|
||||
### Install Node.js Packages
|
||||
|
||||
After installing Rust and Node.js, install the necessary Node.js packages:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### Download the Clash Binary
|
||||
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
- Automatically download it via the provided script:
|
||||
```shell
|
||||
pnpm run check
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
```
|
||||
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
### Run the Development Server
|
||||
|
||||
To run the development server, use the following command:
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
# If an app instance already exists, use a different command
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
If you want to build the project, use:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Contributing Your Changes
|
||||
|
||||
Once you have made your changes:
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Commit your changes with clear and concise commit messages.
|
||||
4. Push your branch to your fork and submit a pull request to our repository.
|
||||
|
||||
We appreciate your contributions and look forward to your active participation in our project!
|
||||
68
README.md
68
README.md
@@ -9,6 +9,27 @@
|
||||
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
## Install
|
||||
|
||||
Click on the corresponding link below to download the installation package. Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
[[Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x64-setup.exe)]
|
||||
[[Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x86-setup.exe)]
|
||||
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_arm64-setup.exe)]
|
||||
|
||||
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_x64.dmg)]
|
||||
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/Clash.Verge_1.5.1_aarch64.dmg)]
|
||||
|
||||
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_amd64.AppImage)]
|
||||
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_amd64.deb)]
|
||||
[[Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_i386.AppImage)]
|
||||
[[Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_i386.deb)]
|
||||
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.1/clash-verge_1.5.1_arm64.deb)]
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
## Features
|
||||
|
||||
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
|
||||
@@ -37,27 +58,6 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x64-setup.exe)
|
||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x86-setup.exe)
|
||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_arm64-setup.exe)
|
||||
|
||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x64.dmg)
|
||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_aarch64.dmg)
|
||||
|
||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_amd64.AppImage)
|
||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_amd64.deb)
|
||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_i386.AppImage)
|
||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_i386.deb)
|
||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_arm64.deb)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
||||
@@ -66,34 +66,14 @@ open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/C
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
||||
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
Then download the clash binary... Or you can download it from [clash meta release](https://github.com/MetaCubeX/Clash.Meta/releases) and rename it according to [tauri config](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
```shell
|
||||
# force update to latest version
|
||||
# pnpm run check --force
|
||||
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Todos
|
||||
|
||||
66
UPDATELOG.md
66
UPDATELOG.md
@@ -1,3 +1,67 @@
|
||||
## v1.5.1
|
||||
|
||||
### Features
|
||||
|
||||
- 保存窗口最大化状态
|
||||
- Proxy Provider 显示数量
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 优化设置项名称
|
||||
- 自定义 GLOBAL 代理组时代理组显示错误的问题
|
||||
|
||||
---
|
||||
|
||||
## v1.5.0
|
||||
|
||||
### Features
|
||||
|
||||
- 删除 Clash 字段过滤功能
|
||||
- 添加 socks 端口和 http 端口设置
|
||||
- 升级内核到 1.18.1
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 修复 32 位版本无法显示流量信息的问题
|
||||
|
||||
---
|
||||
|
||||
## v1.4.11
|
||||
|
||||
### Break Changes
|
||||
|
||||
- 此版本更改了 Windows 安装包安装模式,需要卸载后手动安装,否则无法安装到正确位置
|
||||
|
||||
### Features
|
||||
|
||||
- 优化了系统代理开启的代码,解决了稀有场景下代理开启卡顿的问题
|
||||
- 添加 MacOS 下的 debug 日志,以便日后调试稀有场景下 MacOS 下无法开启系统代理的问题
|
||||
- MacOS 关闭 GUI 时同步杀除后台 GUI [#306](https://github.com/clash-verge-rev/clash-verge-rev/issues/306)
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 解决自动更新时文件占用问题
|
||||
- 解决稀有场景下系统代理开启失败的问题
|
||||
- 删除冗余内核代码
|
||||
|
||||
---
|
||||
|
||||
## v1.4.10
|
||||
|
||||
### Features
|
||||
|
||||
- 设置中添加退出按钮
|
||||
- 支持自定义软件启动页
|
||||
- 在 Proxy Provider 页面展示订阅信息
|
||||
- 优化 Provider 支持
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 更改端口时立即重设系统代理
|
||||
- 网站测试超时错误
|
||||
|
||||
---
|
||||
|
||||
## v1.4.9
|
||||
|
||||
### Features
|
||||
@@ -11,6 +75,8 @@
|
||||
- 连接页面时间排序错误
|
||||
- 连接页面表格宽度优化
|
||||
|
||||
---
|
||||
|
||||
## v1.4.8
|
||||
|
||||
### Features
|
||||
|
||||
42
package.json
42
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.4.9",
|
||||
"license": "GPL-3.0",
|
||||
"version": "1.5.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"dev:diff": "tauri dev -f verge-dev",
|
||||
@@ -19,26 +19,27 @@
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/icons-material": "^5.15.5",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.14.19",
|
||||
"@mui/x-data-grid": "^6.18.2",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@mui/material": "^5.15.5",
|
||||
"@mui/x-data-grid": "^6.18.7",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"ahooks": "^3.7.8",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.6.5",
|
||||
"dayjs": "1.11.5",
|
||||
"i18next": "^23.7.7",
|
||||
"i18next": "^23.7.16",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-router-dom": "^6.21.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.6.2",
|
||||
"recoil": "^0.7.7",
|
||||
@@ -48,15 +49,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.1.1",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"adm-zip": "^0.5.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -64,10 +64,10 @@
|
||||
"husky": "^7.0.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
|
||||
1242
pnpm-lock.yaml
generated
1242
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ const PLATFORM_MAP = {
|
||||
"i686-unknown-linux-gnu": "linux",
|
||||
"aarch64-unknown-linux-gnu": "linux",
|
||||
"armv7-unknown-linux-gnueabihf": "linux",
|
||||
"loongarch64-unknown-linux-gnu": "linux",
|
||||
};
|
||||
const ARCH_MAP = {
|
||||
"x86_64-pc-windows-msvc": "x64",
|
||||
@@ -32,6 +33,7 @@ const ARCH_MAP = {
|
||||
"i686-unknown-linux-gnu": "ia32",
|
||||
"aarch64-unknown-linux-gnu": "arm64",
|
||||
"armv7-unknown-linux-gnueabihf": "arm",
|
||||
"loongarch64-unknown-linux-gnu": "loong64",
|
||||
};
|
||||
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
@@ -63,6 +65,7 @@ const META_ALPHA_MAP = {
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest alpha release version from the version.txt file
|
||||
@@ -108,6 +111,7 @@ const META_MAP = {
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
|
||||
1024
src-tauri/Cargo.lock
generated
1024
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "1.4.9"
|
||||
version = "1.5.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2021"
|
||||
@@ -14,7 +14,7 @@ tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
which = "5.0.0"
|
||||
which = "6.0.0"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
open = "5.0"
|
||||
@@ -38,7 +38,7 @@ window-shadows = { version = "0.2" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
sysproxy = { git="https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
|
||||
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
||||
tauri = { version = "1.5", features = [ "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -2,14 +2,14 @@ use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
utils::{dirs, help},
|
||||
utils::{dirs, help, resolve},
|
||||
};
|
||||
use crate::{ret_err, wrap_err};
|
||||
use anyhow::{Context, Result};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
use tauri::api;
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -266,6 +266,15 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn exit_app(app_handle: tauri::AppHandle) {
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
||||
@@ -25,6 +25,8 @@ impl IClashTemp {
|
||||
let mut map = Mapping::new();
|
||||
|
||||
map.insert("mixed-port".into(), 7897.into());
|
||||
map.insert("socks-port".into(), 7898.into());
|
||||
map.insert("port".into(), 7899.into());
|
||||
map.insert("log-level".into(), "info".into());
|
||||
map.insert("allow-lan".into(), false.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
@@ -35,10 +37,14 @@ impl IClashTemp {
|
||||
}
|
||||
|
||||
fn guard(mut config: Mapping) -> Mapping {
|
||||
let port = Self::guard_mixed_port(&config);
|
||||
let mixed_port = Self::guard_mixed_port(&config);
|
||||
let socks_port = Self::guard_socks_port(&config);
|
||||
let port = Self::guard_port(&config);
|
||||
let ctrl = Self::guard_server_ctrl(&config);
|
||||
|
||||
config.insert("mixed-port".into(), port.into());
|
||||
config.insert("mixed-port".into(), mixed_port.into());
|
||||
config.insert("socks-port".into(), socks_port.into());
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
config
|
||||
}
|
||||
@@ -61,11 +67,23 @@ impl IClashTemp {
|
||||
Self::guard_mixed_port(&self.0)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_socks_port(&self) -> u16 {
|
||||
Self::guard_socks_port(&self.0)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_port(&self) -> u16 {
|
||||
Self::guard_port(&self.0)
|
||||
}
|
||||
|
||||
pub fn get_client_info(&self) -> ClashInfo {
|
||||
let config = &self.0;
|
||||
|
||||
ClashInfo {
|
||||
port: Self::guard_mixed_port(config),
|
||||
mixed_port: Self::guard_mixed_port(config),
|
||||
socks_port: Self::guard_socks_port(config),
|
||||
port: Self::guard_port(config),
|
||||
server: Self::guard_client_ctrl(config),
|
||||
secret: config.get("secret").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
@@ -91,6 +109,36 @@ impl IClashTemp {
|
||||
port
|
||||
}
|
||||
|
||||
pub fn guard_socks_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("socks-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7898);
|
||||
if port == 0 {
|
||||
port = 7898;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
pub fn guard_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7899);
|
||||
if port == 0 {
|
||||
port = 7899;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
||||
config
|
||||
.get("external-controller")
|
||||
@@ -129,6 +177,8 @@ impl IClashTemp {
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct ClashInfo {
|
||||
/// clash core port
|
||||
pub mixed_port: u16,
|
||||
pub socks_port: u16,
|
||||
pub port: u16,
|
||||
/// same as `external-controller`
|
||||
pub server: String,
|
||||
@@ -148,7 +198,9 @@ fn test_clash_info() {
|
||||
|
||||
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
||||
ClashInfo {
|
||||
port,
|
||||
mixed_port: port,
|
||||
socks_port: 7898,
|
||||
port: 7899,
|
||||
server: server.into(),
|
||||
secret: None,
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ pub struct PrfSelected {
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct PrfExtra {
|
||||
pub upload: usize,
|
||||
pub download: usize,
|
||||
pub total: usize,
|
||||
pub expire: usize,
|
||||
pub upload: u64,
|
||||
pub download: u64,
|
||||
pub total: u64,
|
||||
pub expire: u64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
@@ -261,7 +261,9 @@ impl PrfItem {
|
||||
},
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
None => Some(
|
||||
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
|
||||
),
|
||||
};
|
||||
let option = match update_interval {
|
||||
Some(val) => Some(PrfOption {
|
||||
|
||||
@@ -14,9 +14,6 @@ pub struct IProfiles {
|
||||
/// same as PrfConfig.chain
|
||||
pub chain: Option<Vec<String>>,
|
||||
|
||||
/// record valid fields for clash
|
||||
pub valid: Option<Vec<String>>,
|
||||
|
||||
/// profile list
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
@@ -55,12 +52,6 @@ impl IProfiles {
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
valid: Some(vec![
|
||||
"dns".into(),
|
||||
"sub-rules".into(),
|
||||
"unified-delay".into(),
|
||||
"tcp-concurrent".into(),
|
||||
]),
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
@@ -93,10 +84,6 @@ impl IProfiles {
|
||||
self.chain = Some(chain);
|
||||
}
|
||||
|
||||
if let Some(valid) = patch.valid {
|
||||
self.valid = Some(valid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct IVerge {
|
||||
/// copy env type
|
||||
pub env_type: Option<String>,
|
||||
|
||||
/// start page
|
||||
pub start_page: Option<String>,
|
||||
/// startup script path
|
||||
pub startup_script: Option<String>,
|
||||
|
||||
@@ -56,9 +58,6 @@ pub struct IVerge {
|
||||
/// set system proxy bypass
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
|
||||
/// set system proxy method
|
||||
pub system_proxy_registry_mode: Option<bool>,
|
||||
|
||||
/// proxy guard duration
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
|
||||
@@ -82,9 +81,6 @@ pub struct IVerge {
|
||||
/// 默认的延迟测试连接
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为关闭
|
||||
pub enable_clash_fields: Option<bool>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
@@ -102,11 +98,19 @@ pub struct IVerge {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_size_position: Option<Vec<f64>>,
|
||||
|
||||
/// window size and position
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_is_maximized: Option<bool>,
|
||||
|
||||
/// 是否启用随机端口
|
||||
pub enable_random_port: Option<bool>,
|
||||
|
||||
/// verge mixed port 用于覆盖 clash 的 mixed port
|
||||
pub verge_mixed_port: Option<u16>,
|
||||
|
||||
pub verge_socks_port: Option<u16>,
|
||||
|
||||
pub verge_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -153,19 +157,20 @@ impl IVerge {
|
||||
env_type: Some("bash".into()),
|
||||
#[cfg(target_os = "windows")]
|
||||
env_type: Some("powershell".into()),
|
||||
start_page: Some("/".into()),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_system_proxy: Some(false),
|
||||
system_proxy_registry_mode: Some(false),
|
||||
enable_random_port: Some(false),
|
||||
verge_mixed_port: Some(7897),
|
||||
verge_socks_port: Some(7898),
|
||||
verge_port: Some(7899),
|
||||
enable_proxy_guard: Some(false),
|
||||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
..Self::default()
|
||||
}
|
||||
@@ -192,6 +197,7 @@ impl IVerge {
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
patch!(env_type);
|
||||
patch!(start_page);
|
||||
patch!(startup_script);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
@@ -202,10 +208,11 @@ impl IVerge {
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_random_port);
|
||||
patch!(verge_mixed_port);
|
||||
patch!(verge_socks_port);
|
||||
patch!(verge_port);
|
||||
patch!(enable_system_proxy);
|
||||
patch!(enable_proxy_guard);
|
||||
patch!(system_proxy_bypass);
|
||||
patch!(system_proxy_registry_mode);
|
||||
patch!(proxy_guard_duration);
|
||||
|
||||
patch!(theme_setting);
|
||||
@@ -218,9 +225,9 @@ impl IVerge {
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(enable_clash_fields);
|
||||
patch!(auto_log_clean);
|
||||
patch!(window_size_position);
|
||||
patch!(window_is_maximized);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
||||
@@ -259,7 +259,7 @@ impl CoreManager {
|
||||
/// 切换核心
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||
const CLASH_CORES: [&str; 3] = ["clash", "clash-meta", "clash-meta-alpha"];
|
||||
const CLASH_CORES: [&str; 2] = ["clash-meta", "clash-meta-alpha"];
|
||||
|
||||
if !CLASH_CORES.contains(&clash_core.as_str()) {
|
||||
bail!("invalid clash core name \"{clash_core}\"");
|
||||
|
||||
@@ -58,12 +58,6 @@ impl Sysopt {
|
||||
)
|
||||
};
|
||||
|
||||
let registry_mode = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
verge.system_proxy_registry_mode.unwrap_or(false)
|
||||
};
|
||||
|
||||
let current = Sysproxy {
|
||||
enable,
|
||||
host: String::from("127.0.0.1"),
|
||||
@@ -73,15 +67,7 @@ impl Sysopt {
|
||||
|
||||
if enable {
|
||||
let old = Sysproxy::get_system_proxy().ok();
|
||||
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
current.set_system_proxy_with_registry()?;
|
||||
#[cfg(not(windows))]
|
||||
current.set_system_proxy()?;
|
||||
} else {
|
||||
current.set_system_proxy()?;
|
||||
}
|
||||
current.set_system_proxy()?;
|
||||
|
||||
*self.old_sysproxy.lock() = old;
|
||||
*self.cur_sysproxy.lock() = Some(current);
|
||||
@@ -111,26 +97,18 @@ impl Sysopt {
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let registry_mode = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
verge.system_proxy_registry_mode.unwrap_or(false)
|
||||
};
|
||||
|
||||
let mut sysproxy = cur_sysproxy.take().unwrap();
|
||||
|
||||
sysproxy.enable = enable;
|
||||
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
sysproxy.set_system_proxy_with_registry()?;
|
||||
#[cfg(not(windows))]
|
||||
sysproxy.set_system_proxy()?;
|
||||
} else {
|
||||
sysproxy.set_system_proxy()?;
|
||||
}
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
sysproxy.port = port;
|
||||
|
||||
sysproxy.set_system_proxy()?;
|
||||
*cur_sysproxy = Some(sysproxy);
|
||||
|
||||
Ok(())
|
||||
@@ -140,11 +118,7 @@ impl Sysopt {
|
||||
pub fn reset_sysproxy(&self) -> Result<()> {
|
||||
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
||||
let mut old_sysproxy = self.old_sysproxy.lock();
|
||||
let registry_mode = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
verge.system_proxy_registry_mode.unwrap_or(false)
|
||||
};
|
||||
|
||||
let cur_sysproxy = cur_sysproxy.take();
|
||||
|
||||
if let Some(mut old) = old_sysproxy.take() {
|
||||
@@ -159,26 +133,12 @@ impl Sysopt {
|
||||
log::info!(target: "app", "reset proxy to the original proxy");
|
||||
}
|
||||
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
old.set_system_proxy_with_registry()?;
|
||||
#[cfg(not(windows))]
|
||||
old.set_system_proxy()?;
|
||||
} else {
|
||||
old.set_system_proxy()?;
|
||||
}
|
||||
old.set_system_proxy()?;
|
||||
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
|
||||
// 没有原代理,就按现在的代理设置disable即可
|
||||
log::info!(target: "app", "reset proxy by disabling the current proxy");
|
||||
cur.enable = false;
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
cur.set_system_proxy_with_registry()?;
|
||||
#[cfg(not(windows))]
|
||||
cur.set_system_proxy()?;
|
||||
} else {
|
||||
cur.set_system_proxy()?;
|
||||
}
|
||||
cur.set_system_proxy()?;
|
||||
} else {
|
||||
log::info!(target: "app", "reset proxy with no action");
|
||||
}
|
||||
@@ -297,11 +257,7 @@ impl Sysopt {
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
let guard_state = self.guard_state.clone();
|
||||
let registry_mode = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
verge.system_proxy_registry_mode.unwrap_or(false)
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// if it is running, exit
|
||||
let mut state = guard_state.lock().await;
|
||||
@@ -351,14 +307,8 @@ impl Sysopt {
|
||||
port,
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
};
|
||||
if registry_mode {
|
||||
#[cfg(windows)]
|
||||
log_err!(sysproxy.set_system_proxy_with_registry());
|
||||
#[cfg(not(windows))]
|
||||
log_err!(sysproxy.set_system_proxy());
|
||||
} else {
|
||||
log_err!(sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
log_err!(sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
let mut state = guard_state.lock().await;
|
||||
|
||||
@@ -203,14 +203,8 @@ impl Tray {
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => api::process::restart(&app_handle.env()),
|
||||
"quit" => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
"quit" => cmds::exit_app(app_handle.clone()),
|
||||
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
"mode",
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
"socks-port",
|
||||
"port",
|
||||
"allow-lan",
|
||||
"log-level",
|
||||
"ipv6",
|
||||
@@ -15,71 +15,13 @@ pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
|
||||
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||
"proxies",
|
||||
"proxy-groups",
|
||||
"proxy-providers",
|
||||
"rules",
|
||||
"proxy-groups",
|
||||
"rule-providers",
|
||||
"rules",
|
||||
];
|
||||
|
||||
pub const OTHERS_FIELDS: [&str; 31] = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
"hosts",
|
||||
"script",
|
||||
"profile",
|
||||
"payload",
|
||||
"tunnels",
|
||||
"auto-redir",
|
||||
"experimental",
|
||||
"interface-name",
|
||||
"routing-mark",
|
||||
"redir-port",
|
||||
"tproxy-port",
|
||||
"iptables",
|
||||
"external-ui",
|
||||
"bind-address",
|
||||
"authentication",
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"geox-url", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
"skip-auth-prefixes", // meta
|
||||
"external-controller-tls", // meta
|
||||
"global-client-fingerprint", // meta
|
||||
];
|
||||
|
||||
pub fn use_clash_fields() -> Vec<String> {
|
||||
DEFAULT_FIELDS
|
||||
.into_iter()
|
||||
.chain(HANDLE_FIELDS)
|
||||
.chain(OTHERS_FIELDS)
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
|
||||
let others = Vec::from(OTHERS_FIELDS);
|
||||
|
||||
valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
|
||||
valid
|
||||
.into_iter()
|
||||
.filter(|s| others.contains(&s.as_str()))
|
||||
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
|
||||
if !enable {
|
||||
return config;
|
||||
}
|
||||
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
@@ -105,40 +47,35 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
pub fn use_sort(config: Mapping) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
HANDLE_FIELDS.into_iter().for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
}
|
||||
});
|
||||
|
||||
HANDLE_FIELDS
|
||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
|
||||
|
||||
let config_keys: HashSet<&str> = config
|
||||
.keys()
|
||||
.filter_map(|e| e.as_str())
|
||||
.into_iter()
|
||||
.chain(OTHERS_FIELDS)
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
}
|
||||
});
|
||||
.collect();
|
||||
|
||||
if !enable_filter {
|
||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
||||
.into_iter()
|
||||
.chain(OTHERS_FIELDS)
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.collect();
|
||||
|
||||
let config_keys: HashSet<&str> = config
|
||||
.keys()
|
||||
.filter_map(|e| e.as_str())
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
}
|
||||
});
|
||||
DEFAULT_FIELDS.into_iter().for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
}
|
||||
});
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
||||
});
|
||||
|
||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||
let merge = use_filter(merge, &merge_list.collect(), true);
|
||||
let merge = use_filter(merge, &merge_list.collect());
|
||||
|
||||
["rules", "proxies", "proxy-groups"]
|
||||
.iter()
|
||||
|
||||
@@ -22,19 +22,18 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
|
||||
let (clash_core, enable_tun, enable_builtin) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
verge.enable_tun_mode.unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.enable_clash_fields.unwrap_or(true),
|
||||
)
|
||||
};
|
||||
|
||||
// 从profiles里拿东西
|
||||
let (mut config, chain, valid) = {
|
||||
let (mut config, chain) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
|
||||
@@ -49,23 +48,17 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let valid = profiles.valid.clone().unwrap_or_default();
|
||||
|
||||
(current, chain, valid)
|
||||
(current, chain)
|
||||
};
|
||||
|
||||
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||
|
||||
let valid = use_valid_fields(valid);
|
||||
config = use_filter(config, &valid, enable_filter);
|
||||
|
||||
// 处理用户的profile
|
||||
chain.into_iter().for_each(|item| match item.data {
|
||||
ChainType::Merge(merge) => {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(merge, config.to_owned());
|
||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
||||
}
|
||||
ChainType::Script(script) => {
|
||||
let mut logs = vec![];
|
||||
@@ -73,7 +66,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = use_filter(res_config, &valid, enable_filter);
|
||||
config = res_config;
|
||||
logs.extend(res_logs);
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||
@@ -88,8 +81,6 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
let clash_fields = use_clash_fields();
|
||||
|
||||
// 内建脚本最后跑
|
||||
if enable_builtin {
|
||||
ChainItem::builtin()
|
||||
@@ -102,7 +93,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
match item.data {
|
||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, _)) => {
|
||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
||||
config = res_config;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "builtin script error `{err}`");
|
||||
@@ -113,12 +104,11 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
});
|
||||
}
|
||||
|
||||
config = use_filter(config, &clash_fields, enable_filter);
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config, enable_filter);
|
||||
config = use_sort(config);
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
|
||||
exists_set.extend(exists_keys.into_iter());
|
||||
exists_keys = exists_set.into_iter().collect();
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
|
||||
@@ -162,6 +162,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
|
||||
match {
|
||||
let mixed_port = patch.get("mixed-port");
|
||||
let socks_port = patch.get("socks-port");
|
||||
let port = patch.get("port");
|
||||
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||
if mixed_port.is_some() && !enable_random_port {
|
||||
let changed = mixed_port.unwrap()
|
||||
@@ -182,6 +184,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
|
||||
// 激活订阅
|
||||
if mixed_port.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| port.is_some()
|
||||
|| patch.get("secret").is_some()
|
||||
|| patch.get("external-controller").is_some()
|
||||
{
|
||||
@@ -225,6 +229,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let port = patch.verge_mixed_port;
|
||||
|
||||
match {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -249,7 +254,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
if auto_launch.is_some() {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
|
||||
sysopt::Sysopt::global().update_sysproxy()?;
|
||||
sysopt::Sysopt::global().guard_proxy();
|
||||
}
|
||||
@@ -377,23 +382,26 @@ pub async fn test_delay(url: String) -> Result<u32> {
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url);
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
@@ -55,6 +55,7 @@ fn main() -> std::io::Result<()> {
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
cmds::test_delay,
|
||||
cmds::exit_app,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
cmds::get_profiles,
|
||||
@@ -105,34 +106,10 @@ fn main() -> std::io::Result<()> {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
}
|
||||
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
use tauri::Manager;
|
||||
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
|
||||
app_handle.get_window("main").map(|win| {
|
||||
let _ = win.hide();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
|
||||
@@ -80,6 +80,19 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// get the last part of the url, if not found, return empty string
|
||||
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
|
||||
let segments: Vec<&str> = path.split('/').collect();
|
||||
let last_segment = segments.last()?;
|
||||
|
||||
Some(
|
||||
percent_encoding::percent_decode_str(last_segment)
|
||||
.decode_utf8_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// open file
|
||||
/// use vscode by default
|
||||
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
|
||||
@@ -12,6 +12,7 @@ use serde_yaml::Mapping;
|
||||
use std::net::TcpListener;
|
||||
use tauri::api::notification;
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||
|
||||
@@ -156,61 +157,58 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
match builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.build()
|
||||
{
|
||||
Ok(win) => {
|
||||
log::trace!("try to calculate the monitor size");
|
||||
let center = (|| -> Result<bool> {
|
||||
let mut center = false;
|
||||
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||
let size = monitor.size();
|
||||
let pos = win.outer_position()?;
|
||||
|
||||
if pos.x < -400
|
||||
|| pos.x > (size.width - 200).try_into()?
|
||||
|| pos.y < -200
|
||||
|| pos.y > (size.height - 200).try_into()?
|
||||
{
|
||||
center = true;
|
||||
}
|
||||
Ok(center)
|
||||
})();
|
||||
|
||||
if center.unwrap_or(true) {
|
||||
trace_err!(win.center(), "set win center");
|
||||
}
|
||||
|
||||
log::trace!("try to create window");
|
||||
let app_handle = app_handle.clone();
|
||||
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
trace_err!(set_shadow(&window, true), "set win shadow");
|
||||
} else {
|
||||
log::error!(target: "app", "failed to create window, get_window is None")
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
let window = builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.build();
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::log_err!(builder
|
||||
let window = builder
|
||||
.decorations(true)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.build());
|
||||
|
||||
.build();
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::log_err!(builder.decorations(true).transparent(false).build());
|
||||
let window = builder.decorations(true).transparent(false).build();
|
||||
|
||||
match window {
|
||||
Ok(win) => {
|
||||
let is_maximized = Config::verge()
|
||||
.latest()
|
||||
.window_is_maximized
|
||||
.unwrap_or(false);
|
||||
log::trace!("try to calculate the monitor size");
|
||||
let center = (|| -> Result<bool> {
|
||||
let mut center = false;
|
||||
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||
let size = monitor.size();
|
||||
let pos = win.outer_position()?;
|
||||
|
||||
if pos.x < -400
|
||||
|| pos.x > (size.width - 200).try_into()?
|
||||
|| pos.y < -200
|
||||
|| pos.y > (size.height - 200).try_into()?
|
||||
{
|
||||
center = true;
|
||||
}
|
||||
Ok(center)
|
||||
})();
|
||||
|
||||
if center.unwrap_or(true) {
|
||||
trace_err!(win.center(), "set win center");
|
||||
}
|
||||
|
||||
trace_err!(set_shadow(&win, true), "set win shadow");
|
||||
if is_maximized {
|
||||
trace_err!(win.maximize(), "set win maximize");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("failed to create window");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// save window size and position
|
||||
@@ -231,11 +229,11 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
|
||||
let size = size.to_logical::<f64>(scale);
|
||||
let pos = win.outer_position()?;
|
||||
let pos = pos.to_logical::<f64>(scale);
|
||||
|
||||
if size.width >= 600.0 && size.height >= 520.0 {
|
||||
let is_maximized = win.is_maximized()?;
|
||||
verge.window_is_maximized = Some(is_maximized);
|
||||
if !is_maximized && size.width >= 600.0 && size.height >= 520.0 {
|
||||
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.4.9"
|
||||
"version": "1.5.1"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"targets": ["deb", "appimage", "updater"],
|
||||
"deb": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./clash-verge.desktop"
|
||||
"desktopTemplate": "./template/clash-verge.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"targets": ["app", "dmg", "updater"],
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"minimumSystemVersion": "10.15",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"displayLanguageSelector": true,
|
||||
"installerIcon": "icons/icon.ico",
|
||||
"languages": ["SimpChinese", "English"],
|
||||
"license": "../LICENSE"
|
||||
"license": "../LICENSE",
|
||||
"installMode": "perMachine",
|
||||
"template": "./template/installer.nsi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
758
src-tauri/template/installer.nsi
Normal file
758
src-tauri/template/installer.nsi
Normal file
@@ -0,0 +1,758 @@
|
||||
; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi
|
||||
; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.
|
||||
|
||||
Unicode true
|
||||
; Set the compression algorithm. Default is LZMA.
|
||||
!if "{{compression}}" == ""
|
||||
SetCompressor /SOLID lzma
|
||||
!else
|
||||
SetCompressor /SOLID "{{compression}}"
|
||||
!endif
|
||||
|
||||
!include MUI2.nsh
|
||||
!include FileFunc.nsh
|
||||
!include x64.nsh
|
||||
!include WordFunc.nsh
|
||||
!include "LogicLib.nsh"
|
||||
!include "StrFunc.nsh"
|
||||
${StrCase}
|
||||
${StrLoc}
|
||||
|
||||
!define MANUFACTURER "{{manufacturer}}"
|
||||
!define PRODUCTNAME "{{product_name}}"
|
||||
!define VERSION "{{version}}"
|
||||
!define VERSIONWITHBUILD "{{version_with_build}}"
|
||||
!define SHORTDESCRIPTION "{{short_description}}"
|
||||
!define INSTALLMODE "{{install_mode}}"
|
||||
!define LICENSE "{{license}}"
|
||||
!define INSTALLERICON "{{installer_icon}}"
|
||||
!define SIDEBARIMAGE "{{sidebar_image}}"
|
||||
!define HEADERIMAGE "{{header_image}}"
|
||||
!define MAINBINARYNAME "{{main_binary_name}}"
|
||||
!define MAINBINARYSRCPATH "{{main_binary_path}}"
|
||||
!define BUNDLEID "{{bundle_id}}"
|
||||
!define COPYRIGHT "{{copyright}}"
|
||||
!define OUTFILE "{{out_file}}"
|
||||
!define ARCH "{{arch}}"
|
||||
!define PLUGINSPATH "{{additional_plugins_path}}"
|
||||
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
|
||||
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
|
||||
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
|
||||
!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}"
|
||||
!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}"
|
||||
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
|
||||
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
|
||||
!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}"
|
||||
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
|
||||
!define ESTIMATEDSIZE "{{estimated_size}}"
|
||||
|
||||
Name "${PRODUCTNAME}"
|
||||
BrandingText "${COPYRIGHT}"
|
||||
OutFile "${OUTFILE}"
|
||||
|
||||
VIProductVersion "${VERSIONWITHBUILD}"
|
||||
VIAddVersionKey "ProductName" "${PRODUCTNAME}"
|
||||
VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}"
|
||||
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
|
||||
; Plugins path, currently exists for linux only
|
||||
!if "${PLUGINSPATH}" != ""
|
||||
!addplugindir "${PLUGINSPATH}"
|
||||
!endif
|
||||
|
||||
!if "${UNINSTALLERSIGNCOMMAND}" != ""
|
||||
!uninstfinalize '${UNINSTALLERSIGNCOMMAND}'
|
||||
!endif
|
||||
|
||||
; Handle install mode, `perUser`, `perMachine` or `both`
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
RequestExecutionLevel highest
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
RequestExecutionLevel user
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!define MULTIUSER_MUI
|
||||
!define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}"
|
||||
!define MULTIUSER_INSTALLMODE_COMMANDLINE
|
||||
!if "${ARCH}" == "x64"
|
||||
!define MULTIUSER_USE_PROGRAMFILES64
|
||||
!else if "${ARCH}" == "arm64"
|
||||
!define MULTIUSER_USE_PROGRAMFILES64
|
||||
!endif
|
||||
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}"
|
||||
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser"
|
||||
!define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME
|
||||
!define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation
|
||||
!define MULTIUSER_EXECUTIONLEVEL Highest
|
||||
!include MultiUser.nsh
|
||||
!endif
|
||||
|
||||
; installer icon
|
||||
!if "${INSTALLERICON}" != ""
|
||||
!define MUI_ICON "${INSTALLERICON}"
|
||||
!endif
|
||||
|
||||
; installer sidebar image
|
||||
!if "${SIDEBARIMAGE}" != ""
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
|
||||
!endif
|
||||
|
||||
; installer header image
|
||||
!if "${HEADERIMAGE}" != ""
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
|
||||
!endif
|
||||
|
||||
; Define registry key to store installer language
|
||||
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}"
|
||||
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
|
||||
|
||||
; Installer pages, must be ordered as they appear
|
||||
; 1. Welcome Page
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
|
||||
; 2. License Page (if defined)
|
||||
!if "${LICENSE}" != ""
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_LICENSE "${LICENSE}"
|
||||
!endif
|
||||
|
||||
; 3. Install mode (if it is set to `both`)
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MULTIUSER_PAGE_INSTALLMODE
|
||||
!endif
|
||||
|
||||
|
||||
; 4. Custom page to ask user if he wants to reinstall/uninstall
|
||||
; only if a previous installtion was detected
|
||||
Var ReinstallPageCheck
|
||||
Page custom PageReinstall PageLeaveReinstall
|
||||
Function PageReinstall
|
||||
; Uninstall previous WiX installation if exists.
|
||||
;
|
||||
; A WiX installer stores the isntallation info in registry
|
||||
; using a UUID and so we have to loop through all keys under
|
||||
; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
|
||||
; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}
|
||||
;
|
||||
; This has a potentional issue that there maybe another installation that matches
|
||||
; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,
|
||||
; however, this should be fine since the user will have to confirm the uninstallation
|
||||
; and they can chose to abort it if doesn't make sense.
|
||||
StrCpy $0 0
|
||||
|
||||
wix_loop:
|
||||
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
|
||||
StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on
|
||||
IntOp $0 $0 + 1
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
|
||||
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher"
|
||||
StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
|
||||
${StrCase} $R1 $R0 "L"
|
||||
${StrLoc} $R0 $R1 "msiexec" ">"
|
||||
StrCmp $R0 0 0 wix_done
|
||||
StrCpy $R7 "wix"
|
||||
StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1"
|
||||
Goto compare_version
|
||||
wix_done:
|
||||
|
||||
; Check if there is an existing installation, if not, abort the reinstall page
|
||||
ReadRegStr $R0 SHCTX "${UNINSTKEY}" ""
|
||||
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||
${IfThen} "$R0$R1" == "" ${|} Abort ${|}
|
||||
|
||||
; Compare this installar version with the existing installation
|
||||
; and modify the messages presented to the user accordingly
|
||||
compare_version:
|
||||
StrCpy $R4 "$(older)"
|
||||
${If} $R7 == "wix"
|
||||
ReadRegStr $R0 HKLM "$R6" "DisplayVersion"
|
||||
${Else}
|
||||
ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion"
|
||||
${EndIf}
|
||||
${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|}
|
||||
|
||||
nsis_tauri_utils::SemverCompare "${VERSION}" $R0
|
||||
Pop $R0
|
||||
; Reinstalling the same version
|
||||
${If} $R0 == 0
|
||||
StrCpy $R1 "$(alreadyInstalledLong)"
|
||||
StrCpy $R2 "$(addOrReinstall)"
|
||||
StrCpy $R3 "$(uninstallApp)"
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)"
|
||||
StrCpy $R5 "2"
|
||||
; Upgrading
|
||||
${ElseIf} $R0 == 1
|
||||
StrCpy $R1 "$(olderOrUnknownVersionInstalled)"
|
||||
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||
StrCpy $R3 "$(dontUninstall)"
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||
StrCpy $R5 "1"
|
||||
; Downgrading
|
||||
${ElseIf} $R0 == -1
|
||||
StrCpy $R1 "$(newerVersionInstalled)"
|
||||
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||
!if "${ALLOWDOWNGRADES}" == "true"
|
||||
StrCpy $R3 "$(dontUninstall)"
|
||||
!else
|
||||
StrCpy $R3 "$(dontUninstallDowngrade)"
|
||||
!endif
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||
StrCpy $R5 "1"
|
||||
${Else}
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
Call SkipIfPassive
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $R4
|
||||
${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 24u $R1
|
||||
Pop $R1
|
||||
|
||||
${NSD_CreateRadioButton} 30u 50u -30u 8u $R2
|
||||
Pop $R2
|
||||
${NSD_OnClick} $R2 PageReinstallUpdateSelection
|
||||
|
||||
${NSD_CreateRadioButton} 30u 70u -30u 8u $R3
|
||||
Pop $R3
|
||||
; disable this radio button if downgrading and downgrades are disabled
|
||||
!if "${ALLOWDOWNGRADES}" == "false"
|
||||
${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|}
|
||||
!endif
|
||||
${NSD_OnClick} $R3 PageReinstallUpdateSelection
|
||||
|
||||
; Check the first radio button if this the first time
|
||||
; we enter this page or if the second button wasn't
|
||||
; selected the last time we were on this page
|
||||
${If} $ReinstallPageCheck != 2
|
||||
SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||
${Else}
|
||||
SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||
${EndIf}
|
||||
|
||||
${NSD_SetFocus} $R2
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
Function PageReinstallUpdateSelection
|
||||
${NSD_GetState} $R2 $R1
|
||||
${If} $R1 == ${BST_CHECKED}
|
||||
StrCpy $ReinstallPageCheck 1
|
||||
${Else}
|
||||
StrCpy $ReinstallPageCheck 2
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
Function PageLeaveReinstall
|
||||
${NSD_GetState} $R2 $R1
|
||||
|
||||
; $R5 holds whether we are reinstalling the same version or not
|
||||
; $R5 == "1" -> different versions
|
||||
; $R5 == "2" -> same version
|
||||
;
|
||||
; $R1 holds the radio buttons state. its meaning is dependant on the context
|
||||
StrCmp $R5 "1" 0 +2 ; Existing install is not the same version?
|
||||
StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling
|
||||
StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling
|
||||
|
||||
reinst_uninstall:
|
||||
HideWindow
|
||||
ClearErrors
|
||||
|
||||
${If} $R7 == "wix"
|
||||
ReadRegStr $R1 HKLM "$R6" "UninstallString"
|
||||
ExecWait '$R1' $0
|
||||
${Else}
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||
ExecWait '$R1 /P _?=$4' $0
|
||||
${EndIf}
|
||||
|
||||
BringToFront
|
||||
|
||||
${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code
|
||||
|
||||
${If} $0 <> 0
|
||||
${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
${If} $0 = 1 ; User aborted uninstaller?
|
||||
StrCmp $R5 "2" 0 +2 ; Is the existing install the same version?
|
||||
Quit ; ...yes, already installed, we are done
|
||||
Abort
|
||||
${EndIf}
|
||||
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||
Abort
|
||||
${Else}
|
||||
StrCpy $0 $R1 1
|
||||
${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString
|
||||
Delete $R1
|
||||
RMDir $INSTDIR
|
||||
${EndIf}
|
||||
reinst_done:
|
||||
FunctionEnd
|
||||
|
||||
; 5. Choose install directoy page
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
; 6. Start menu shortcut page
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
Var AppStartMenuFolder
|
||||
!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder
|
||||
|
||||
; 7. Installation page
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
; 8. Finish page
|
||||
;
|
||||
; Don't auto jump to finish page after installation page,
|
||||
; because the installation page has useful info that can be used debug any issues with the installer.
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||
; Use show readme button in the finish page as a button create a desktop shortcut
|
||||
!define MUI_FINISHPAGE_SHOWREADME
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
|
||||
; Show run app after installation.
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
; Uninstaller Pages
|
||||
; 1. Confirm uninstall page
|
||||
Var DeleteAppDataCheckbox
|
||||
Var DeleteAppDataCheckboxState
|
||||
!define /ifndef WS_EX_LAYOUTRTL 0x00400000
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow
|
||||
Function un.ConfirmShow
|
||||
FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog
|
||||
${If} $(^RTL) == 1
|
||||
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
|
||||
${Else}
|
||||
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
|
||||
${EndIf}
|
||||
Pop $DeleteAppDataCheckbox
|
||||
SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1
|
||||
SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1
|
||||
FunctionEnd
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave
|
||||
Function un.ConfirmLeave
|
||||
SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState
|
||||
FunctionEnd
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
|
||||
; 2. Uninstalling Page
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;Languages
|
||||
{{#each languages}}
|
||||
!insertmacro MUI_LANGUAGE "{{this}}"
|
||||
{{/each}}
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
{{#each language_files}}
|
||||
!include "{{this}}"
|
||||
{{/each}}
|
||||
|
||||
!macro SetContext
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
SetShellVarContext current
|
||||
!else if "${INSTALLMODE}" == "perMachine"
|
||||
SetShellVarContext all
|
||||
!endif
|
||||
|
||||
${If} ${RunningX64}
|
||||
!if "${ARCH}" == "x64"
|
||||
SetRegView 64
|
||||
!else if "${ARCH}" == "arm64"
|
||||
SetRegView 64
|
||||
!else
|
||||
SetRegView 32
|
||||
!endif
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
Var PassiveMode
|
||||
Function .onInit
|
||||
${GetOptions} $CMDLINE "/P" $PassiveMode
|
||||
IfErrors +2 0
|
||||
StrCpy $PassiveMode 1
|
||||
|
||||
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
!endif
|
||||
|
||||
!insertmacro SetContext
|
||||
|
||||
${If} $INSTDIR == ""
|
||||
; Set default install location
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
${If} ${RunningX64}
|
||||
!if "${ARCH}" == "x64"
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
!else if "${ARCH}" == "arm64"
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
!else
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
!endif
|
||||
${Else}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
!else if "${INSTALLMODE}" == "currentUser"
|
||||
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
|
||||
!endif
|
||||
|
||||
Call RestorePreviousInstallLocation
|
||||
${EndIf}
|
||||
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_INIT
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
!macro CheckAllVergeProcesses
|
||||
; Check if Clash Verge.exe is running
|
||||
nsis_tauri_utils::FindProcess "Clash Verge.exe"
|
||||
${If} $R0 != 0
|
||||
; Kill the process
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "Clash Verge.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "Clash Verge.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
|
||||
; Check if clash-verge-service.exe is running
|
||||
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
|
||||
${If} $R0 != 0
|
||||
; Kill the process
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
|
||||
; Check if clash-meta-alpha.exe is running
|
||||
nsis_tauri_utils::FindProcess "clash-meta-alpha.exe"
|
||||
${If} $R0 != 0
|
||||
; Kill the process
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "clash-meta-alpha.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "clash-meta-alpha.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
; Check if clash-meta.exe is running
|
||||
nsis_tauri_utils::FindProcess "clash-meta.exe"
|
||||
${If} $R0 != 0
|
||||
; Kill the process
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "clash-meta.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "clash-meta.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
Section
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
SectionEnd
|
||||
|
||||
Section EarlyChecks
|
||||
; Abort silent installer if downgrades is disabled
|
||||
!if "${ALLOWDOWNGRADES}" == "false"
|
||||
IfSilent 0 silent_downgrades_done
|
||||
; If downgrading
|
||||
${If} $R0 == -1
|
||||
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||
${If} $0 != 0
|
||||
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||
FileWrite $0 "$(silentDowngrades)"
|
||||
${EndIf}
|
||||
Abort
|
||||
${EndIf}
|
||||
silent_downgrades_done:
|
||||
!endif
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section WebView2
|
||||
; Check if Webview2 is already installed and skip this section
|
||||
${If} ${RunningX64}
|
||||
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${Else}
|
||||
ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${EndIf}
|
||||
ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
|
||||
StrCmp $4 "" 0 webview2_done
|
||||
StrCmp $5 "" 0 webview2_done
|
||||
|
||||
; Webview2 install modes
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper"
|
||||
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
DetailPrint "$(webview2Downloading)"
|
||||
nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Pop $0
|
||||
${If} $0 == 0
|
||||
DetailPrint "$(webview2DownloadSuccess)"
|
||||
${Else}
|
||||
DetailPrint "$(webview2DownloadError)"
|
||||
Abort "$(webview2AbortError)"
|
||||
${EndIf}
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper"
|
||||
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}"
|
||||
DetailPrint "$(installingWebview2)"
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller"
|
||||
Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||
File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}"
|
||||
DetailPrint "$(installingWebview2)"
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
Goto webview2_done
|
||||
|
||||
install_webview2:
|
||||
DetailPrint "$(installingWebview2)"
|
||||
; $6 holds the path to the webview2 installer
|
||||
ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1
|
||||
${If} $1 == 0
|
||||
DetailPrint "$(webview2InstallSuccess)"
|
||||
${Else}
|
||||
DetailPrint "$(webview2InstallError)"
|
||||
Abort "$(webview2AbortError)"
|
||||
${EndIf}
|
||||
webview2_done:
|
||||
SectionEnd
|
||||
|
||||
!macro CheckIfAppIsRunning
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
IfSilent kill 0
|
||||
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
|
||||
kill:
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
${If} $R0 = 0
|
||||
Goto app_check_done
|
||||
${Else}
|
||||
IfSilent silent ui
|
||||
silent:
|
||||
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||
${If} $0 != 0
|
||||
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||
FileWrite $0 "$(appRunning)$\n"
|
||||
${EndIf}
|
||||
Abort
|
||||
ui:
|
||||
Abort "$(failedToKillApp)"
|
||||
${EndIf}
|
||||
cancel:
|
||||
Abort "$(appRunning)"
|
||||
${EndIf}
|
||||
app_check_done:
|
||||
!macroend
|
||||
|
||||
Section Install
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
|
||||
; Copy resources
|
||||
{{#each resources_dirs}}
|
||||
CreateDirectory "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
{{#each resources}}
|
||||
File /a "/oname={{this.[1]}}" "{{@key}}"
|
||||
{{/each}}
|
||||
|
||||
; Copy external binaries
|
||||
{{#each binaries}}
|
||||
File /a "/oname={{this}}" "{{@key}}"
|
||||
{{/each}}
|
||||
|
||||
; Create uninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Save $INSTDIR in registry for future installations
|
||||
WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
; Save install mode to be selected by default for the next installation such as updating
|
||||
; or when uninstalling
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1
|
||||
!endif
|
||||
|
||||
; Registry information for add/remove programs
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1"
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1"
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "${ESTIMATEDSIZE}"
|
||||
|
||||
; Create start menu shortcut (GUI)
|
||||
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||
Call CreateStartMenuShortcut
|
||||
!insertmacro MUI_STARTMENU_WRITE_END
|
||||
|
||||
; Create shortcuts for silent and passive installers, which
|
||||
; can be disabled by passing `/NS` flag
|
||||
; GUI installer has buttons for users to control creating them
|
||||
IfSilent check_ns_flag 0
|
||||
${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|}
|
||||
Goto shortcuts_done
|
||||
check_ns_flag:
|
||||
${GetOptions} $CMDLINE "/NS" $R0
|
||||
IfErrors 0 shortcuts_done
|
||||
Call CreateDesktopShortcut
|
||||
Call CreateStartMenuShortcut
|
||||
shortcuts_done:
|
||||
|
||||
; Auto close this page for passive mode
|
||||
${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|}
|
||||
SectionEnd
|
||||
|
||||
Function .onInstSuccess
|
||||
; Check for `/R` flag only in silent and passive installers because
|
||||
; GUI installer has a toggle for the user to (re)start the app
|
||||
IfSilent check_r_flag 0
|
||||
${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|}
|
||||
Goto run_done
|
||||
check_r_flag:
|
||||
${GetOptions} $CMDLINE "/R" $R0
|
||||
IfErrors run_done 0
|
||||
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
|
||||
run_done:
|
||||
FunctionEnd
|
||||
|
||||
Function un.onInit
|
||||
!insertmacro SetContext
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_UNINIT
|
||||
!endif
|
||||
|
||||
!insertmacro MUI_UNGETLANGUAGE
|
||||
FunctionEnd
|
||||
|
||||
Section Uninstall
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
; Delete the app directory and its content from disk
|
||||
; Copy main executable
|
||||
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
|
||||
; Delete resources
|
||||
{{#each resources}}
|
||||
Delete "$INSTDIR\\{{this.[1]}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete external binaries
|
||||
{{#each binaries}}
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
${If} $DeleteAppDataCheckboxState == 1
|
||||
RMDir /R /REBOOTOK "$INSTDIR"
|
||||
${Else}
|
||||
{{#each resources_ancestors}}
|
||||
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
RMDir "$INSTDIR"
|
||||
${EndIf}
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
|
||||
; Remove desktop shortcuts
|
||||
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
DeleteRegKey SHCTX "${UNINSTKEY}"
|
||||
!else if "${INSTALLMODE}" == "perMachine"
|
||||
DeleteRegKey HKLM "${UNINSTKEY}"
|
||||
!else
|
||||
DeleteRegKey HKCU "${UNINSTKEY}"
|
||||
!endif
|
||||
|
||||
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
|
||||
|
||||
; Delete app data
|
||||
${If} $DeleteAppDataCheckboxState == 1
|
||||
SetShellVarContext current
|
||||
RmDir /r "$APPDATA\${BUNDLEID}"
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
${EndIf}
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
SetAutoClose true
|
||||
SectionEnd
|
||||
|
||||
Function RestorePreviousInstallLocation
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
StrCmp $4 "" +2 0
|
||||
StrCpy $INSTDIR $4
|
||||
FunctionEnd
|
||||
|
||||
Function SkipIfPassive
|
||||
${IfThen} $PassiveMode == 1 ${|} Abort ${|}
|
||||
FunctionEnd
|
||||
|
||||
Function CreateDesktopShortcut
|
||||
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
ApplicationID::Set "$DESKTOP\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
|
||||
FunctionEnd
|
||||
|
||||
Function CreateStartMenuShortcut
|
||||
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
|
||||
FunctionEnd
|
||||
@@ -27,8 +27,8 @@ body {
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
.base-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
|
||||
> section {
|
||||
position: relative;
|
||||
|
||||
@@ -7,25 +7,32 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
styled,
|
||||
Box,
|
||||
alpha,
|
||||
Typography,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getProviders, providerUpdate } from "@/services/api";
|
||||
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getProviders", getProviders);
|
||||
const { data } = useSWR("getProxyProviders", getProxyProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await providerUpdate(key);
|
||||
await proxyProviderUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProviders");
|
||||
await mutate("getProxyProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
@@ -43,7 +50,23 @@ export const ProviderButton = () => {
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Proxy Provider")}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key, item]) => {
|
||||
await proxyProviderUpdate(key);
|
||||
await mutate("getProxies");
|
||||
await mutate("getProxyProviders");
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
@@ -53,30 +76,84 @@ export const ProviderButton = () => {
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
const sub = item.subscriptionInfo;
|
||||
const hasSubInfo = !!sub;
|
||||
const upload = sub?.Upload || 0;
|
||||
const download = sub?.Download || 0;
|
||||
const total = sub?.Total || 0;
|
||||
const expire = sub?.Expire || 0;
|
||||
const progress = Math.round(
|
||||
((download + upload) * 100) / (total + 0.1)
|
||||
);
|
||||
return (
|
||||
<ListItem sx={{ p: 0 }} key={key}>
|
||||
<ListItemText
|
||||
primary={key}
|
||||
secondary={
|
||||
<>
|
||||
<span style={{ marginRight: "4em" }}>
|
||||
Type: {item.vehicleType}
|
||||
</span>
|
||||
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
|
||||
Updated: {time.fromNow()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
<>
|
||||
<ListItem
|
||||
sx={(theme) => ({
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
boxShadow: theme.shadows[2],
|
||||
mb: 1,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
||||
{item.proxies.length}
|
||||
</TypeBox>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||
<span title="Used / Total">
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title="Expire Time">
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
color="inherit"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
@@ -84,3 +161,38 @@ export const ProviderButton = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
color: alpha(theme.palette.secondary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
color: alpha(theme.palette.primary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const boxStyle = {
|
||||
height: 26,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
};
|
||||
|
||||
function parseExpire(expire?: number) {
|
||||
if (!expire) return "-";
|
||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
159
src/components/rule/provider-button.tsx
Normal file
159
src/components/rule/provider-button.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import dayjs from "dayjs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
styled,
|
||||
Box,
|
||||
alpha,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { RefreshRounded } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
|
||||
import { BaseDialog } from "../base";
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSWR("getRuleProviders", getRuleProviders);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
|
||||
const handleUpdate = useLockFn(async (key: string) => {
|
||||
await ruleProviderUpdate(key);
|
||||
await mutate("getRules");
|
||||
await mutate("getRuleProviders");
|
||||
});
|
||||
|
||||
if (!hasProvider) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Rule Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key, item]) => {
|
||||
await ruleProviderUpdate(key);
|
||||
await mutate("getRules");
|
||||
await mutate("getRuleProviders");
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item]) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={(theme) => ({
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
boxShadow: theme.shadows[2],
|
||||
mb: 1,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
||||
{item.ruleCount}
|
||||
</TypeBox>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{item.behavior}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="Update Provider"
|
||||
onClick={() => handleUpdate(key)}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
color: alpha(theme.palette.secondary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
color: alpha(theme.palette.primary.main, 0.8),
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
marginRight: "4px",
|
||||
padding: "0 2px",
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
@@ -1,123 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Checkbox, Divider, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { InfoRounded } from "@mui/icons-material";
|
||||
import { getRuntimeExists } from "@/services/cmds";
|
||||
import {
|
||||
HANDLE_FIELDS,
|
||||
DEFAULT_FIELDS,
|
||||
OTHERS_FIELDS,
|
||||
} from "@/utils/clash-fields";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
const otherFields = [...OTHERS_FIELDS];
|
||||
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS];
|
||||
|
||||
export const ClashFieldViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { profiles = {}, patchProfiles } = useProfiles();
|
||||
const { data: existsKeys = [], mutate: mutateExists } = useSWR(
|
||||
"getRuntimeExists",
|
||||
getRuntimeExists
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
mutateExists();
|
||||
setSelected(profiles.valid || []);
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const handleChange = (item: string) => {
|
||||
if (!item) return;
|
||||
|
||||
setSelected((old) =>
|
||||
old.includes(item) ? old.filter((e) => e !== item) : [...old, item]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setOpen(false);
|
||||
|
||||
const oldSet = new Set(profiles.valid || []);
|
||||
const curSet = new Set(selected);
|
||||
const joinSet = new Set(selected.concat([...oldSet]));
|
||||
|
||||
if (curSet.size === oldSet.size && curSet.size === joinSet.size) return;
|
||||
|
||||
try {
|
||||
await patchProfiles({ valid: [...curSet] });
|
||||
// Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Clash Field")}
|
||||
contentSx={{
|
||||
pb: 0,
|
||||
width: 320,
|
||||
height: 300,
|
||||
overflowY: "auto",
|
||||
userSelect: "text",
|
||||
}}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Back")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleSave}
|
||||
>
|
||||
{otherFields.map((item) => {
|
||||
const inSelect = selected.includes(item);
|
||||
const inConfig = existsKeys.includes(item);
|
||||
|
||||
return (
|
||||
<Stack key={item} mb={0.5} direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
checked={inSelect}
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
onChange={() => handleChange(item)}
|
||||
/>
|
||||
<Typography width="100%">{item}</Typography>
|
||||
|
||||
{!inSelect && inConfig && <WarnIcon />}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
<Divider sx={{ my: 1 }}>
|
||||
<Typography color="text.secondary" fontSize={14}>
|
||||
Clash Verge Control Fields
|
||||
</Typography>
|
||||
</Divider>
|
||||
|
||||
{handleFields.map((item) => (
|
||||
<Stack key={item} mb={0.5} direction="row" alignItems="center">
|
||||
<Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} />
|
||||
<Typography>{item}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
||||
|
||||
function WarnIcon() {
|
||||
return (
|
||||
<Tooltip title="The field exists in the config but not enabled.">
|
||||
<InfoRounded color="warning" sx={{ cursor: "pointer", opacity: 0.5 }} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -13,26 +13,42 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { verge, patchVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mixedPort, setMixedPort] = useState(
|
||||
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
|
||||
);
|
||||
const [socksPort, setSocksPort] = useState(
|
||||
verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898
|
||||
);
|
||||
const [port, setPort] = useState(
|
||||
verge?.verge_mixed_port ?? clashInfo?.port ?? 7897
|
||||
verge?.verge_port ?? clashInfo?.port ?? 7899
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
if (verge?.verge_mixed_port) setPort(verge?.verge_mixed_port);
|
||||
if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port);
|
||||
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port);
|
||||
if (verge?.verge_port) setPort(verge?.verge_port);
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
if (port === verge?.verge_mixed_port) {
|
||||
if (
|
||||
mixedPort === verge?.verge_mixed_port &&
|
||||
socksPort === verge?.verge_socks_port &&
|
||||
port === verge?.verge_port
|
||||
) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await patchInfo({ "mixed-port": port });
|
||||
await patchVerge({ verge_mixed_port: port });
|
||||
await patchInfo({ "mixed-port": mixedPort });
|
||||
await patchInfo({ "socks-port": socksPort });
|
||||
await patchInfo({ port });
|
||||
await patchVerge({ verge_mixed_port: mixedPort });
|
||||
await patchVerge({ verge_socks_port: socksPort });
|
||||
await patchVerge({ verge_port: port });
|
||||
setOpen(false);
|
||||
Notice.success("Change Clash port successfully!", 1000);
|
||||
} catch (err: any) {
|
||||
@@ -54,6 +70,30 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Mixed Port" />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
sx={{ width: 135 }}
|
||||
value={mixedPort}
|
||||
onChange={(e) =>
|
||||
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Socks Port" />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
sx={{ width: 135 }}
|
||||
value={socksPort}
|
||||
onChange={(e) =>
|
||||
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Http Port" />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
|
||||
@@ -21,7 +21,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const [values, setValues] = useState({
|
||||
appLogLevel: "info",
|
||||
autoCloseConnection: true,
|
||||
enableClashFields: true,
|
||||
enableBuiltinEnhanced: true,
|
||||
proxyLayoutColumn: 6,
|
||||
defaultLatencyTest: "",
|
||||
@@ -34,7 +33,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
setValues({
|
||||
appLogLevel: verge?.app_log_level ?? "info",
|
||||
autoCloseConnection: verge?.auto_close_connection ?? true,
|
||||
enableClashFields: verge?.enable_clash_fields ?? true,
|
||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||
defaultLatencyTest: verge?.default_latency_test || "",
|
||||
@@ -49,7 +47,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
await patchVerge({
|
||||
app_log_level: values.appLogLevel,
|
||||
auto_close_connection: values.autoCloseConnection,
|
||||
enable_clash_fields: values.enableClashFields,
|
||||
enable_builtin_enhanced: values.enableBuiltinEnhanced,
|
||||
proxy_layout_column: values.proxyLayoutColumn,
|
||||
default_latency_test: values.defaultLatencyTest,
|
||||
@@ -105,17 +102,6 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Enable Clash Fields Filter")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableClashFields}
|
||||
onChange={(_, c) =>
|
||||
setValues((v) => ({ ...v, enableClashFields: c }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
||||
<Switch
|
||||
|
||||
@@ -11,15 +11,11 @@ import {
|
||||
Switch,
|
||||
TextField,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { getSystemProxy } from "@/services/cmds";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -35,14 +31,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
enable_proxy_guard,
|
||||
system_proxy_bypass,
|
||||
proxy_guard_duration,
|
||||
system_proxy_registry_mode,
|
||||
} = verge ?? {};
|
||||
|
||||
const [value, setValue] = useState({
|
||||
guard: enable_proxy_guard,
|
||||
bypass: system_proxy_bypass,
|
||||
duration: proxy_guard_duration ?? 10,
|
||||
registryMode: system_proxy_registry_mode,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -52,7 +46,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
guard: enable_proxy_guard,
|
||||
bypass: system_proxy_bypass,
|
||||
duration: proxy_guard_duration ?? 10,
|
||||
registryMode: system_proxy_registry_mode,
|
||||
});
|
||||
getSystemProxy().then((p) => setSysproxy(p));
|
||||
},
|
||||
@@ -76,9 +69,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
if (value.bypass !== system_proxy_bypass) {
|
||||
patch.system_proxy_bypass = value.bypass;
|
||||
}
|
||||
if (value.registryMode !== system_proxy_registry_mode) {
|
||||
patch.system_proxy_registry_mode = value.registryMode;
|
||||
}
|
||||
|
||||
try {
|
||||
await patchVerge(patch);
|
||||
@@ -92,7 +82,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("System Proxy Setting")}
|
||||
contentSx={{ width: 450, maxHeight: 500 }}
|
||||
contentSx={{ width: 450, maxHeight: 300 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
@@ -144,27 +134,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{OS === "windows" && (
|
||||
<Tooltip
|
||||
title={
|
||||
enabled
|
||||
? t("Please disable the system proxy")
|
||||
: t("Using the registry instead of Windows API")
|
||||
}
|
||||
>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Use Registry")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={enabled}
|
||||
checked={value.registryMode}
|
||||
onChange={(_, e) =>
|
||||
setValue((v) => ({ ...v, registryMode: e }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DialogRef, Notice } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||
import { ClashFieldViewer } from "./mods/clash-field-viewer";
|
||||
import { ClashPortViewer } from "./mods/clash-port-viewer";
|
||||
import { ControllerViewer } from "./mods/controller-viewer";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
@@ -39,11 +38,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
|
||||
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
|
||||
|
||||
const {
|
||||
enable_random_port = false,
|
||||
verge_mixed_port,
|
||||
enable_clash_fields = true,
|
||||
} = verge ?? {};
|
||||
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
|
||||
|
||||
const webRef = useRef<DialogRef>(null);
|
||||
const fieldRef = useRef<DialogRef>(null);
|
||||
@@ -70,7 +65,6 @@ const SettingClash = ({ onError }: Props) => {
|
||||
return (
|
||||
<SettingList title={t("Clash Setting")}>
|
||||
<WebUIViewer ref={webRef} />
|
||||
<ClashFieldViewer ref={fieldRef} />
|
||||
<ClashPortViewer ref={portRef} />
|
||||
<ControllerViewer ref={ctrlRef} />
|
||||
<ClashCoreViewer ref={coreRef} />
|
||||
@@ -121,7 +115,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t("Mixed Port")}
|
||||
label={t("Port Config")}
|
||||
extra={
|
||||
<Tooltip title={t("Random Port")}>
|
||||
<IconButton
|
||||
@@ -176,19 +170,6 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{enable_clash_fields && (
|
||||
<SettingItem label={t("Clash Field")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => fieldRef.current?.open()}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem
|
||||
label={t("Clash Core")}
|
||||
extra={
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Input,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
||||
import { exitApp, openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
@@ -26,8 +26,7 @@ import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { portableFlag } from "@/pages/_layout";
|
||||
|
||||
import { routers } from "@/pages/_routers";
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
}
|
||||
@@ -38,8 +37,14 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
const { theme_mode, language, tray_event, env_type, startup_script } =
|
||||
verge ?? {};
|
||||
const {
|
||||
theme_mode,
|
||||
language,
|
||||
tray_event,
|
||||
env_type,
|
||||
startup_script,
|
||||
start_page,
|
||||
} = verge ?? {};
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
const hotkeyRef = useRef<DialogRef>(null);
|
||||
const miscRef = useRef<DialogRef>(null);
|
||||
@@ -134,6 +139,23 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Start Page")}>
|
||||
<GuardState
|
||||
value={start_page ?? "/"}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ start_page: e })}
|
||||
onGuard={(e) => patchVerge({ start_page: e })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||
{routers.map((page: { label: string; link: string }) => {
|
||||
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
|
||||
})}
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Startup Script")}>
|
||||
<GuardState
|
||||
value={startup_script ?? ""}
|
||||
@@ -270,18 +292,29 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{!portableFlag && (
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={onCheckUpdate}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Exit")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => {
|
||||
exitApp();
|
||||
}}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Verge Version")}>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
|
||||
|
||||
@@ -75,7 +75,6 @@ export const TestItem = (props: Props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onDelay();
|
||||
listenTsetEvent();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useForm, Controller } from "react-hook-form";
|
||||
import { TextField } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, Notice } from "@/components/base";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface Props {
|
||||
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void;
|
||||
@@ -67,7 +68,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
let uid;
|
||||
|
||||
if (openType === "new") {
|
||||
uid = crypto.randomUUID();
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
newList = [...testList, item];
|
||||
await patchVerge({ test_list: newList });
|
||||
|
||||
@@ -48,11 +48,16 @@ export const useClashInfo = () => {
|
||||
|
||||
const patchInfo = async (
|
||||
patch: Partial<
|
||||
Pick<IConfigData, "mixed-port" | "external-controller" | "secret">
|
||||
Pick<
|
||||
IConfigData,
|
||||
"port" | "socks-port" | "mixed-port" | "external-controller" | "secret"
|
||||
>
|
||||
>
|
||||
) => {
|
||||
const hasInfo =
|
||||
patch["mixed-port"] != null ||
|
||||
patch["socks-port"] != null ||
|
||||
patch["port"] != null ||
|
||||
patch["external-controller"] != null ||
|
||||
patch.secret != null;
|
||||
|
||||
@@ -68,6 +73,26 @@ export const useClashInfo = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (patch["socks-port"]) {
|
||||
const port = patch["socks-port"];
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
}
|
||||
}
|
||||
|
||||
if (patch["port"]) {
|
||||
const port = patch["port"];
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
}
|
||||
}
|
||||
|
||||
await patchClashConfig(patch);
|
||||
mutateInfo();
|
||||
mutate("getClashConfig");
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"Filter conditions": "Filter conditions",
|
||||
"Refresh profiles": "Refresh profiles",
|
||||
"Rules": "Rules",
|
||||
"Update All": "Update All",
|
||||
"Update At": "Update At",
|
||||
|
||||
"Type": "Type",
|
||||
"Name": "Name",
|
||||
@@ -73,7 +75,7 @@
|
||||
"Allow Lan": "Allow Lan",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Log Level",
|
||||
"Mixed Port": "Mixed Port",
|
||||
"Port Config": "Port Config",
|
||||
"Random Port": "Random Port",
|
||||
"After restart to take effect": "After restart to take effect",
|
||||
"External": "External",
|
||||
@@ -91,7 +93,6 @@
|
||||
"Proxy Guard": "Proxy Guard",
|
||||
"Guard Duration": "Guard Duration",
|
||||
"Proxy Bypass": "Proxy Bypass",
|
||||
"Use Registry": "Use Registry",
|
||||
"Enable status": "Enable status",
|
||||
"Server Addr": "Server Addr",
|
||||
"Bypass": "Bypass",
|
||||
@@ -99,6 +100,7 @@
|
||||
"Theme Mode": "Theme Mode",
|
||||
"Tray Click Event": "Tray Click Event",
|
||||
"Copy Env Type": "Copy Env Type",
|
||||
"Start Page": "Start Page",
|
||||
"Startup Script": "Startup Script",
|
||||
"Browse": "Browse",
|
||||
"Show Main Window": "Show Main Window",
|
||||
@@ -126,6 +128,7 @@
|
||||
"Back": "Back",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"Exit": "Exit",
|
||||
|
||||
"Default": "Default",
|
||||
"Download Speed": "Download Speed",
|
||||
@@ -156,7 +159,5 @@
|
||||
"Retain 30 Days": "Retain 30 Days",
|
||||
"Retain 90 Days": "Retain 90 Days",
|
||||
|
||||
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
|
||||
"Please disable the system proxy": "Please disable the system proxy",
|
||||
"Using the registry instead of Windows API": "Using the registry instead of Windows API"
|
||||
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it"
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
"Filter": "Фильтр",
|
||||
"Filter conditions": "Условия фильтрации",
|
||||
"Refresh profiles": "Обновить профили",
|
||||
"Rules": "Правила",
|
||||
"Update All": "Обновить все",
|
||||
"Update At": "Обновлено в",
|
||||
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
@@ -70,7 +73,7 @@
|
||||
"Allow Lan": "Разрешить локальную сеть",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Уровень логов",
|
||||
"Mixed Port": "Смешанный порт",
|
||||
"Port Config": "Настройка порта",
|
||||
"Random Port": "Случайный порт",
|
||||
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
||||
"Clash Core": "Ядро Clash",
|
||||
@@ -85,10 +88,10 @@
|
||||
"Proxy Guard": "Защита прокси",
|
||||
"Guard Duration": "Период защиты",
|
||||
"Proxy Bypass": "Игнорирование прокси",
|
||||
"Use Registry": "Использование реестра",
|
||||
"Current System Proxy": "Текущий системный прокси",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Tray Click Event": "Событие щелчка в лотке",
|
||||
"Start Page": "Главная страница",
|
||||
"Copy Env Type": "Скопировать тип Env",
|
||||
"Startup Script": "Скрипт запуска",
|
||||
"Browse": "Просмотреть",
|
||||
@@ -113,6 +116,7 @@
|
||||
"Back": "Назад",
|
||||
"Save": "Сохранить",
|
||||
"Cancel": "Отмена",
|
||||
"Exit": "Выход",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
@@ -126,7 +130,5 @@
|
||||
"enable_tun_mode": "Включить режим туннеля",
|
||||
"disable_tun_mode": "Отключить режим туннеля",
|
||||
|
||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
|
||||
"Please disable the system proxy": "Пожалуйста, отключите системный прокси",
|
||||
"Using the registry instead of Windows API": "Использование реестра вместо Windows API"
|
||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную"
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"Filter conditions": "过滤条件",
|
||||
"Refresh profiles": "刷新订阅",
|
||||
"Rules": "规则",
|
||||
"Update All": "更新全部",
|
||||
"Update At": "更新于",
|
||||
|
||||
"Type": "类型",
|
||||
"Name": "名称",
|
||||
@@ -73,7 +75,7 @@
|
||||
"Allow Lan": "局域网连接",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "日志等级",
|
||||
"Mixed Port": "端口设置",
|
||||
"Port Config": "端口设置",
|
||||
"Random Port": "随机端口",
|
||||
"After restart to take effect": "重启后生效",
|
||||
"External": "外部控制",
|
||||
@@ -91,7 +93,6 @@
|
||||
"Proxy Guard": "系统代理守卫",
|
||||
"Guard Duration": "代理守卫间隔",
|
||||
"Proxy Bypass": "代理绕过",
|
||||
"Use Registry": "使用注册表",
|
||||
"Current System Proxy": "当前系统代理",
|
||||
"Enable status": "开启状态:",
|
||||
"Server Addr": "服务地址:",
|
||||
@@ -99,6 +100,7 @@
|
||||
"Theme Mode": "主题模式",
|
||||
"Tray Click Event": "托盘点击事件",
|
||||
"Copy Env Type": "复制环境变量类型",
|
||||
"Start Page": "启动页面",
|
||||
"Startup Script": "启动脚本",
|
||||
"Browse": "浏览",
|
||||
"Show Main Window": "显示主窗口",
|
||||
@@ -126,6 +128,7 @@
|
||||
"Back": "返回",
|
||||
"Save": "保存",
|
||||
"Cancel": "取消",
|
||||
"Exit": "退出",
|
||||
|
||||
"Default": "默认",
|
||||
"Download Speed": "下载速度",
|
||||
@@ -156,7 +159,5 @@
|
||||
"Retain 30 Days": "保留30天",
|
||||
"Retain 90 Days": "保留90天",
|
||||
|
||||
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
|
||||
"Please disable the system proxy": "请先关闭系统代理",
|
||||
"Using the registry instead of Windows API": "使用注册表替代Windows API"
|
||||
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { getPortableFlag } from "@/services/cmds";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
export let portableFlag = false;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -36,8 +36,8 @@ const Layout = () => {
|
||||
const { theme } = useCustomTheme();
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { language } = verge || {};
|
||||
|
||||
const { language, start_page } = verge || {};
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,7 +54,7 @@ const Layout = () => {
|
||||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProviders");
|
||||
mutate("getProxyProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
@@ -88,7 +88,10 @@ const Layout = () => {
|
||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||
i18next.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
if (start_page) {
|
||||
navigate(start_page);
|
||||
}
|
||||
}, [language, start_page]);
|
||||
|
||||
return (
|
||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||
|
||||
@@ -12,11 +12,6 @@ export const routers = [
|
||||
link: "/",
|
||||
ele: ProxiesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Test",
|
||||
link: "/test",
|
||||
ele: TestPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Profiles",
|
||||
link: "/profile",
|
||||
@@ -37,6 +32,11 @@ export const routers = [
|
||||
link: "/logs",
|
||||
ele: LogsPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Test",
|
||||
link: "/test",
|
||||
ele: TestPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Settings",
|
||||
link: "/settings",
|
||||
|
||||
@@ -172,6 +172,7 @@ const ConnectionsPage = () => {
|
||||
sx={{
|
||||
mr: 1,
|
||||
width: i18n.language === "en" ? 190 : 120,
|
||||
height: 33.375,
|
||||
'[role="button"]': { py: 0.65 },
|
||||
}}
|
||||
>
|
||||
@@ -193,6 +194,7 @@ const ConnectionsPage = () => {
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -80,7 +80,12 @@ const LogPage = () => {
|
||||
autoComplete="off"
|
||||
value={logState}
|
||||
onChange={(e) => setLogState(e.target.value)}
|
||||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
||||
sx={{
|
||||
width: 120,
|
||||
height: 33.375,
|
||||
mr: 1,
|
||||
'[role="button"]': { py: 0.65 },
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
|
||||
@@ -2,10 +2,11 @@ import useSWR from "swr";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Box, Paper, TextField } from "@mui/material";
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import { getRules } from "@/services/api";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import RuleItem from "@/components/rule/rule-item";
|
||||
import { ProviderButton } from "@/components/rule/provider-button";
|
||||
|
||||
const RulesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,7 +19,16 @@ const RulesPage = () => {
|
||||
}, [data, filterText]);
|
||||
|
||||
return (
|
||||
<BasePage full title={t("Rules")} contentStyle={{ height: "100%" }}>
|
||||
<BasePage
|
||||
full
|
||||
title={t("Rules")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BasePage } from "@/components/base";
|
||||
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
|
||||
import { TestItem } from "@/components/test/test-item";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const TestPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -34,19 +35,19 @@ const TestPage = () => {
|
||||
// test list
|
||||
const testList = verge?.test_list ?? [
|
||||
{
|
||||
uid: crypto.randomUUID(),
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: "https://www.apple.com/favicon.ico",
|
||||
},
|
||||
{
|
||||
uid: crypto.randomUUID(),
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: `<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#000000"/></svg>`,
|
||||
},
|
||||
{
|
||||
uid: crypto.randomUUID(),
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: `<svg enable-background="new 0 0 48 48" height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-11.045 0-20 8.955-20 20s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z" fill="#ffc107"/><path d="m6.306 14.691 6.571 4.819c1.778-4.402 6.084-7.51 11.123-7.51 3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-7.682 0-14.344 4.337-17.694 10.691z" fill="#ff3d00"/><path d="m24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238c-2.008 1.521-4.504 2.43-7.219 2.43-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025c3.31 6.477 10.032 10.921 17.805 10.921z" fill="#4caf50"/><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-.792 2.237-2.231 4.166-4.087 5.571.001-.001.002-.001.003-.002l6.19 5.238c-.438.398 6.591-4.807 6.591-14.807 0-1.341-.138-2.65-.389-3.917z" fill="#1976d2"/></svg>`,
|
||||
|
||||
@@ -105,9 +105,8 @@ export const getProxiesInner = async () => {
|
||||
export const getProxies = async () => {
|
||||
const [proxyRecord, providerRecord] = await Promise.all([
|
||||
getProxiesInner(),
|
||||
getProviders(),
|
||||
getProxyProviders(),
|
||||
]);
|
||||
|
||||
// provider name map
|
||||
const providerMap = Object.fromEntries(
|
||||
Object.entries(providerRecord).flatMap(([provider, item]) =>
|
||||
@@ -131,24 +130,27 @@ export const getProxies = async () => {
|
||||
|
||||
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
|
||||
|
||||
let groups: IProxyGroupItem[] = [];
|
||||
let groups = Object.values(proxyRecord)
|
||||
.filter((each) => each.name !== "GLOBAL" && each.all)
|
||||
.map((each) => ({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
}));
|
||||
|
||||
if (global?.all) {
|
||||
groups = global.all
|
||||
let globalGroups = global.all
|
||||
.filter((name) => proxyRecord[name]?.all)
|
||||
.map((name) => proxyRecord[name])
|
||||
.map((each) => ({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
}));
|
||||
} else {
|
||||
groups = Object.values(proxyRecord)
|
||||
.filter((each) => each.name !== "GLOBAL" && each.all)
|
||||
.map((each) => ({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
}))
|
||||
.sort((a, b) => b.name.localeCompare(a.name));
|
||||
let globalNames = globalGroups.map((each) => each.name);
|
||||
groups = groups
|
||||
.filter((group) => {
|
||||
return !globalNames.includes(group.name);
|
||||
})
|
||||
.concat(globalGroups);
|
||||
}
|
||||
|
||||
const proxies = [direct, reject].concat(
|
||||
@@ -166,11 +168,31 @@ export const getProxies = async () => {
|
||||
};
|
||||
|
||||
// get proxy providers
|
||||
export const getProviders = async () => {
|
||||
export const getProxyProviders = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
|
||||
const providers = (response.providers || {}) as Record<string, IProviderItem>;
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
IProxyProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuleProviders = async () => {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/rules");
|
||||
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
IRuleProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
@@ -188,11 +210,16 @@ export const providerHealthCheck = async (name: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const providerUpdate = async (name: string) => {
|
||||
export const proxyProviderUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const ruleProviderUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/rules/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const getConnections = async () => {
|
||||
const instance = await getAxios();
|
||||
const result = await instance.get("/connections");
|
||||
|
||||
@@ -199,3 +199,7 @@ export async function invoke_uwp_tool() {
|
||||
export async function getPortableFlag() {
|
||||
return invoke<boolean>("get_portable_flag");
|
||||
}
|
||||
|
||||
export async function exitApp() {
|
||||
return invoke("exit_app");
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class DelayManager {
|
||||
if (delay <= 0) return "Error";
|
||||
if (delay > 1e5) return "Error";
|
||||
if (delay >= 10000) return "Timeout"; // 10s
|
||||
return `${delay} ms`;
|
||||
return `${delay}`;
|
||||
}
|
||||
|
||||
formatDelayColor(delay: number) {
|
||||
|
||||
27
src/services/types.d.ts
vendored
27
src/services/types.d.ts
vendored
@@ -61,12 +61,28 @@ type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
||||
all: IProxyItem[];
|
||||
};
|
||||
|
||||
interface IProviderItem {
|
||||
interface IProxyProviderItem {
|
||||
name: string;
|
||||
type: string;
|
||||
proxies: IProxyItem[];
|
||||
updatedAt: string;
|
||||
vehicleType: string;
|
||||
subscriptionInfo?: {
|
||||
Upload: number;
|
||||
Download: number;
|
||||
Total: number;
|
||||
Expire: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IRuleProviderItem {
|
||||
name: string;
|
||||
behavior: string;
|
||||
format: string;
|
||||
ruleCount: number;
|
||||
type: string;
|
||||
updatedAt: string;
|
||||
vehicleType: string;
|
||||
}
|
||||
|
||||
interface ITrafficItem {
|
||||
@@ -115,7 +131,9 @@ interface IConnections {
|
||||
|
||||
interface IClashInfo {
|
||||
// status: string;
|
||||
port?: number; // clash mixed port
|
||||
mixed_port?: number; // clash mixed port
|
||||
socks_port?: number; // clash socks port
|
||||
port?: number; // clash http port
|
||||
server?: string; // external-controller
|
||||
secret?: string;
|
||||
}
|
||||
@@ -168,6 +186,7 @@ interface IVergeConfig {
|
||||
tray_event?: "main_window" | "system_proxy" | "tun_mode" | string;
|
||||
env_type?: "bash" | "cmd" | "powershell" | string;
|
||||
startup_script?: string;
|
||||
start_page?: string;
|
||||
clash_core?: string;
|
||||
theme_mode?: "light" | "dark" | "system";
|
||||
traffic_graph?: boolean;
|
||||
@@ -179,10 +198,11 @@ interface IVergeConfig {
|
||||
enable_system_proxy?: boolean;
|
||||
enable_random_port?: boolean;
|
||||
verge_mixed_port?: number;
|
||||
verge_socks_port?: number;
|
||||
verge_port?: number;
|
||||
enable_proxy_guard?: boolean;
|
||||
proxy_guard_duration?: number;
|
||||
system_proxy_bypass?: string;
|
||||
system_proxy_registry_mode?: boolean;
|
||||
web_ui_list?: string[];
|
||||
hotkeys?: string[];
|
||||
theme_setting?: {
|
||||
@@ -199,7 +219,6 @@ interface IVergeConfig {
|
||||
};
|
||||
auto_close_connection?: boolean;
|
||||
default_latency_test?: string;
|
||||
enable_clash_fields?: boolean;
|
||||
enable_builtin_enhanced?: boolean;
|
||||
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||
proxy_layout_column?: number;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
export const HANDLE_FIELDS = [
|
||||
"mode",
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
"allow-lan",
|
||||
"log-level",
|
||||
"ipv6",
|
||||
"secret",
|
||||
"external-controller",
|
||||
];
|
||||
|
||||
export const DEFAULT_FIELDS = [
|
||||
"proxies",
|
||||
"proxy-groups",
|
||||
"proxy-providers",
|
||||
"rules",
|
||||
"rule-providers",
|
||||
] as const;
|
||||
|
||||
export const OTHERS_FIELDS = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
"hosts",
|
||||
"script",
|
||||
"profile",
|
||||
"payload",
|
||||
"tunnels",
|
||||
"auto-redir",
|
||||
"experimental",
|
||||
"interface-name",
|
||||
"routing-mark",
|
||||
"redir-port",
|
||||
"tproxy-port",
|
||||
"iptables",
|
||||
"external-ui",
|
||||
"bind-address",
|
||||
"authentication",
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"geox-url", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
"skip-auth-prefixes", // meta
|
||||
"external-controller-tls", // meta
|
||||
"global-client-fingerprint", // meta
|
||||
] as const;
|
||||
Reference in New Issue
Block a user