Compare commits
28 Commits
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -92,7 +92,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
|
||||
release-for-linux:
|
||||
strategy:
|
||||
|
||||
24
README.md
24
README.md
@@ -12,7 +12,7 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
## Features
|
||||
|
||||
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/clash-verge-rev/clash-verge-rev/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
|
||||
- Simple UI and supports custom theme color.
|
||||
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
|
||||
- System proxy setting and guard.
|
||||
@@ -41,18 +41,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
|
||||
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.5/Clash.Verge_1.4.5_x64-setup.exe)
|
||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/Clash.Verge_1.4.5_x86-setup.exe)
|
||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/Clash.Verge_1.4.5_arm64-setup.exe)
|
||||
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/Clash.Verge_1.4.7_x64-setup.exe)
|
||||
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/Clash.Verge_1.4.7_x86-setup.exe)
|
||||
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/Clash.Verge_1.4.7_arm64-setup.exe)
|
||||
|
||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/Clash.Verge_1.4.5_x64.dmg)
|
||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/Clash.Verge_1.4.5_aarch64.dmg)
|
||||
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/Clash.Verge_1.4.7_x64.dmg)
|
||||
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/Clash.Verge_1.4.7_aarch64.dmg)
|
||||
|
||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/clash-verge_1.4.5_amd64.AppImage)
|
||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/clash-verge_1.4.5_amd64.deb)
|
||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/clash-verge_1.4.5_i386.AppImage)
|
||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/clash-verge_1.4.5_i386.deb)
|
||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.5/clash-verge_1.4.5_arm64.deb)
|
||||
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/clash-verge_1.4.7_amd64.AppImage)
|
||||
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/clash-verge_1.4.7_amd64.deb)
|
||||
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/clash-verge_1.4.7_i386.AppImage)
|
||||
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/clash-verge_1.4.7_i386.deb)
|
||||
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.7/clash-verge_1.4.7_arm64.deb)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
@@ -72,7 +72,7 @@ You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getti
|
||||
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.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||
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
|
||||
|
||||
27
UPDATELOG.md
27
UPDATELOG.md
@@ -1,3 +1,30 @@
|
||||
## v1.4.7
|
||||
|
||||
### Features
|
||||
|
||||
- Windows 便携版禁用应用内更新
|
||||
- 支持代理组 Hidden 选项
|
||||
- 支持 URL Scheme(MacOS & Linux)
|
||||
|
||||
---
|
||||
|
||||
## v1.4.6
|
||||
|
||||
### Features
|
||||
|
||||
- 更新 Clash Meta(mihomo) 内核到 v1.18.0
|
||||
- 支持 URL Scheme(暂时仅支持 Windows)
|
||||
- 添加窗口置顶按钮
|
||||
- UI 优化调整
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 修复一些编译错误
|
||||
- 获取订阅名称错误
|
||||
- 订阅信息解析错误
|
||||
|
||||
---
|
||||
|
||||
## v1.4.5
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.4.5",
|
||||
"version": "1.4.7",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
|
||||
@@ -28,13 +28,13 @@ index 4c6dde5..5fd9ad8 100644
|
||||
@@ -25,7 +25,6 @@ log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.29"
|
||||
sysinfo = "0.30"
|
||||
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.5"
|
||||
@@ -34,6 +33,7 @@ port_scanner = "0.1.5"
|
||||
delay_timer = "0.11"
|
||||
delay_timer = "0.11.5"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
+quick-rs = { path = "quick-rs" }
|
||||
@@ -123,7 +123,7 @@ index 6c207d9..d47dc33 100644
|
||||
- if result == "\"\"" {
|
||||
- anyhow::bail!("main function should return object");
|
||||
- }
|
||||
- return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||
- Ok(serde_json::from_str::<Mapping>(result.as_str())?)
|
||||
- });
|
||||
-
|
||||
- let mut out = outputs.lock().unwrap();
|
||||
|
||||
@@ -48,7 +48,7 @@ const SIDECAR_HOST = target
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
/* ======= clash meta alpha======= */
|
||||
const VERSION_URL =
|
||||
const META_ALPHA_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
|
||||
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
|
||||
let META_ALPHA_VERSION;
|
||||
@@ -65,22 +65,38 @@ const META_ALPHA_MAP = {
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
async function getLatestVersion() {
|
||||
// Fetch the latest alpha release version from the version.txt file
|
||||
async function getLatestAlphaVersion() {
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(VERSION_URL, { method: "GET" });
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
console.log(`Latest release version: ${META_ALPHA_VERSION}`);
|
||||
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest release version:", error.message);
|
||||
console.error("Error fetching latest alpha version:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======= clash meta stable ======= */
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION = "v1.17.0";
|
||||
let META_VERSION;
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||
@@ -94,6 +110,33 @@ const META_MAP = {
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
async function getLatestReleaseVersion() {
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
console.log(`Latest release version: ${META_VERSION}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest release version:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* check available
|
||||
*/
|
||||
@@ -273,8 +316,8 @@ async function downloadFile(url, path) {
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
const SERVICE_URL =
|
||||
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
|
||||
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () =>
|
||||
resolveResource({
|
||||
@@ -316,15 +359,16 @@ const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "clash-meta-alpha",
|
||||
func: () => getLatestVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
func: () =>
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
retry: 5,
|
||||
},
|
||||
{
|
||||
name: "clash-meta",
|
||||
func: () => resolveSidecar(clashMeta()),
|
||||
func: () =>
|
||||
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||
retry: 5,
|
||||
},
|
||||
// { name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
|
||||
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
||||
|
||||
303
src-tauri/Cargo.lock
generated
303
src-tauri/Cargo.lock
generated
@@ -28,19 +28,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.11",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.6.10"
|
||||
@@ -101,6 +88,16 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b"
|
||||
dependencies = [
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
@@ -238,6 +235,17 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.5"
|
||||
@@ -559,7 +567,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "1.4.5"
|
||||
version = "1.4.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto-launch",
|
||||
@@ -591,7 +599,6 @@ dependencies = [
|
||||
"warp",
|
||||
"which 5.0.0",
|
||||
"window-shadows",
|
||||
"windows-sys 0.52.0",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
@@ -931,9 +938,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "delay_timer"
|
||||
version = "0.11.4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e3040b73d9397711697558109c983a2dc6fc63e98785ffbefd3ece57b46b67"
|
||||
checksum = "d70c0d5d2addc05d1e0cf7e60b3a5bf08415f03136a773d7c66b4e4a3b892cef"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1136,6 +1143,27 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@@ -1787,7 +1815,7 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash 0.7.7",
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2141,17 +2169,6 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "iptools"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03bfb870879ce6a141b644653d63b203d290ec5f3b6919cf7b30cba06a164a5"
|
||||
dependencies = [
|
||||
"ahash 0.8.6",
|
||||
"once_cell",
|
||||
"regex 1.10.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -2452,6 +2469,19 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dirs-next",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -2525,6 +2555,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.0"
|
||||
@@ -2675,6 +2714,18 @@ dependencies = [
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
@@ -2712,6 +2763,19 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226"
|
||||
dependencies = [
|
||||
"log 0.4.20",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
@@ -2943,6 +3007,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.1.4"
|
||||
@@ -3254,7 +3328,7 @@ dependencies = [
|
||||
"base64 0.21.5",
|
||||
"indexmap 2.1.0",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"quick-xml 0.31.0",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -3381,6 +3455,15 @@ version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
@@ -4307,6 +4390,12 @@ dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.7"
|
||||
@@ -4363,9 +4452,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.29.11"
|
||||
version = "0.30.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666"
|
||||
checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
@@ -4373,18 +4462,18 @@ dependencies = [
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"winapi",
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysproxy"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/zzzgydi/sysproxy-rs?branch=main#f8fab6a542c41cf3b973f4fe77d399756e3efff1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9707a79d3b95683aa5a9521e698ffd878b8fb289727c25a69157fb85d529ffff"
|
||||
dependencies = [
|
||||
"interfaces",
|
||||
"iptools",
|
||||
"thiserror",
|
||||
"windows 0.52.0",
|
||||
"winapi",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
@@ -4535,6 +4624,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"infer 0.9.0",
|
||||
"minisign-verify",
|
||||
"notify-rust",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"open 3.2.0",
|
||||
@@ -4710,6 +4800,16 @@ dependencies = [
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2"
|
||||
dependencies = [
|
||||
"quick-xml 0.30.0",
|
||||
"windows 0.51.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.1"
|
||||
@@ -5171,6 +5271,17 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset 0.9.0",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
@@ -5678,6 +5789,16 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.51.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
|
||||
dependencies = [
|
||||
"windows-core 0.51.1",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
@@ -6112,6 +6233,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd"
|
||||
dependencies = [
|
||||
"nix 0.26.4",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
@@ -6122,23 +6253,69 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.28"
|
||||
name = "zbus"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e"
|
||||
checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io 1.13.0",
|
||||
"async-lock 2.8.0",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"byteorder",
|
||||
"derivative",
|
||||
"enumflags2",
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix 0.26.4",
|
||||
"once_cell",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"winapi",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.28"
|
||||
name = "zbus_macros"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b"
|
||||
checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
"regex 1.10.2",
|
||||
"syn 1.0.109",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6151,3 +6328,41 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "3.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"enumflags2",
|
||||
"libc",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "3.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "1.4.5"
|
||||
version = "1.4.7"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0"
|
||||
@@ -14,6 +14,7 @@ tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
sysproxy = "0.3.0"
|
||||
which = "5.0.0"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
@@ -24,28 +25,26 @@ dunce = "1.0"
|
||||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.29"
|
||||
sysinfo = "0.30"
|
||||
rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.5"
|
||||
once_cell = "1.18"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11"
|
||||
delay_timer = "0.11.5"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
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/zzzgydi/sysproxy-rs", branch = "main" }
|
||||
tauri = { version = "1.5", features = ["icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
tauri = { version = "1.5", features = [ "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.0.0" # 高版本会返回错误 Status
|
||||
deelevate = "0.2.0"
|
||||
winreg = { version = "0.52", features = ["transactions"] }
|
||||
windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||
winreg = "0.52.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
#openssl
|
||||
|
||||
17
src-tauri/Info.plist
Normal file
17
src-tauri/Info.plist
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>io.github.clash-verge-rev.clash-verge-rev</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>clash</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
9
src-tauri/clash-verge.desktop
Normal file
9
src-tauri/clash-verge.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Categories={{{categories}}}
|
||||
Comment={{{comment}}}
|
||||
Exec={{{exec}}} %u
|
||||
Icon={{{icon}}}
|
||||
Name={{{name}}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/clash;
|
||||
@@ -219,7 +219,7 @@ pub fn open_app_dir() -> CmdResult<()> {
|
||||
#[tauri::command]
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
@@ -252,10 +252,15 @@ pub async fn clash_api_get_proxy_delay(
|
||||
) -> CmdResult<clash_api::DelayRes> {
|
||||
match clash_api::get_proxy_delay(name, url).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(format!("{}", err.to_string())),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
||||
@@ -65,8 +65,8 @@ impl IClashTemp {
|
||||
let config = &self.0;
|
||||
|
||||
ClashInfo {
|
||||
port: Self::guard_mixed_port(&config),
|
||||
server: Self::guard_client_ctrl(&config),
|
||||
port: Self::guard_mixed_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()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||
@@ -98,7 +98,7 @@ impl IClashTemp {
|
||||
Some(val_str) => {
|
||||
let val_str = val_str.trim();
|
||||
|
||||
let val = match val_str.starts_with(":") {
|
||||
let val = match val_str.starts_with(':') {
|
||||
true => format!("127.0.0.1{val_str}"),
|
||||
false => val_str.to_owned(),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::utils::{dirs, help, tmpl};
|
||||
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,7 +8,7 @@ use sysproxy::Sysproxy;
|
||||
|
||||
use super::Config;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct PrfItem {
|
||||
pub uid: Option<String>,
|
||||
|
||||
@@ -96,25 +96,7 @@ impl PrfOption {
|
||||
a.update_interval = b.update_interval.or(a.update_interval);
|
||||
Some(a)
|
||||
}
|
||||
t @ _ => t.0.or(t.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrfItem {
|
||||
fn default() -> Self {
|
||||
PrfItem {
|
||||
uid: None,
|
||||
itype: None,
|
||||
name: None,
|
||||
desc: None,
|
||||
file: None,
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
updated: None,
|
||||
option: None,
|
||||
file_data: None,
|
||||
t => t.0.or(t.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +134,7 @@ impl PrfItem {
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_script(name, desc)
|
||||
}
|
||||
typ @ _ => bail!("invalid profile item type \"{typ}\""),
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +170,7 @@ impl PrfItem {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
@@ -213,25 +195,26 @@ impl PrfItem {
|
||||
}
|
||||
// 使用系统代理
|
||||
else if with_proxy {
|
||||
match Sysproxy::get_system_proxy() {
|
||||
Ok(p @ Sysproxy { enable: true, .. }) => {
|
||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||
let proxy_scheme = format!("http://{}:{}", p.host, p.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 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 version = format!("clash-verge-rev");
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => format!("clash-verge/v{}", v),
|
||||
None => "clash-verge/unknown".to_string(),
|
||||
};
|
||||
|
||||
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||
|
||||
let resp = builder.build()?.get(url).send().await?;
|
||||
@@ -247,12 +230,11 @@ impl PrfItem {
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
let sub_info = value.to_str().unwrap_or("");
|
||||
|
||||
Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
||||
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
@@ -261,14 +243,18 @@ impl PrfItem {
|
||||
// parse the Content-Disposition
|
||||
let filename = match header.get("Content-Disposition") {
|
||||
Some(value) => {
|
||||
let filename = value.to_str().unwrap_or("");
|
||||
match help::parse_str::<String>(filename, "filename=") {
|
||||
Some(filename) => Some(filename),
|
||||
None => match help::parse_str::<String>(filename, "filename*=") {
|
||||
let filename = format!("{value:?}");
|
||||
let filename = filename.trim_matches('"');
|
||||
match help::parse_str::<String>(filename, "filename*") {
|
||||
Some(filename) => {
|
||||
let iter = percent_encoding::percent_decode(filename.as_bytes());
|
||||
let filename = iter.decode_utf8().unwrap_or_default();
|
||||
filename.split("''").last().map(|s| s.to_string())
|
||||
}
|
||||
None => match help::parse_str::<String>(filename, "filename") {
|
||||
Some(filename) => {
|
||||
let iter = percent_encoding::percent_decode(filename.as_bytes());
|
||||
let filename = iter.decode_utf8().unwrap_or_default();
|
||||
filename.split("''").last().map(|s| s.to_string())
|
||||
let filename = filename.trim_matches('"');
|
||||
Some(filename.to_string())
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
|
||||
@@ -37,13 +37,13 @@ impl IProfiles {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
// compatible with the old old old version
|
||||
profiles.items.as_mut().map(|items| {
|
||||
if let Some(items) = profiles.items.as_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -152,17 +152,19 @@ impl IProfiles {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
self.items.as_mut().map(|items| items.push(item));
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
items.push(item)
|
||||
}
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// reorder items
|
||||
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut old_index = None;
|
||||
let mut new_index = None;
|
||||
|
||||
for i in 0..items.len() {
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == Some(active_id.clone()) {
|
||||
old_index = Some(i);
|
||||
}
|
||||
@@ -182,7 +184,7 @@ impl IProfiles {
|
||||
|
||||
/// update the item value
|
||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
@@ -255,11 +257,11 @@ impl IProfiles {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
let current = current.clone();
|
||||
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut index = None;
|
||||
|
||||
// get the index
|
||||
for i in 0..items.len() {
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == Some(uid.clone()) {
|
||||
index = Some(i);
|
||||
break;
|
||||
@@ -267,19 +269,19 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
if let Some(index) = index {
|
||||
items.remove(index).file.map(|file| {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// delete the original uid
|
||||
if current == uid {
|
||||
self.current = match items.len() > 0 {
|
||||
self.current = match !items.is_empty() {
|
||||
true => items[0].uid.clone(),
|
||||
false => None,
|
||||
};
|
||||
@@ -299,7 +301,7 @@ impl IProfiles {
|
||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||
None => bail!("failed to get the file field"),
|
||||
};
|
||||
return Ok(help::read_merge_mapping(&file_path)?);
|
||||
return help::read_merge_mapping(&file_path);
|
||||
}
|
||||
bail!("failed to find the current profile \"uid:{current}\"");
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn put_configs(path: &str) -> Result<()> {
|
||||
|
||||
match response.status().as_u16() {
|
||||
204 => Ok(()),
|
||||
status @ _ => {
|
||||
status => {
|
||||
bail!("failed to put configs with status \"{status}\"")
|
||||
}
|
||||
}
|
||||
@@ -83,12 +83,12 @@ fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||
/// 缩短clash的日志
|
||||
pub fn parse_log(log: String) -> String {
|
||||
if log.starts_with("time=") && log.len() > 33 {
|
||||
return (&log[33..]).to_owned();
|
||||
return (log[33..]).to_owned();
|
||||
}
|
||||
if log.len() > 9 {
|
||||
return (&log[9..]).to_owned();
|
||||
return (log[9..]).to_owned();
|
||||
}
|
||||
return log;
|
||||
log
|
||||
}
|
||||
|
||||
/// 缩短clash -t的错误输出
|
||||
@@ -105,7 +105,7 @@ pub fn parse_check_output(log: String) -> String {
|
||||
};
|
||||
|
||||
if mr > m {
|
||||
return (&log[e..mr]).to_owned();
|
||||
return (log[e..mr]).to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub fn parse_check_output(log: String) -> String {
|
||||
let r = log.find("path=").or(Some(log.len()));
|
||||
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
return (&log[(l + 6)..(r - 1)]).to_owned();
|
||||
return (log[(l + 6)..(r - 1)]).to_owned();
|
||||
}
|
||||
|
||||
log
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{bail, Context, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{fs, io::Write, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use sysinfo::{Pid, System};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -35,12 +35,12 @@ impl CoreManager {
|
||||
.map(|pid| {
|
||||
let mut system = System::new();
|
||||
system.refresh_all();
|
||||
system.process(Pid::from_u32(pid)).map(|proc| {
|
||||
if let Some(proc) = system.process(Pid::from_u32(pid)) {
|
||||
if proc.name().contains("clash") {
|
||||
log::debug!(target: "app", "kill old clash process");
|
||||
proc.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async {
|
||||
@@ -68,7 +68,7 @@ impl CoreManager {
|
||||
|
||||
if !output.status.success() {
|
||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||
let error = match error.len() > 0 {
|
||||
let error = match !error.is_empty() {
|
||||
true => error,
|
||||
false => output.stdout.clone(),
|
||||
};
|
||||
@@ -110,7 +110,7 @@ impl CoreManager {
|
||||
use super::win_service;
|
||||
|
||||
// 服务模式
|
||||
let enable = { Config::verge().latest().enable_service_mode.clone() };
|
||||
let enable = { Config::verge().latest().enable_service_mode };
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
*self.use_service_mode.lock() = enable;
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Handle {
|
||||
self.app_handle
|
||||
.lock()
|
||||
.as_ref()
|
||||
.map_or(None, |a| a.get_window("main"))
|
||||
.and_then(|a| a.get_window("main"))
|
||||
}
|
||||
|
||||
pub fn refresh_clash() {
|
||||
|
||||
@@ -65,17 +65,17 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_dashboard" => || feat::open_dashboard(),
|
||||
"open_dashboard" => feat::open_dashboard,
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"clash_mode_script" => || feat::change_clash_mode("script".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"enable_system_proxy" => || feat::enable_system_proxy(),
|
||||
"disable_system_proxy" => || feat::disable_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
||||
"enable_tun_mode" => || feat::enable_tun_mode(),
|
||||
"disable_tun_mode" => || feat::disable_tun_mode(),
|
||||
"toggle_system_proxy" => feat::toggle_system_proxy,
|
||||
"enable_system_proxy" => feat::enable_system_proxy,
|
||||
"disable_system_proxy" => feat::disable_system_proxy,
|
||||
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||
"enable_tun_mode" => feat::enable_tun_mode,
|
||||
"disable_tun_mode" => feat::disable_tun_mode,
|
||||
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
};
|
||||
@@ -86,7 +86,7 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
self.get_manager()?.unregister(&hotkey)?;
|
||||
self.get_manager()?.unregister(hotkey)?;
|
||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -110,7 +110,7 @@ impl Hotkey {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
|
||||
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
hotkeys.iter().for_each(|hotkey| {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Sysopt {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
@@ -66,7 +66,7 @@ impl Sysopt {
|
||||
};
|
||||
|
||||
if enable {
|
||||
let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
|
||||
let old = Sysproxy::get_system_proxy().ok();
|
||||
current.set_system_proxy()?;
|
||||
|
||||
*self.old_sysproxy.lock() = old;
|
||||
@@ -93,7 +93,7 @@ impl Sysopt {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
@@ -142,7 +142,7 @@ impl Sysopt {
|
||||
|
||||
/// init the auto launch
|
||||
pub fn init_launch(&self) -> Result<()> {
|
||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
||||
let enable = { Config::verge().latest().enable_auto_launch };
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
@@ -233,7 +233,7 @@ impl Sysopt {
|
||||
drop(auto_launch);
|
||||
return self.init_launch();
|
||||
}
|
||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
||||
let enable = { Config::verge().latest().enable_auto_launch };
|
||||
let enable = enable.unwrap_or(false);
|
||||
let auto_launch = auto_launch.as_ref().unwrap();
|
||||
|
||||
@@ -271,9 +271,9 @@ impl Sysopt {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.enable_proxy_guard.clone().unwrap_or(false),
|
||||
verge.proxy_guard_duration.clone().unwrap_or(10),
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
verge.proxy_guard_duration.unwrap_or(10),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Timer {
|
||||
let timer_map = self.timer_map.lock();
|
||||
let delay_timer = self.delay_timer.lock();
|
||||
|
||||
Config::profiles().latest().get_items().map(|items| {
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
@@ -61,7 +61,7 @@ impl Timer {
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,9 +114,9 @@ pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
config.get(&key).map(|value| {
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if !enable_filter {
|
||||
@@ -134,9 +134,9 @@ pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
config.get(&key).map(|value| {
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ pub fn use_keys(config: &Mapping) -> Vec<String> {
|
||||
.map(|s| {
|
||||
let mut s = s.to_string();
|
||||
s.make_ascii_lowercase();
|
||||
return s;
|
||||
s
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ mod merge;
|
||||
mod script;
|
||||
mod tun;
|
||||
|
||||
pub(self) use self::field::*;
|
||||
|
||||
use self::chain::*;
|
||||
use self::field::*;
|
||||
use self::merge::*;
|
||||
use self::script::*;
|
||||
use self::tun::*;
|
||||
@@ -28,9 +27,9 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
verge.enable_tun_mode.clone().unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
||||
verge.enable_clash_fields.clone().unwrap_or(true),
|
||||
verge.enable_tun_mode.unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.enable_clash_fields.unwrap_or(true),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -39,18 +38,18 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
|
||||
let current = profiles.current_mapping().unwrap_or(Mapping::new());
|
||||
let current = profiles.current_mapping().unwrap_or_default();
|
||||
|
||||
let chain = match profiles.chain.as_ref() {
|
||||
Some(chain) => chain
|
||||
.iter()
|
||||
.filter_map(|uid| profiles.get_item(uid).ok())
|
||||
.filter_map(|item| <Option<ChainItem>>::from(item))
|
||||
.filter_map(<Option<ChainItem>>::from)
|
||||
.collect::<Vec<ChainItem>>(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let valid = profiles.valid.clone().unwrap_or(vec![]);
|
||||
let valid = profiles.valid.clone().unwrap_or_default();
|
||||
|
||||
(current, chain, valid)
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
|
||||
if result == "\"\"" {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||
Ok(serde_json::from_str::<Mapping>(result.as_str())?)
|
||||
});
|
||||
|
||||
let mut out = outputs.lock().unwrap();
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
|
||||
// 切换系统代理
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy.clone();
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -110,7 +110,7 @@ pub fn disable_system_proxy() {
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let enable = Config::verge().data().enable_tun_mode.clone();
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -164,14 +164,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
let mixed_port = patch.get("mixed-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.clone().unwrap()
|
||||
let changed = mixed_port.unwrap()
|
||||
!= Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
// 检查端口占用
|
||||
if changed {
|
||||
if let Some(port) = mixed_port.clone().unwrap().as_u64() {
|
||||
if let Some(port) = mixed_port.unwrap().as_u64() {
|
||||
if !port_scanner::local_port_available(port as u16) {
|
||||
Config::clash().discard();
|
||||
bail!("port already in use");
|
||||
|
||||
@@ -25,7 +25,10 @@ fn main() -> std::io::Result<()> {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.system_tray(SystemTray::new())
|
||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
||||
.setup(|app| {
|
||||
resolve::resolve_setup(app);
|
||||
Ok(())
|
||||
})
|
||||
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
@@ -34,6 +37,7 @@ fn main() -> std::io::Result<()> {
|
||||
cmds::open_logs_dir,
|
||||
cmds::open_web_url,
|
||||
cmds::open_core_dir,
|
||||
cmds::get_portable_flag,
|
||||
// cmds::kill_sidecar,
|
||||
cmds::restart_sidecar,
|
||||
cmds::grant_permission,
|
||||
@@ -132,13 +136,13 @@ fn main() -> std::io::Result<()> {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, false);
|
||||
let _ = resolve::save_window_size_position(app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::core::handle;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{
|
||||
api::path::{data_dir, resource_dir},
|
||||
@@ -7,16 +8,18 @@ use tauri::{
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||
#[cfg(feature = "verge-dev")]
|
||||
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||
|
||||
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
|
||||
static CLASH_CONFIG: &str = "config.yaml";
|
||||
static VERGE_CONFIG: &str = "verge.yaml";
|
||||
static PROFILE_YAML: &str = "profiles.yaml";
|
||||
|
||||
/// get the verge app home dir
|
||||
pub fn app_home_dir() -> Result<PathBuf> {
|
||||
/// init portable flag
|
||||
pub fn init_portable_flag() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
@@ -24,13 +27,27 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||
|
||||
if dir.exists() {
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
||||
PORTABLE_FLAG.get_or_init(|| true);
|
||||
}
|
||||
}
|
||||
PORTABLE_FLAG.get_or_init(|| false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get the verge app home dir
|
||||
pub fn app_home_dir() -> Result<PathBuf> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let flag = PORTABLE_FLAG.get().unwrap_or(&false);
|
||||
if *flag {
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
||||
}
|
||||
|
||||
Ok(data_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||
.join(APP_ID))
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(&path)
|
||||
let yaml_str = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||
@@ -71,15 +71,12 @@ pub fn get_uid(prefix: &str) -> String {
|
||||
/// parse the string
|
||||
/// xxx=123123; => 123123
|
||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
target.find(key).and_then(|idx| {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
|
||||
match value.split(';').nth(0) {
|
||||
Some(value) => value.trim().parse(),
|
||||
None => value.trim().parse(),
|
||||
target.split(';').map(str::trim).find_map(|s| {
|
||||
let mut parts = s.splitn(2, '=');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,11 +89,11 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
let code = "code";
|
||||
|
||||
let _ = match Program::from_str(code) {
|
||||
Ok(code) => open(&app.shell_scope(), &path.to_string_lossy(), Some(code)),
|
||||
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Can't find VScode `{err}`");
|
||||
// default open
|
||||
open(&app.shell_scope(), &path.to_string_lossy(), None)
|
||||
open(&app.shell_scope(), path.to_string_lossy(), None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,17 +159,17 @@ fn test_parse_value() {
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
||||
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111);
|
||||
assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222);
|
||||
assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
||||
parse_str::<String>(test_2, "filename").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "aaa"), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload1"), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire1"), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn delete_log() -> Result<()> {
|
||||
let auto_log_clean = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.data();
|
||||
verge.auto_log_clean.clone().unwrap_or(0)
|
||||
verge.auto_log_clean.unwrap_or(0)
|
||||
};
|
||||
|
||||
let day = match auto_log_clean {
|
||||
@@ -130,10 +130,8 @@ pub fn delete_log() -> Result<()> {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for file in fs::read_dir(&log_dir)? {
|
||||
if let Ok(file) = file {
|
||||
let _ = process_file(file);
|
||||
}
|
||||
for file in fs::read_dir(&log_dir)?.flatten() {
|
||||
let _ = process_file(file);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -141,6 +139,7 @@ pub fn delete_log() -> Result<()> {
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
let _ = dirs::init_portable_flag();
|
||||
let _ = init_log();
|
||||
let _ = delete_log();
|
||||
|
||||
@@ -299,3 +298,45 @@ pub fn init_service() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize url scheme
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_exe = app_exe.to_string_lossy().into_owned();
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
|
||||
clash.set_value("", &"Clash Verge")?;
|
||||
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
|
||||
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
|
||||
default_icon.set_value("", &app_exe)?;
|
||||
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
|
||||
command.set_value("", &format!("{app_exe} \"%1\""))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
let output = std::process::Command::new("xdg-mime")
|
||||
.arg("default")
|
||||
.arg("clash-verge.desktop")
|
||||
.arg("x-scheme-handler/clash")
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"failed to set clash scheme, {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::config::IVerge;
|
||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
||||
use crate::config::{IVerge, PrfOption};
|
||||
use crate::{
|
||||
config::{Config, PrfItem},
|
||||
core::*,
|
||||
utils::init,
|
||||
utils::server,
|
||||
};
|
||||
use crate::{log_err, trace_err};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde_yaml::Mapping;
|
||||
use std::net::TcpListener;
|
||||
use tauri::api::notification;
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
|
||||
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||
|
||||
pub fn find_unused_port() -> Result<u16> {
|
||||
match TcpListener::bind("127.0.0.1:0") {
|
||||
Ok(listener) => {
|
||||
@@ -27,12 +36,15 @@ pub fn find_unused_port() -> Result<u16> {
|
||||
pub fn resolve_setup(app: &mut App) {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let version = app.package_info().version.to_string();
|
||||
handle::Handle::global().init(app.app_handle());
|
||||
VERSION.get_or_init(|| version.clone());
|
||||
|
||||
log_err!(init::init_resources());
|
||||
#[cfg(target_os = "windows")]
|
||||
log_err!(init::init_service());
|
||||
log_err!(init::init_scheme());
|
||||
|
||||
// 处理随机端口
|
||||
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||
|
||||
@@ -74,7 +86,7 @@ pub fn resolve_setup(app: &mut App) {
|
||||
log::trace!("init system tray");
|
||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||
|
||||
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
||||
let silent_start = { Config::verge().data().enable_silent_start };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window(&app.app_handle());
|
||||
}
|
||||
@@ -85,6 +97,13 @@ pub fn resolve_setup(app: &mut App) {
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||
log_err!(timer::Timer::global().init());
|
||||
|
||||
let argvs: Vec<String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
tauri::async_runtime::block_on(async {
|
||||
resolve_scheme(argvs[1].to_owned()).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy
|
||||
@@ -219,3 +238,31 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) {
|
||||
let url = param
|
||||
.trim_start_matches("clash://install-config/?url=")
|
||||
.trim_start_matches("clash://install-config?url=");
|
||||
let option = PrfOption {
|
||||
user_agent: None,
|
||||
with_proxy: Some(true),
|
||||
self_proxy: None,
|
||||
update_interval: None,
|
||||
};
|
||||
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
|
||||
if Config::profiles().data().append_item(item).is_ok() {
|
||||
notification::Notification::new(crate::utils::dirs::APP_ID)
|
||||
.title("Clash Verge")
|
||||
.body("Import profile success")
|
||||
.show()
|
||||
.unwrap();
|
||||
};
|
||||
} else {
|
||||
notification::Notification::new(crate::utils::dirs::APP_ID)
|
||||
.title("Clash Verge")
|
||||
.body("Import profile failed")
|
||||
.show()
|
||||
.unwrap();
|
||||
log::error!("failed to parse url: {}", url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,42 @@ use super::resolve;
|
||||
use crate::config::IVerge;
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use std::convert::Infallible;
|
||||
use tauri::AppHandle;
|
||||
use warp::Filter;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct QueryParam {
|
||||
param: String,
|
||||
}
|
||||
|
||||
/// check whether there is already exists
|
||||
pub fn check_singleton() -> Result<()> {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
if !local_port_available(port) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let url = format!("http://127.0.0.1:{port}/commands/visible");
|
||||
let resp = reqwest::get(url).await?.text().await?;
|
||||
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
if &resp == "ok" {
|
||||
let argvs: Vec<String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
let param = argvs[1].as_str();
|
||||
reqwest::get(format!(
|
||||
"http://127.0.0.1:{port}/commands/scheme?param={param}"
|
||||
))
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
} else {
|
||||
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
}
|
||||
bail!("app exists");
|
||||
}
|
||||
|
||||
@@ -34,11 +57,22 @@ pub fn embed_server(app_handle: AppHandle) {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let commands = warp::path!("commands" / "visible").map(move || {
|
||||
let ping = warp::path!("commands" / "ping").map(move || "ok");
|
||||
|
||||
let visible = warp::path!("commands" / "visible").map(move || {
|
||||
resolve::create_window(&app_handle);
|
||||
format!("ok")
|
||||
"ok"
|
||||
});
|
||||
|
||||
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
|
||||
let scheme = warp::path!("commands" / "scheme")
|
||||
.and(warp::query::<QueryParam>())
|
||||
.and_then(scheme_handler);
|
||||
|
||||
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
|
||||
resolve::resolve_scheme(query.param).await;
|
||||
Ok("ok")
|
||||
}
|
||||
let commands = ping.or(visible).or(scheme);
|
||||
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
///! Some config file template
|
||||
//! Some config file template
|
||||
|
||||
/// template for new a profile item
|
||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.4.5"
|
||||
"version": "1.4.7"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -51,6 +51,9 @@
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [],
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"bundle": {
|
||||
"targets": ["deb", "appimage", "updater"],
|
||||
"deb": {
|
||||
"depends": ["openssl"]
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./clash-verge.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
|
||||
.the-bar {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 8px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -27,15 +27,25 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 5px 5px;
|
||||
padding: 10px 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
.base-content {
|
||||
width: 100%;
|
||||
// max-width: 850px;
|
||||
width: calc(100% - 10px * 2);
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-padding {
|
||||
> section {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
|
||||
.base-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ interface Props {
|
||||
header?: React.ReactNode; // something behind title
|
||||
contentStyle?: React.CSSProperties;
|
||||
children?: ReactNode;
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<Props> = (props) => {
|
||||
const { title, header, contentStyle, children } = props;
|
||||
const { title, header, contentStyle, full, children } = props;
|
||||
const { theme } = useCustomTheme();
|
||||
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
@@ -28,7 +29,7 @@ export const BasePage: React.FC<Props> = (props) => {
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="base-container"
|
||||
className={full ? "base-container no-padding" : "base-container"}
|
||||
style={{ backgroundColor: isDark ? "#090909" : "#ffffff" }}
|
||||
>
|
||||
<section
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Button, ButtonGroup } from "@mui/material";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
CloseRounded,
|
||||
CropSquareRounded,
|
||||
FilterNoneRounded,
|
||||
HorizontalRuleRounded,
|
||||
PushPinOutlined,
|
||||
PushPinRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -12,12 +14,37 @@ export const LayoutControl = () => {
|
||||
const minWidth = 40;
|
||||
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const [isPined, setIsPined] = useState(false);
|
||||
appWindow.isMaximized().then((isMaximized) => {
|
||||
setIsMaximized(() => isMaximized);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup
|
||||
variant="text"
|
||||
sx={{
|
||||
height: "100%",
|
||||
".MuiButtonGroup-grouped": {
|
||||
borderRadius: "0px",
|
||||
borderRight: "0px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
|
||||
onClick={() => {
|
||||
appWindow.setAlwaysOnTop(!isPined);
|
||||
setIsPined((isPined) => !isPined);
|
||||
}}
|
||||
>
|
||||
{isPined ? (
|
||||
<PushPinRounded fontSize="small" />
|
||||
) : (
|
||||
<PushPinOutlined fontSize="small" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ minWidth, svg: { transform: "scale(0.9)" } }}
|
||||
@@ -48,11 +75,15 @@ export const LayoutControl = () => {
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ minWidth, svg: { transform: "scale(1.05)" } }}
|
||||
sx={{
|
||||
minWidth,
|
||||
svg: { transform: "scale(1.05)" },
|
||||
":hover": { bgcolor: "#ff000090" },
|
||||
}}
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<CloseRounded fontSize="small" />
|
||||
</Button>
|
||||
</>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -209,7 +209,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
placeholder={`clash-verge-rev`}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
label="User Agent"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -98,6 +98,8 @@ export const ProxyItemMini = (props: Props) => {
|
||||
)}
|
||||
<TypeBox component="span">{proxy.type}</TypeBox>
|
||||
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
{proxy.xudp && <TypeBox component="span">XUDP</TypeBox>}
|
||||
{proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -104,6 +104,10 @@ export const ProxyItem = (props: Props) => {
|
||||
)}
|
||||
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
{showType && proxy.xudp && (
|
||||
<TypeBox component="span">XUDP</TypeBox>
|
||||
)}
|
||||
{showType && proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
props;
|
||||
const { type, group, headState, proxy, proxyCol } = item;
|
||||
|
||||
if (type === 0) {
|
||||
if (type === 0 && !group.hidden) {
|
||||
return (
|
||||
<ListItemButton
|
||||
dense
|
||||
@@ -61,7 +61,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 1) {
|
||||
if (type === 1 && !group.hidden) {
|
||||
return (
|
||||
<ProxyHead
|
||||
sx={{ pl: indent ? 4.5 : 2.5, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
|
||||
@@ -74,7 +74,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 2) {
|
||||
if (type === 2 && !group.hidden) {
|
||||
return (
|
||||
<ProxyItem
|
||||
groupName={group.name}
|
||||
@@ -87,7 +87,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 3) {
|
||||
if (type === 3 && !group.hidden) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -105,7 +105,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 4) {
|
||||
if (type === 4 && !group.hidden) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -168,6 +168,6 @@ const FlexBox = styled("div")`
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
width: 80px;
|
||||
width: 85px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { atomUpdateState } from "@/services/states";
|
||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { portableFlag } from "@/pages/_layout";
|
||||
|
||||
const UpdateLog = styled(Box)(() => ({
|
||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||
@@ -46,6 +47,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
}, [updateInfo]);
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
if (portableFlag) {
|
||||
Notice.error(t("Portable Updater Error"));
|
||||
return;
|
||||
}
|
||||
if (updateState) return;
|
||||
setUpdateState(true);
|
||||
if (eventListener !== null) {
|
||||
|
||||
@@ -18,6 +18,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";
|
||||
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
@@ -213,7 +214,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
||||
{!portableFlag && (
|
||||
<SettingItem label={t("Check for Updates")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
||||
@@ -145,5 +145,7 @@
|
||||
"Never Clean": "Never Clean",
|
||||
"Retain 7 Days": "Retain 7 Days",
|
||||
"Retain 30 Days": "Retain 30 Days",
|
||||
"Retain 90 Days": "Retain 90 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"
|
||||
}
|
||||
|
||||
@@ -115,5 +115,7 @@
|
||||
"disable_system_proxy": "Отключить системный прокси",
|
||||
"toggle_tun_mode": "Переключить режим туннеля",
|
||||
"enable_tun_mode": "Включить режим туннеля",
|
||||
"disable_tun_mode": "Отключить режим туннеля"
|
||||
"disable_tun_mode": "Отключить режим туннеля",
|
||||
|
||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную"
|
||||
}
|
||||
|
||||
@@ -145,5 +145,7 @@
|
||||
"Never Clean": "不清理",
|
||||
"Retain 7 Days": "保留7天",
|
||||
"Retain 30 Days": "保留30天",
|
||||
"Retain 90 Days": "保留90天"
|
||||
"Retain 90 Days": "保留90天",
|
||||
|
||||
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { getPortableFlag } from "@/services/cmds";
|
||||
|
||||
export let portableFlag = false;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -71,10 +74,12 @@ const Layout = () => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
void appWindow.unminimize();
|
||||
void appWindow.show();
|
||||
void appWindow.setFocus();
|
||||
|
||||
setTimeout(async () => {
|
||||
portableFlag = await getPortableFlag();
|
||||
await appWindow.unminimize();
|
||||
await appWindow.show();
|
||||
await appWindow.setFocus();
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
@@ -119,9 +124,7 @@ const Layout = () => {
|
||||
<div className="the-logo" data-windrag>
|
||||
<LogoSvg />
|
||||
|
||||
{!(OS === "windows" && WIN_PORTABLE) && (
|
||||
<UpdateButton className="the-newbtn" />
|
||||
)}
|
||||
{!portableFlag && <UpdateButton className="the-newbtn" />}
|
||||
</div>
|
||||
|
||||
<List className="the-menu">
|
||||
|
||||
@@ -114,6 +114,7 @@ const ConnectionsPage = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Connections")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
@@ -142,75 +143,72 @@ const ConnectionsPage = () => {
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box sx={{ boxShadow: 0, height: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "12px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
userSelect: "text",
|
||||
}}
|
||||
>
|
||||
{!isTableLayout && (
|
||||
<Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
value={curOrderOpt}
|
||||
onChange={(e) => setOrderOpt(e.target.value)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
width: i18n.language === "en" ? 190 : 120,
|
||||
'[role="button"]': { py: 0.65 },
|
||||
}}
|
||||
>
|
||||
{Object.keys(orderOpts).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "10px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
userSelect: "text",
|
||||
}}
|
||||
>
|
||||
{!isTableLayout && (
|
||||
<Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
value={curOrderOpt}
|
||||
onChange={(e) => setOrderOpt(e.target.value)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
width: i18n.language === "en" ? 190 : 120,
|
||||
'[role="button"]': { py: 0.65 },
|
||||
}}
|
||||
>
|
||||
{Object.keys(orderOpts).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Box height="calc(100% - 50px)" sx={{ userSelect: "text" }}>
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)" sx={{ userSelect: "text" }}>
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ const LogPage = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
@@ -66,61 +67,52 @@ const LogPage = () => {
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
boxShadow: 0,
|
||||
height: "100%",
|
||||
userSelect: "text",
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "10px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "12px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
<Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
value={logState}
|
||||
onChange={(e) => setLogState(e.target.value)}
|
||||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
||||
>
|
||||
<Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
value={logState}
|
||||
onChange={(e) => setLogState(e.target.value)}
|
||||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="err">ERROR</MenuItem>
|
||||
</Select>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="err">ERROR</MenuItem>
|
||||
</Select>
|
||||
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)">
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
itemContent={(index, item) => <LogItem value={item} />}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)">
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
itemContent={(index, item) => <LogItem value={item} />}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty text="No Logs" />
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<BaseEmpty text="No Logs" />
|
||||
)}
|
||||
</Box>
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
@@ -51,6 +51,7 @@ const ProxyPage = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
contentStyle={{ height: "100%" }}
|
||||
title={t("Proxy Groups")}
|
||||
header={
|
||||
@@ -72,16 +73,7 @@ const ProxyPage = () => {
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
boxShadow: 0,
|
||||
height: "100%",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<ProxyGroups mode={curMode!} />
|
||||
</Box>
|
||||
<ProxyGroups mode={curMode!} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,45 +18,43 @@ const RulesPage = () => {
|
||||
}, [data, filterText]);
|
||||
|
||||
return (
|
||||
<BasePage title={t("Rules")} contentStyle={{ height: "100%" }}>
|
||||
<Box sx={{ boxSizing: "border-box", boxShadow: 0, height: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "12px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
<BasePage full title={t("Rules")} contentStyle={{ height: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "10px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)">
|
||||
{rules.length > 0 ? (
|
||||
<Virtuoso
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty text="No Rules" />
|
||||
)}
|
||||
</Box>
|
||||
<Box height="calc(100% - 50px)">
|
||||
{rules.length > 0 ? (
|
||||
<Virtuoso
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty text="No Rules" />
|
||||
)}
|
||||
</Box>
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
@@ -119,7 +119,14 @@ export const getProxies = async () => {
|
||||
const generateItem = (name: string) => {
|
||||
if (proxyRecord[name]) return proxyRecord[name];
|
||||
if (providerMap[name]) return providerMap[name];
|
||||
return { name, type: "unknown", udp: false, history: [] };
|
||||
return {
|
||||
name,
|
||||
type: "unknown",
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
history: [],
|
||||
};
|
||||
};
|
||||
|
||||
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
|
||||
|
||||
@@ -191,3 +191,7 @@ export async function invoke_uwp_tool() {
|
||||
Notice.error(err?.message || err.toString(), 1500)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPortableFlag() {
|
||||
return invoke<boolean>("get_portable_flag");
|
||||
}
|
||||
|
||||
4
src/services/types.d.ts
vendored
4
src/services/types.d.ts
vendored
@@ -14,7 +14,6 @@ type Platform =
|
||||
/**
|
||||
* defines in `vite.config.ts`
|
||||
*/
|
||||
declare const WIN_PORTABLE: boolean;
|
||||
declare const OS_PLATFORM: Platform;
|
||||
|
||||
/**
|
||||
@@ -45,12 +44,15 @@ interface IProxyItem {
|
||||
name: string;
|
||||
type: string;
|
||||
udp: boolean;
|
||||
xudp: boolean;
|
||||
tfo: boolean;
|
||||
history: {
|
||||
time: string;
|
||||
delay: number;
|
||||
}[];
|
||||
all?: string[];
|
||||
now?: string;
|
||||
hidden?: boolean;
|
||||
provider?: string; // 记录是否来自provider
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,5 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
OS_PLATFORM: `"${process.platform}"`,
|
||||
WIN_PORTABLE: !!process.env.VITE_WIN_PORTABLE,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user