mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-17 14:50:48 +08:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396a3e81d8 | ||
|
|
17fd1248d0 | ||
|
|
0190702616 | ||
|
|
c35bf38420 | ||
|
|
89d20e564a | ||
|
|
3c24ff3afc | ||
|
|
daf0398750 | ||
|
|
6b14c2b763 | ||
|
|
5bf2f9b8ed | ||
|
|
10a2655288 | ||
|
|
ac07397818 | ||
|
|
30d061d00f | ||
|
|
360d8a5143 | ||
|
|
70e0a5adc8 | ||
|
|
ef52f81494 | ||
|
|
e5c1de3ad3 | ||
|
|
576fd700ae | ||
|
|
476df65a1b | ||
|
|
bd0a4863a8 | ||
|
|
d25fbc05e2 | ||
|
|
b8a0b6f1f4 | ||
|
|
18c7ed1ccc | ||
|
|
b14db06955 | ||
|
|
215dcee3f1 | ||
|
|
37e5c22a5a | ||
|
|
bb0b6c3f77 | ||
|
|
a37df46ce7 | ||
|
|
73fbec2514 | ||
|
|
f66fa08b2c | ||
|
|
bae606bc9d | ||
|
|
1a2d5e988a | ||
|
|
d20bd62b90 | ||
|
|
204bf43b9a | ||
|
|
3ea0d20e2c |
@@ -2,9 +2,6 @@ name: Alpha Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
tags-ignore: [updater, alpha]
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -58,7 +55,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -84,8 +81,8 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Rev Alpha"
|
||||
tagName: alpha-__VERSION__
|
||||
releaseName: "Clash Verge Rev Alpha __VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
@@ -129,7 +126,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -218,6 +215,8 @@ jobs:
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
updater/update-alpha.json
|
||||
updater/update-alpha-proxy.json
|
||||
|
||||
alpha-for-fixed-webview2:
|
||||
strategy:
|
||||
@@ -248,7 +247,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
@@ -55,6 +55,10 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### Donation
|
||||
|
||||
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
||||
+24
-24
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.4-alpha",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||
@@ -19,32 +19,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^6.3.0",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/x-data-grid": "^7.23.5",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.0",
|
||||
"@mui/icons-material": "^6.4.2",
|
||||
"@mui/lab": "6.0.0-beta.25",
|
||||
"@mui/material": "^6.4.2",
|
||||
"@mui/x-data-grid": "^7.25.0",
|
||||
"@tauri-apps/api": "2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.3.0",
|
||||
"@tauri-apps/plugin-updater": "2.4.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
"cli-color": "^2.0.4",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.43",
|
||||
"glob": "^11.0.0",
|
||||
"i18next": "^23.16.8",
|
||||
"glob": "^11.0.1",
|
||||
"i18next": "^24.2.2",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -54,31 +54,31 @@
|
||||
"peggy": "^4.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-monaco-editor": "^0.56.2",
|
||||
"react-router-dom": "^6.28.1",
|
||||
"react-monaco-editor": "^0.56.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"react-virtuoso": "^4.6.3",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.0",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.2"
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.0",
|
||||
"@tauri-apps/cli": "2.1.0",
|
||||
"@tauri-apps/cli": "2.2.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
@@ -87,10 +87,10 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.4.2",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"sass": "^1.83.0",
|
||||
"sass": "^1.83.4",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
|
||||
Generated
+1111
-1207
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -5,7 +5,7 @@ import { extract } from "tar";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
import proxyAgent from "https-proxy-agent";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { execSync } from "child_process";
|
||||
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
||||
import { glob } from "glob";
|
||||
@@ -85,7 +85,7 @@ async function getLatestAlphaVersion() {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
@@ -132,7 +132,7 @@ async function getLatestReleaseVersion() {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
@@ -332,7 +332,7 @@ async function resolveResource(binInfo) {
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
|
||||
Generated
+807
-752
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.0.3"
|
||||
version = "2.0.4-alpha"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -13,19 +13,19 @@ build = "build.rs"
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
tauri-build = { version = "2.0.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
dirs = "6.0"
|
||||
open = "5.1"
|
||||
log = "0.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.33.0"
|
||||
sysinfo = "0.33.1"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
@@ -35,14 +35,14 @@ delay_timer = "0.11"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
window-shadows = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||
image = "0.24"
|
||||
imageproc = "0.23"
|
||||
rusttype = "0.9"
|
||||
tauri = { version = "2.1.1", features = [
|
||||
tauri = { version = "2.2.5", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -53,13 +53,13 @@ network-interface = { version = "2.0.0", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.0"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-notification = "2.2.0"
|
||||
tauri-plugin-notification = "2.2.1"
|
||||
tauri-plugin-process = "2.2.0"
|
||||
tauri-plugin-clipboard-manager = "2.2.0"
|
||||
tauri-plugin-clipboard-manager = "2.2.1"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-devtools = "2.0.0-rc"
|
||||
url = "2.5.2"
|
||||
zip = "2.2.0"
|
||||
url = "2.5.4"
|
||||
zip = "2.2.2"
|
||||
reqwest_dav = "0.1.14"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
@@ -71,8 +71,8 @@ sys-locale = "0.3.1"
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.52.0"
|
||||
url = "2.5.2"
|
||||
winreg = "0.55.0"
|
||||
url = "2.5.4"
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
@@ -81,8 +81,8 @@ users = "0.11.0"
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.2.0"
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.3.0"
|
||||
tauri-plugin-window-state = "2.2.0"
|
||||
tauri-plugin-updater = "2.4.0"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
#openssl
|
||||
|
||||
[features]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+14
-2
@@ -22,6 +22,7 @@ pub fn copy_clash_env() -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
@@ -62,7 +63,6 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -87,6 +86,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult {
|
||||
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
@@ -169,6 +175,12 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
Ok(feat::change_clash_mode(payload))
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
|
||||
@@ -71,7 +71,6 @@ impl IProfiles {
|
||||
if let Some(current) = patch.current {
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(current);
|
||||
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
}
|
||||
@@ -465,4 +464,23 @@ impl IProfiles {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断profile是否是current指向的
|
||||
pub fn is_current_profile_index(&self, index: String) -> bool {
|
||||
self.current == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||
match self.items.as_ref() {
|
||||
Some(items) => Some(items.iter().filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ pub struct IVerge {
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
/// enable global hotkey
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
|
||||
/// 切换代理时自动关闭连接
|
||||
pub auto_close_connection: Option<bool>,
|
||||
@@ -178,6 +181,9 @@ pub struct IVerge {
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
/// 轻量模式 - 只保留内核运行
|
||||
pub enable_lite_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -279,6 +285,8 @@ impl IVerge {
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(true),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_lite_mode: Some(false),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -345,6 +353,7 @@ impl IVerge {
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
patch!(enable_global_hotkey);
|
||||
|
||||
patch!(auto_close_connection);
|
||||
patch!(auto_check_update);
|
||||
@@ -359,6 +368,7 @@ impl IVerge {
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(enable_lite_mode);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@@ -411,6 +421,7 @@ pub struct IVergeResponse {
|
||||
pub enable_silent_start: Option<bool>,
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
pub use_default_bypass: Option<bool>,
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
@@ -446,6 +457,7 @@ pub struct IVergeResponse {
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_lite_mode: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
@@ -472,6 +484,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
enable_silent_start: verge.enable_silent_start,
|
||||
enable_system_proxy: verge.enable_system_proxy,
|
||||
enable_proxy_guard: verge.enable_proxy_guard,
|
||||
enable_global_hotkey: verge.enable_global_hotkey,
|
||||
use_default_bypass: verge.use_default_bypass,
|
||||
system_proxy_bypass: verge.system_proxy_bypass,
|
||||
proxy_guard_duration: verge.proxy_guard_duration,
|
||||
@@ -507,6 +520,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_lite_mode: verge.enable_lite_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::tray::Tray;
|
||||
use crate::config::*;
|
||||
use crate::core::{clash_api, handle, service};
|
||||
use crate::core::tray::Tray;
|
||||
use crate::log_err;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use anyhow::{bail, Result};
|
||||
#[cfg(target_os = "macos")]
|
||||
use anyhow::Result;
|
||||
use sysinfo::{Pid, System};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use crate::config::Config;
|
||||
use crate::core::service;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use port_scanner::local_port_available;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HealthChecker;
|
||||
|
||||
impl HealthChecker {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub async fn check_ports(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_config = verge.latest();
|
||||
let mixed_port = verge_config.verge_mixed_port.unwrap_or(7897);
|
||||
let socks_port = verge_config.verge_socks_port.unwrap_or(7890);
|
||||
let http_port = verge_config.verge_port.unwrap_or(7891);
|
||||
|
||||
if !local_port_available(mixed_port) {
|
||||
bail!("Mixed port {} is already in use", mixed_port);
|
||||
}
|
||||
|
||||
if verge_config.verge_socks_enabled.unwrap_or(true) && !local_port_available(socks_port) {
|
||||
bail!("Socks port {} is already in use", socks_port);
|
||||
}
|
||||
|
||||
if verge_config.verge_http_enabled.unwrap_or(true) && !local_port_available(http_port) {
|
||||
bail!("Http port {} is already in use", http_port);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_service_health(&self) -> Result<()> {
|
||||
if let Ok(response) = service::check_service().await {
|
||||
if let Some(body) = response.data {
|
||||
let sys = System::new_all();
|
||||
if let Ok(pid) = body.bin_path.parse::<u32>() {
|
||||
if let Some(process) = sys.process(Pid::from(pid as usize)) {
|
||||
if !process.name().to_string_lossy().contains("mihomo") {
|
||||
log::warn!(target: "app", "Found non-mihomo process using service port");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::core::handle;
|
||||
use crate::{config::Config, feat, log_err};
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
use tauri::async_runtime;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
}
|
||||
@@ -21,8 +24,22 @@ impl Hotkey {
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
if !enable_global_hotkey {
|
||||
println!("Global hotkey is disabled, skipping registration");
|
||||
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||
println!("Found {} hotkeys to register", hotkeys.len());
|
||||
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
|
||||
|
||||
for hotkey in hotkeys.iter() {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
@@ -30,56 +47,112 @@ impl Hotkey {
|
||||
|
||||
match (key, func) {
|
||||
(Some(key), Some(func)) => {
|
||||
log_err!(self.register(key, func));
|
||||
println!("Registering hotkey: {} -> {}", key, func);
|
||||
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
|
||||
if let Err(e) = self.register(key, func) {
|
||||
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
||||
} else {
|
||||
println!("Successfully registered hotkey {} -> {}", key, func);
|
||||
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let key = key.unwrap_or("None");
|
||||
let func = func.unwrap_or("None");
|
||||
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
||||
println!("Invalid hotkey configuration: `{key}`:`{func}`");
|
||||
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current.lock().clone_from(hotkeys);
|
||||
} else {
|
||||
println!("No hotkeys configured");
|
||||
log::info!(target: "app", "No hotkeys configured");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||
|
||||
if manager.is_registered(hotkey) {
|
||||
println!("Hotkey {} was already registered, unregistering first", hotkey);
|
||||
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_or_close_dashboard" => feat::open_or_close_dashboard,
|
||||
"open_or_close_dashboard" => {
|
||||
println!("Registering open_or_close_dashboard function");
|
||||
log::info!(target: "app", "Registering open_or_close_dashboard function");
|
||||
|| {
|
||||
println!("=== Hotkey Dashboard Window Operation Start ===");
|
||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
async_runtime::spawn_blocking(|| {
|
||||
println!("Creating window in spawn_blocking");
|
||||
log::info!(target: "app", "Creating window in spawn_blocking");
|
||||
resolve::create_window();
|
||||
});
|
||||
|
||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
||||
}
|
||||
},
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => feat::toggle_system_proxy,
|
||||
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
||||
"quit" => || feat::quit(Some(0)),
|
||||
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
_ => {
|
||||
println!("Invalid function: {}", func);
|
||||
log::error!(target: "app", "Invalid function: {}", func);
|
||||
bail!("invalid function \"{func}\"");
|
||||
}
|
||||
};
|
||||
|
||||
let is_quit = func.trim() == "quit";
|
||||
|
||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
if hotkey.key == Code::KeyQ {
|
||||
println!("Hotkey pressed: {:?}", hotkey);
|
||||
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
|
||||
|
||||
if hotkey.key == Code::KeyQ && is_quit {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
println!("Executing quit function");
|
||||
log::info!(target: "app", "Executing quit function");
|
||||
f();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
println!("Executing function directly");
|
||||
log::info!(target: "app", "Executing function directly");
|
||||
f();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::debug!(target: "app", "register hotkey {hotkey} {func}");
|
||||
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
||||
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -87,7 +160,6 @@ impl Hotkey {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister(hotkey)?;
|
||||
|
||||
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use sysinfo::{Pid, System, Signal};
|
||||
use crate::utils::dirs;
|
||||
use std::time::Duration;
|
||||
|
||||
const KILL_WAIT: Duration = Duration::from_millis(500);
|
||||
const FINAL_WAIT: Duration = Duration::from_millis(1000);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessLock {
|
||||
pid_file: PathBuf,
|
||||
}
|
||||
|
||||
impl ProcessLock {
|
||||
pub fn new() -> Result<Self> {
|
||||
let pid_file = dirs::app_home_dir()?.join("mihomo.pid");
|
||||
println!("Creating ProcessLock with PID file: {:?}", pid_file);
|
||||
log::info!(target: "app", "Creating ProcessLock with PID file: {:?}", pid_file);
|
||||
Ok(Self { pid_file })
|
||||
}
|
||||
|
||||
fn is_target_process(process_name: &str) -> bool {
|
||||
let name = process_name.to_lowercase();
|
||||
(name.contains("mihomo") || name.contains("clash"))
|
||||
&& !name.contains("clash-verge")
|
||||
&& !name.contains("clash.meta")
|
||||
}
|
||||
|
||||
fn kill_process(pid: Pid, process: &sysinfo::Process) -> bool {
|
||||
let process_name = process.name().to_string_lossy().to_lowercase();
|
||||
let process_pid = pid.as_u32();
|
||||
|
||||
println!("Force killing process (PID: {}, Name: {})", process_pid, process_name);
|
||||
log::info!(target: "app", "Force killing process (PID: {}, Name: {})", process_pid, process_name);
|
||||
|
||||
// 直接使用 SIGKILL 强制终止
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
println!("Sending SIGKILL to process {}", process_pid);
|
||||
log::info!(target: "app", "Sending SIGKILL to process {}", process_pid);
|
||||
let _ = process.kill_with(Signal::Kill);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
println!("Force killing process {}", process_pid);
|
||||
log::info!(target: "app", "Force killing process {}", process_pid);
|
||||
process.kill();
|
||||
}
|
||||
|
||||
std::thread::sleep(KILL_WAIT);
|
||||
|
||||
// 检查进程是否还在运行
|
||||
let mut sys = System::new();
|
||||
sys.refresh_all();
|
||||
|
||||
let is_terminated = sys.process(pid).is_none();
|
||||
if !is_terminated {
|
||||
println!("Failed to terminate process {}", process_pid);
|
||||
log::error!(target: "app", "Failed to terminate process {}", process_pid);
|
||||
} else {
|
||||
println!("Process {} has been terminated", process_pid);
|
||||
log::info!(target: "app", "Process {} has been terminated", process_pid);
|
||||
}
|
||||
|
||||
is_terminated
|
||||
}
|
||||
|
||||
fn kill_other_processes(include_current: bool) -> Result<()> {
|
||||
println!("Starting process cleanup (include_current: {})", include_current);
|
||||
log::info!(target: "app", "Starting process cleanup (include_current: {})", include_current);
|
||||
|
||||
let mut sys = System::new();
|
||||
sys.refresh_all();
|
||||
|
||||
let current_pid = std::process::id();
|
||||
let mut failed_kills = Vec::new();
|
||||
|
||||
// 收集需要终止的进程
|
||||
let target_processes: Vec<_> = sys.processes()
|
||||
.iter()
|
||||
.filter(|(pid, process)| {
|
||||
let process_pid = pid.as_u32();
|
||||
let process_name = process.name().to_string_lossy();
|
||||
|
||||
Self::is_target_process(&process_name) &&
|
||||
(include_current || process_pid != current_pid)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 如果没有目标进程,直接返回
|
||||
if target_processes.is_empty() {
|
||||
println!("No target processes found");
|
||||
log::info!(target: "app", "No target processes found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 终止进程
|
||||
for (pid, process) in target_processes {
|
||||
if !Self::kill_process(*pid, process) {
|
||||
failed_kills.push((pid.as_u32(), process.name().to_string_lossy().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// 最终检查
|
||||
std::thread::sleep(FINAL_WAIT);
|
||||
sys.refresh_all();
|
||||
|
||||
let remaining: Vec<_> = sys.processes()
|
||||
.iter()
|
||||
.filter(|(_, process)| Self::is_target_process(&process.name().to_string_lossy()))
|
||||
.map(|(pid, process)| (pid.as_u32(), process.name().to_string_lossy().to_string()))
|
||||
.collect();
|
||||
|
||||
if !remaining.is_empty() {
|
||||
println!("Failed to terminate processes: {:?}", remaining);
|
||||
log::error!(target: "app", "Failed to terminate processes: {:?}", remaining);
|
||||
bail!("Failed to terminate processes: {:?}", remaining);
|
||||
}
|
||||
|
||||
println!("Process cleanup completed");
|
||||
log::info!(target: "app", "Process cleanup completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn acquire(&self) -> Result<()> {
|
||||
println!("Attempting to acquire process lock");
|
||||
log::info!(target: "app", "Attempting to acquire process lock");
|
||||
|
||||
// 首先尝试终止其他进程
|
||||
Self::kill_other_processes(false)?;
|
||||
|
||||
if self.pid_file.exists() {
|
||||
fs::remove_file(&self.pid_file)?;
|
||||
}
|
||||
|
||||
fs::write(&self.pid_file, std::process::id().to_string())?;
|
||||
println!("Process lock acquired successfully");
|
||||
log::info!(target: "app", "Process lock acquired successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn release(&self) -> Result<()> {
|
||||
println!("Starting release process");
|
||||
log::info!(target: "app", "Starting release process");
|
||||
|
||||
Self::kill_other_processes(true)?;
|
||||
|
||||
if self.pid_file.exists() {
|
||||
println!("Removing PID file");
|
||||
log::info!(target: "app", "Removing PID file");
|
||||
fs::remove_file(&self.pid_file)?;
|
||||
}
|
||||
|
||||
println!("Release process completed");
|
||||
log::info!(target: "app", "Release process completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProcessLock {
|
||||
fn drop(&mut self) {
|
||||
if self.pid_file.exists() {
|
||||
println!("ProcessLock being dropped");
|
||||
log::info!(target: "app", "ProcessLock being dropped");
|
||||
let _ = self.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use parking_lot::RwLock;
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use tauri::menu::CheckMenuItem;
|
||||
use tauri::menu::{CheckMenuItem, IsMenuItem};
|
||||
use tauri::AppHandle;
|
||||
use tauri::{
|
||||
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
@@ -68,44 +68,64 @@ impl Tray {
|
||||
|
||||
pub fn create_systray(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_incon_id = TrayIconId::new("main");
|
||||
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
// 创建初始菜单
|
||||
let initial_menu = create_tray_menu(
|
||||
&app_handle,
|
||||
Some("rule"),
|
||||
false,
|
||||
false,
|
||||
Vec::new(),
|
||||
)?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Right,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
// 使用 TrayIconBuilder 构建托盘
|
||||
let _tray = tauri::tray::TrayIconBuilder::<Wry>::with_id(TrayIconId::new("main"))
|
||||
.menu(&initial_menu) // 先设置菜单
|
||||
.show_menu_on_left_click(false) // 再禁用左键菜单
|
||||
.on_tray_icon_event(move |_tray, event| {
|
||||
// 处理左键点击事件
|
||||
if let TrayIconEvent::Click {
|
||||
button,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match button {
|
||||
// 左键点击
|
||||
MouseButton::Left => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// 确保菜单不会显示
|
||||
let _ = _tray.set_show_menu_on_left_click(false);
|
||||
}
|
||||
|
||||
// 获取并执行自定义事件
|
||||
let tray_event = Config::verge()
|
||||
.latest()
|
||||
.tray_event
|
||||
.clone()
|
||||
.unwrap_or("main_window".into());
|
||||
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// 其他按钮点击不处理
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_menu_event(on_menu_event)
|
||||
.build(&app_handle)?;
|
||||
|
||||
// 初始化托盘状态
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,6 +144,10 @@ impl Tray {
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.data()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
@@ -131,6 +155,7 @@ impl Tray {
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
profile_uid_and_name,
|
||||
)?));
|
||||
Ok(())
|
||||
}
|
||||
@@ -363,6 +388,7 @@ fn create_tray_menu(
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
tun_mode_enabled: bool,
|
||||
profile_uid_and_name: Vec<(String, String)>,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
let version = VERSION.get().unwrap();
|
||||
@@ -382,6 +408,25 @@ fn create_tray_menu(
|
||||
.collect::<std::collections::HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles().data().is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
&format!("profiles_{}", profile_uid),
|
||||
t(&profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap()
|
||||
}).collect();
|
||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
|
||||
let open_window = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -422,6 +467,14 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
t("Profiles"),
|
||||
true,
|
||||
&profile_menu_items,
|
||||
).unwrap();
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
@@ -530,6 +583,8 @@ fn create_tray_menu(
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
copy_env,
|
||||
@@ -562,6 +617,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"quit" => {
|
||||
println!("quit");
|
||||
feat::quit(Some(0));
|
||||
},
|
||||
id if id.starts_with("profiles_") => {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -48,16 +48,13 @@ impl SpeedRate {
|
||||
let img = image::load_from_memory(&icon)?;
|
||||
let (width, height) = (img.width(), img.height());
|
||||
|
||||
let mut image = ImageBuffer::new((width as f32 * 4.0) as u32, height);
|
||||
image::imageops::replace(&mut image, &img, 0, 0);
|
||||
|
||||
let font =
|
||||
Font::try_from_bytes(include_bytes!("../../../assets/fonts/SFCompact.ttf")).unwrap();
|
||||
Font::try_from_bytes(include_bytes!("../../../assets/fonts/FiraCode-Medium.ttf")).unwrap();
|
||||
|
||||
// 修改颜色和阴影参数
|
||||
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); // 纯白色
|
||||
let shadow_color = Rgba([0u8, 0u8, 0u8, 180u8]); // 半透明黑色阴影
|
||||
let base_size = height as f32 * 0.5;
|
||||
let shadow_color = Rgba([0u8, 0u8, 0u8, 120u8]); // 降低阴影不透明度
|
||||
let base_size = height as f32 * 0.6; // 保持字体大小
|
||||
let scale = Scale::uniform(base_size);
|
||||
|
||||
let up_text = format_bytes_speed(rate.up);
|
||||
@@ -76,20 +73,32 @@ impl SpeedRate {
|
||||
.last()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let right_margin = 8;
|
||||
let canvas_width = width * 4;
|
||||
let up_x = canvas_width as f32 - up_width - right_margin as f32;
|
||||
let down_x = canvas_width as f32 - down_width - right_margin as f32;
|
||||
let icon_text_gap = 40; // 图标和文字之间的间隔
|
||||
|
||||
// 计算所需的总宽度:图标宽度 + 间隔 + 最大文本宽度
|
||||
let max_text_width = up_width.max(down_width);
|
||||
let total_width = width as f32 + icon_text_gap as f32 + max_text_width;
|
||||
|
||||
let mut image = ImageBuffer::new(total_width.ceil() as u32, height);
|
||||
// 将图标绘制在最左边
|
||||
image::imageops::replace(&mut image, &img, 0_i64, 0_i64);
|
||||
|
||||
// 计算文字的起始x坐标(图标宽度 + 间隔)
|
||||
let text_start_x = width as i32 + icon_text_gap as i32;
|
||||
|
||||
// 添加阴影效果
|
||||
let shadow_offset = 1; // 阴影偏移量
|
||||
let shadow_offset = 1;
|
||||
|
||||
// 计算垂直位置
|
||||
let up_y = 0; // 上行速率紧贴顶部
|
||||
let down_y = height as i32 - base_size as i32; // 下行速率紧贴底部
|
||||
|
||||
// 绘制上行速率(先画阴影,再画文字)
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
shadow_color,
|
||||
up_x as i32 + shadow_offset,
|
||||
1 + shadow_offset,
|
||||
text_start_x + shadow_offset,
|
||||
up_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
@@ -97,8 +106,8 @@ impl SpeedRate {
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
text_color,
|
||||
up_x as i32,
|
||||
1,
|
||||
text_start_x,
|
||||
up_y,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
@@ -108,8 +117,8 @@ impl SpeedRate {
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
shadow_color,
|
||||
down_x as i32 + shadow_offset,
|
||||
height as i32 - (base_size as i32) - 1 + shadow_offset,
|
||||
text_start_x + shadow_offset,
|
||||
down_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
@@ -117,8 +126,8 @@ impl SpeedRate {
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
text_color,
|
||||
down_x as i32,
|
||||
height as i32 - (base_size as i32) - 1,
|
||||
text_start_x,
|
||||
down_y,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
|
||||
@@ -36,14 +36,30 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||
revise!(dns_val, "fake-ip-range", "172.29.0.1/16");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
// 检查现有的 enhanced-mode 设置
|
||||
let current_mode = dns_val
|
||||
.get(&Value::from("enhanced-mode"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
if !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||
}
|
||||
|
||||
if !dns_val.contains_key(&Value::from("fake-ip-range")) {
|
||||
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
revise!(
|
||||
@@ -72,7 +88,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
dns_val
|
||||
.get("fake-ip-range")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("172.29.0.1/16")
|
||||
.unwrap_or("198.18.0.1/16")
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
+73
-8
@@ -4,6 +4,7 @@
|
||||
//! - timer 定时器
|
||||
//! - cmds 页面调用
|
||||
//!
|
||||
use crate::cmds;
|
||||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
@@ -18,16 +19,43 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
// 打开面板
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window();
|
||||
}
|
||||
}
|
||||
@@ -65,7 +93,6 @@ pub fn restart_app() {
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
|
||||
@@ -103,6 +130,19 @@ pub fn toggle_system_proxy() {
|
||||
});
|
||||
}
|
||||
|
||||
// 切换代理文件
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match cmds::patch_profiles_config_by_profile_index(profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
handle::Handle::refresh_verge();
|
||||
},
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
@@ -175,6 +215,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
let lite_mode = patch.enable_lite_mode;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -195,10 +236,12 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
let mut should_restart_core = false;
|
||||
let mut should_update_clash_config = false;
|
||||
let mut should_update_verge_config = false;
|
||||
let mut should_update_launch = false;
|
||||
let mut should_update_sysproxy = false;
|
||||
let mut should_update_systray_icon = false;
|
||||
@@ -212,12 +255,13 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
should_update_systray_tooltip = true;
|
||||
should_update_systray_icon = true;
|
||||
}
|
||||
|
||||
if enable_global_hotkey.is_some() {
|
||||
should_update_verge_config = true;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
should_restart_core = true;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
should_restart_core = true;
|
||||
@@ -272,6 +316,10 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if should_update_verge_config {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if should_update_launch {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
@@ -295,6 +343,23 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
if should_update_systray_tooltip {
|
||||
tray::Tray::global().update_tooltip()?;
|
||||
}
|
||||
|
||||
// 处理轻量模式切换
|
||||
if lite_mode.is_some() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if lite_mode.unwrap() {
|
||||
// 完全退出 webview 进程
|
||||
window.close()?; // 先关闭窗口
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
if let Some(webview) = app_handle.get_webview_window("main") {
|
||||
webview.destroy()?; // 销毁 webview 进程
|
||||
}
|
||||
} else {
|
||||
resolve::create_window(); // 重新创建窗口
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
|
||||
@@ -6,6 +6,7 @@ mod feat;
|
||||
mod utils;
|
||||
use crate::core::hotkey;
|
||||
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
||||
use config::Config;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
@@ -82,6 +83,7 @@ pub fn run() {
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::patch_clash_config,
|
||||
cmds::patch_clash_mode,
|
||||
cmds::change_clash_core,
|
||||
cmds::get_runtime_config,
|
||||
cmds::get_runtime_yaml,
|
||||
@@ -159,6 +161,12 @@ pub fn run() {
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
log_err!(hotkey::Hotkey::global().init())
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(false) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -169,6 +177,12 @@ pub fn run() {
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
log_err!(hotkey::Hotkey::global().reset())
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -223,6 +223,7 @@ pub fn format_bytes_speed(speed: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_format_bytes_speed() {
|
||||
assert_eq!(format_bytes_speed(0), "0B/s");
|
||||
|
||||
@@ -95,16 +95,19 @@ pub async fn resolve_setup(app: &mut App) {
|
||||
log_err!(tray::Tray::global().init());
|
||||
log_err!(tray::Tray::global().create_systray());
|
||||
|
||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
||||
|
||||
// 初始化热键
|
||||
log::trace!(target: "app", "init hotkeys");
|
||||
log_err!(hotkey::Hotkey::global().init());
|
||||
|
||||
let silent_start = { Config::verge().data().enable_silent_start };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window();
|
||||
}
|
||||
|
||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
||||
|
||||
log_err!(tray::Tray::global().update_part());
|
||||
log_err!(hotkey::Hotkey::global().init());
|
||||
log_err!(timer::Timer::global().init());
|
||||
}
|
||||
|
||||
@@ -123,10 +126,18 @@ pub fn resolve_reset() {
|
||||
|
||||
/// create main window
|
||||
pub fn create_window() {
|
||||
println!("Starting to create window");
|
||||
log::info!(target: "app", "Starting to create window");
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window, trying to show it");
|
||||
log::info!(target: "app", "Found existing window, trying to show it");
|
||||
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
println!("Window is minimized, unminimizing");
|
||||
log::info!(target: "app", "Window is minimized, unminimizing");
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
@@ -134,14 +145,16 @@ pub fn create_window() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Creating new window");
|
||||
log::info!(target: "app", "Creating new window");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let _ = tauri::WebviewWindowBuilder::new(
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.visible(false)
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.decorations(false)
|
||||
@@ -152,7 +165,7 @@ pub fn create_window() {
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = tauri::WebviewWindowBuilder::new(
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
@@ -162,11 +175,10 @@ pub fn create_window() {
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.build()
|
||||
.unwrap();
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let _ = tauri::WebviewWindowBuilder::new(
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
@@ -176,8 +188,20 @@ pub fn create_window() {
|
||||
.inner_size(890.0, 700.0)
|
||||
.min_inner_size(620.0, 550.0)
|
||||
.transparent(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
.build();
|
||||
|
||||
match window {
|
||||
Ok(window) => {
|
||||
println!("Window created successfully, attempting to show");
|
||||
log::info!(target: "app", "Window created successfully, attempting to show");
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to create window: {:?}", e);
|
||||
log::error!(target: "app", "Failed to create window: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
|
||||
@@ -25,20 +25,24 @@
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.4-alpha",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||
]
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["clash", "clash-verge"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"bundle": {
|
||||
"targets": ["deb", "rpm"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
"replaces": ["clash-verge"],
|
||||
"postInstallScript": "./packages/linux/post-install.sh",
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
"obsoletes": ["clash-verge"],
|
||||
"postInstallScript": "./packages/linux/post-install.sh",
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
}
|
||||
},
|
||||
"externalBin": [
|
||||
"./resources/clash-verge-service",
|
||||
"./resources/install-service",
|
||||
"./resources/uninstall-service",
|
||||
"./sidecar/verge-mihomo",
|
||||
"./sidecar/verge-mihomo-alpha"
|
||||
]
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,8 @@
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico"
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,5 @@
|
||||
"template": "./packages/windows/installer.nsi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true,
|
||||
"menuOnLeftClick": false
|
||||
},
|
||||
"windows": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
? `${metadata.host}:${metadata.remoteDestination}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
|
||||
const information = [
|
||||
{ label: t("Host"), value: host },
|
||||
@@ -79,7 +79,8 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
label: t("Source"),
|
||||
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
},
|
||||
{ label: t("Destination IP"), value: metadata.destinationIP },
|
||||
{ label: t("Destination"), value: metadata.remoteDestination },
|
||||
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
|
||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
{ field: "source", headerName: t("Source"), flex: 200, minWidth: 130 },
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: t("Destination IP"),
|
||||
field: "remoteDestination",
|
||||
headerName: t("Destination"),
|
||||
flex: 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
@@ -88,7 +88,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
@@ -98,7 +98,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
remoteDestination: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
connectionData: each,
|
||||
};
|
||||
@@ -115,9 +115,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
sx={{
|
||||
border: "none",
|
||||
"div:focus": { outline: "none !important" },
|
||||
"& div[aria-rowindex]": {
|
||||
backgroundColor: `${backgroundColor} !important`,
|
||||
},
|
||||
}}
|
||||
columnVisibilityModel={columnVisible}
|
||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||
|
||||
@@ -25,7 +25,11 @@ export const useRenderList = (mode: string) => {
|
||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
||||
"getProxies",
|
||||
getProxies,
|
||||
{ refreshInterval: 45000 },
|
||||
{
|
||||
refreshInterval: 2000,
|
||||
dedupingInterval: 1000,
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { styled, Typography } from "@mui/material";
|
||||
import { styled, Typography, Switch } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { HotkeyInput } from "./hotkey-input";
|
||||
@@ -29,6 +29,9 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { verge, patchVerge } = useVerge();
|
||||
|
||||
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
||||
const [enableGlobalHotkey, setEnableHotkey] = useState(
|
||||
verge?.enable_global_hotkey ?? true,
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
@@ -69,7 +72,10 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
.filter(Boolean);
|
||||
|
||||
try {
|
||||
await patchVerge({ hotkeys });
|
||||
await patchVerge({
|
||||
hotkeys,
|
||||
enable_global_hotkey: enableGlobalHotkey,
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
@@ -80,13 +86,22 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Hotkey Setting")}
|
||||
contentSx={{ width: 450, maxHeight: 330 }}
|
||||
contentSx={{ width: 450, maxHeight: 380 }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onSave}
|
||||
>
|
||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||
<Typography>{t("Enable Global Hotkey")}</Typography>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={enableGlobalHotkey}
|
||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||
/>
|
||||
</ItemWrapper>
|
||||
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
<ItemWrapper key={func}>
|
||||
<Typography>{t(func)}</Typography>
|
||||
|
||||
@@ -60,6 +60,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
env_type,
|
||||
startup_script,
|
||||
start_page,
|
||||
enable_lite_mode,
|
||||
} = verge ?? {};
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
const hotkeyRef = useRef<DialogRef>(null);
|
||||
@@ -69,7 +70,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
const onChangeData = (patch: any) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
@@ -292,6 +293,14 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
|
||||
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
|
||||
|
||||
<SettingItem
|
||||
label={t("Lite Mode")}
|
||||
extra={
|
||||
<TooltipIcon title={t("Lite Mode Info")} sx={{ opacity: "0.7" }} />
|
||||
}
|
||||
onClick={() => patchVerge({ enable_lite_mode: true })}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => {
|
||||
exitApp();
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "عملية",
|
||||
"Time": "الوقت",
|
||||
"Source": "المصدر",
|
||||
"Destination IP": "عنوان IP الوجهة",
|
||||
"Destination": "عنوان IP الوجهة",
|
||||
"DestinationPort": "ميناء الوجهة",
|
||||
"Close Connection": "إغلاق الاتصال",
|
||||
"Rules": "القواعد",
|
||||
"Rule Provider": "مزود القواعد",
|
||||
@@ -326,6 +327,7 @@
|
||||
"Default Latency Test Info": "يُستخدم فقط لاختبار طلب HTTP العميل. لن يؤثر على ملف التكوين",
|
||||
"Default Latency Timeout": "مهلة التأخير الافتراضية",
|
||||
"Hotkey Setting": "إعدادات الاختصارات",
|
||||
"Enable Global Hotkey": "تمكين مفتاح التشغيل السريع العالمي",
|
||||
"open_or_close_dashboard": "فتح/إغلاق لوحة التحكم",
|
||||
"clash_mode_rule": "وضع القواعد",
|
||||
"clash_mode_global": "الوضع العالمي",
|
||||
@@ -434,5 +436,7 @@
|
||||
"Rule Mode": "وضع القواعد",
|
||||
"Global Mode": "الوضع العالمي",
|
||||
"Direct Mode": "الوضع المباشر",
|
||||
"Enable Tray Speed": "تفعيل سرعة التراي"
|
||||
"Enable Tray Speed": "تفعيل سرعة التراي",
|
||||
"Lite Mode": "وضع الأداء الخفيف",
|
||||
"Lite Mode Info": "إيقاف الواجهة الرسومية والإبقاء على تشغيل النواة"
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "Process",
|
||||
"Time": "Time",
|
||||
"Source": "Source",
|
||||
"Destination IP": "Destination IP",
|
||||
"Destination": "Destination IP",
|
||||
"DestinationPort": "Destination Port",
|
||||
"Close Connection": "Close Connection",
|
||||
"Rules": "Rules",
|
||||
"Rule Provider": "Rule Provider",
|
||||
@@ -328,6 +329,7 @@
|
||||
"Default Latency Test Info": "Used for HTTP client request testing only and won't make a difference to the configuration file",
|
||||
"Default Latency Timeout": "Default Latency Timeout",
|
||||
"Hotkey Setting": "Hotkey Setting",
|
||||
"Enable Global Hotkey": "Enable Global Hotkey",
|
||||
"open_or_close_dashboard": "Open/Close Dashboard",
|
||||
"clash_mode_rule": "Rule Mode",
|
||||
"clash_mode_global": "Global Mode",
|
||||
@@ -437,5 +439,7 @@
|
||||
"Rule Mode": "Rule Mode",
|
||||
"Global Mode": "Global Mode",
|
||||
"Direct Mode": "Direct Mode",
|
||||
"Enable Tray Speed": "Enable Tray Speed"
|
||||
"Enable Tray Speed": "Enable Tray Speed",
|
||||
"Lite Mode": "Lightweight Mode",
|
||||
"Lite Mode Info": "Close the GUI and keep only the kernel running"
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "فرآیند",
|
||||
"Time": "زمان",
|
||||
"Source": "منبع",
|
||||
"Destination IP": "آدرس IP مقصد",
|
||||
"Destination": "آدرس IP مقصد",
|
||||
"DestinationPort": "بندر هدف",
|
||||
"Close Connection": "بستن اتصال",
|
||||
"Rules": "قوانین",
|
||||
"Rule Provider": "تأمینکننده قانون",
|
||||
@@ -328,6 +329,7 @@
|
||||
"Default Latency Test Info": "فقط برای تست درخواستهای کلاینت HTTP استفاده میشود و بر فایل پیکربندی تأثیری نخواهد داشت",
|
||||
"Default Latency Timeout": "زمان انتظار تأخیر پیشفرض",
|
||||
"Hotkey Setting": "تنظیمات کلیدهای میانبر",
|
||||
"Enable Global Hotkey": "فعال کردن کلید میانبر سراسری",
|
||||
"open_or_close_dashboard": "باز/بستن داشبورد",
|
||||
"clash_mode_rule": "حالت قانون",
|
||||
"clash_mode_global": "حالت جهانی",
|
||||
@@ -431,5 +433,7 @@
|
||||
"Rule Mode": "حالت قوانین",
|
||||
"Global Mode": "حالت جهانی",
|
||||
"Direct Mode": "حالت مستقیم",
|
||||
"Enable Tray Speed": "فعال کردن سرعت ترای"
|
||||
"Enable Tray Speed": "فعال کردن سرعت ترای",
|
||||
"Lite Mode": "در فارسی",
|
||||
"Lite Mode Info": "رابط کاربری گرافیکی را ببندید و فقط هسته را در حال اجرا نگه دارید"
|
||||
}
|
||||
|
||||
+6
-2
@@ -210,7 +210,8 @@
|
||||
"Process": "Proses",
|
||||
"Time": "Waktu",
|
||||
"Source": "Sumber",
|
||||
"Destination IP": "IP Tujuan",
|
||||
"Destination": "IP Tujuan",
|
||||
"DestinationPort": "Port Tujuan",
|
||||
"Close Connection": "Tutup Koneksi",
|
||||
"Rules": "Aturan",
|
||||
"Rule Provider": "Penyedia Aturan",
|
||||
@@ -359,6 +360,7 @@
|
||||
"Default Latency Test Info": "Digunakan hanya untuk pengujian permintaan klien HTTP dan tidak akan mempengaruhi file konfigurasi",
|
||||
"Default Latency Timeout": "Waktu Habis Latensi Default",
|
||||
"Hotkey Setting": "Pengaturan Pintasan",
|
||||
"Enable Global Hotkey": "Aktifkan Hotkey Global",
|
||||
"open_or_close_dashboard": "Buka/Tutup Dasbor",
|
||||
"clash_mode_rule": "Mode Aturan",
|
||||
"clash_mode_global": "Mode Global",
|
||||
@@ -430,5 +432,7 @@
|
||||
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
|
||||
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
|
||||
"Failed to fetch backup files": "Gagal mengambil file cadangan",
|
||||
"Enable Tray Speed": "Aktifkan Tray Speed"
|
||||
"Enable Tray Speed": "Aktifkan Tray Speed",
|
||||
"Lite Mode": "Mode Ringan",
|
||||
"Lite Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan"
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "Процесс",
|
||||
"Time": "Время подключения",
|
||||
"Source": "Исходный адрес",
|
||||
"Destination IP": "IP-адрес назначения",
|
||||
"Destination": "IP-адрес назначения",
|
||||
"DestinationPort": "Целевой порт",
|
||||
"Close Connection": "Закрыть соединение",
|
||||
"Rules": "Правила",
|
||||
"Rule Provider": "Провайдер правило",
|
||||
@@ -328,6 +329,7 @@
|
||||
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
||||
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
||||
"Hotkey Setting": "Настройки клавиатурных сокращений",
|
||||
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
|
||||
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
"clash_mode_global": "Глобальный режим",
|
||||
@@ -431,5 +433,7 @@
|
||||
"Rule Mode": "Режим правил",
|
||||
"Global Mode": "Глобальный режим",
|
||||
"Direct Mode": "Прямой режим",
|
||||
"Enable Tray Speed": "Включить скорость в лотке"
|
||||
"Enable Tray Speed": "Включить скорость в лотке",
|
||||
"Lite Mode": "Облегченный режим",
|
||||
"Lite Mode Info": "Закройте графический интерфейс и оставьте работать только ядро"
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "Процесс",
|
||||
"Time": "Тоташу вакыты",
|
||||
"Source": "Чыганак адресы",
|
||||
"Destination IP": "Максат IP-адресы",
|
||||
"Destination": "Максат IP-адресы",
|
||||
"DestinationPort": "Барасы порты",
|
||||
"Close Connection": "Тоташуны ябу",
|
||||
"Rules": "Кагыйдәләр",
|
||||
"Rule Provider": "Кагыйдә провайдеры",
|
||||
@@ -327,6 +328,7 @@
|
||||
"Default Latency Test Info": "Бу фәкать клиентның HTTP сораулары тесты өчен кулланыла, конфигурация файлына йогынты ясамый",
|
||||
"Default Latency Timeout": "Тоткарлануның стандарт таймауты",
|
||||
"Hotkey Setting": "Клавиатура төймәләре (hotkey) көйләүләре",
|
||||
"Enable Global Hotkey": "Глобаль Хоткейны кушу",
|
||||
"open_or_close_dashboard": "Панельне ачу/ябу",
|
||||
"clash_mode_rule": "Кагыйдәләр режимы",
|
||||
"clash_mode_global": "Глобаль режим",
|
||||
@@ -430,5 +432,7 @@
|
||||
"Rule Mode": "Кагыйдә режимы",
|
||||
"Global Mode": "Глобаль режим",
|
||||
"Direct Mode": "Туры режим",
|
||||
"Enable Tray Speed": "Трей скоростьне үстерү"
|
||||
"Enable Tray Speed": "Трей скоростьне үстерү",
|
||||
"Lite Mode": "Җиңел Режим",
|
||||
"Lite Mode Info": "GUI-ны ябыгыз һәм бары тик төшне генә эшләтеп калдырыгыз"
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,8 @@
|
||||
"Process": "进程",
|
||||
"Time": "连接时间",
|
||||
"Source": "源地址",
|
||||
"Destination IP": "目标地址",
|
||||
"Destination": "目标地址",
|
||||
"DestinationPort": "目标端口",
|
||||
"Close Connection": "关闭连接",
|
||||
"Rules": "规则",
|
||||
"Rule Provider": "规则集合",
|
||||
@@ -328,6 +329,7 @@
|
||||
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
|
||||
"Default Latency Timeout": "测试超时时间",
|
||||
"Hotkey Setting": "热键设置",
|
||||
"Enable Global Hotkey": "启用全局热键",
|
||||
"open_or_close_dashboard": "打开/关闭面板",
|
||||
"clash_mode_rule": "规则模式",
|
||||
"clash_mode_global": "全局模式",
|
||||
@@ -431,5 +433,7 @@
|
||||
"Rule Mode": "规则模式",
|
||||
"Global Mode": "全局模式",
|
||||
"Direct Mode": "直连模式",
|
||||
"Enable Tray Speed": "启用托盘速率"
|
||||
"Enable Tray Speed": "启用托盘速率",
|
||||
"Lite Mode": "轻量模式",
|
||||
"Lite Mode Info": "关闭GUI界面,仅保留内核运行"
|
||||
}
|
||||
|
||||
+25
-8
@@ -27,6 +27,7 @@ import { getPortableFlag } from "@/services/cmds";
|
||||
import React from "react";
|
||||
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
export let portableFlag = false;
|
||||
@@ -91,6 +92,24 @@ const Layout = () => {
|
||||
await appWindow.show();
|
||||
await appWindow.setFocus();
|
||||
}, 50);
|
||||
|
||||
// 监听窗口显示/隐藏事件
|
||||
const setupListeners = async () => {
|
||||
const unlisten1 = await listen("verge://hide-window", () => {
|
||||
appWindow.hide();
|
||||
});
|
||||
|
||||
const unlisten2 = await listen("verge://show-window", () => {
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten1();
|
||||
unlisten2();
|
||||
};
|
||||
};
|
||||
|
||||
setupListeners();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -128,14 +147,12 @@ const Layout = () => {
|
||||
({ palette }) => ({
|
||||
bgcolor: palette.background.paper,
|
||||
}),
|
||||
OS === "linux"
|
||||
? {
|
||||
borderRadius: "8px",
|
||||
border: "2px solid var(--divider-color)",
|
||||
width: "calc(100vw - 4px)",
|
||||
height: "calc(100vh - 4px)",
|
||||
}
|
||||
: {},
|
||||
{
|
||||
borderRadius: "8px",
|
||||
border: "2px solid var(--divider-color)",
|
||||
width: "calc(100vw - 4px)",
|
||||
height: "calc(100vh - 4px)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="layout__left">
|
||||
|
||||
+36
-29
@@ -139,7 +139,15 @@ const ConnectionsPage = () => {
|
||||
<BasePage
|
||||
full
|
||||
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
|
||||
contentStyle={{ height: "100%" }}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
// margin: "0 10px",
|
||||
// backgroundColor: isDark ? "#282a36" : "#ffffff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ mx: 1 }}>
|
||||
@@ -185,6 +193,10 @@ const ConnectionsPage = () => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
userSelect: "text",
|
||||
// backgroundColor: isDark ? "#282a36" : "#ffffff",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{!isTableLayout && (
|
||||
@@ -202,34 +214,29 @@ const ConnectionsPage = () => {
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
height="calc(100% - 65px)"
|
||||
sx={{
|
||||
userSelect: "text",
|
||||
margin: "10px",
|
||||
borderRadius: "8px",
|
||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
||||
}}
|
||||
>
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(_, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
style={{
|
||||
flex: 1,
|
||||
// backgroundColor: isDark ? "#282a36" : "#ffffff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
data={filterConn}
|
||||
itemContent={(_, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
+21
-22
@@ -44,7 +44,12 @@ const LogPage = () => {
|
||||
<BasePage
|
||||
full
|
||||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
}}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
@@ -102,27 +107,21 @@ const LogPage = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
height="calc(100% - 65px)"
|
||||
sx={{
|
||||
margin: "10px",
|
||||
borderRadius: "8px",
|
||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
||||
}}
|
||||
>
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
itemContent={(index, item) => (
|
||||
<LogItem value={item} searchState={searchState} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</Box>
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
itemContent={(index, item) => (
|
||||
<LogItem value={item} searchState={searchState} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
+42
-43
@@ -367,20 +367,19 @@ const ProfilePage = () => {
|
||||
{t("New")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
pl: "10px",
|
||||
mr: "10px",
|
||||
height: "calc(100% - 68px)",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
<Box
|
||||
sx={{
|
||||
pl: "10px",
|
||||
pr: "10px",
|
||||
height: "calc(100% - 68px)",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||
@@ -412,38 +411,38 @@ const ProfilePage = () => {
|
||||
</SortableContext>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</DndContext>
|
||||
<Divider
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||
></Divider>
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Merge"
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Divider
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||
></Divider>
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Merge"
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Script"
|
||||
logInfo={chainLogs["Script"]}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Script"
|
||||
logInfo={chainLogs["Script"]}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DndContext>
|
||||
|
||||
<ProfileViewer
|
||||
ref={viewerRef}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Button, ButtonGroup } from "@mui/material";
|
||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { patchClashConfig, patchClashMode } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||
@@ -29,7 +29,7 @@ const ProxyPage = () => {
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
await patchClashConfig({ mode });
|
||||
await patchClashMode(mode);
|
||||
mutateClash();
|
||||
});
|
||||
|
||||
|
||||
+27
-29
@@ -39,7 +39,12 @@ const RulesPage = () => {
|
||||
<BasePage
|
||||
full
|
||||
title={t("Rules")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
}}
|
||||
header={
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
@@ -59,34 +64,27 @@ const RulesPage = () => {
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
height="calc(100% - 65px)"
|
||||
sx={{
|
||||
margin: "10px",
|
||||
borderRadius: "8px",
|
||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rules.length > 0 ? (
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
scrollerRef={(ref) => {
|
||||
if (ref) ref.addEventListener("scroll", handleScroll);
|
||||
}}
|
||||
/>
|
||||
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
||||
</>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</Box>
|
||||
{rules.length > 0 ? (
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={rules}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
scrollerRef={(ref) => {
|
||||
if (ref) ref.addEventListener("scroll", handleScroll);
|
||||
}}
|
||||
/>
|
||||
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
||||
</>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,6 +91,10 @@ export async function patchClashConfig(payload: Partial<IConfigData>) {
|
||||
return invoke<void>("patch_clash_config", { payload });
|
||||
}
|
||||
|
||||
export async function patchClashMode(payload: String) {
|
||||
return invoke<void>("patch_clash_mode", { payload });
|
||||
}
|
||||
|
||||
export async function getVergeConfig() {
|
||||
return invoke<IVergeConfig>("get_verge_config");
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -120,6 +120,7 @@ interface IConnectionsItem {
|
||||
sourcePort: string;
|
||||
destinationPort: string;
|
||||
destinationIP?: string;
|
||||
remoteDestination?: string;
|
||||
process?: string;
|
||||
processPath?: string;
|
||||
};
|
||||
@@ -708,9 +709,11 @@ interface IVergeConfig {
|
||||
tun_tray_icon?: boolean;
|
||||
enable_tray_speed?: boolean;
|
||||
enable_tun_mode?: boolean;
|
||||
enable_lite_mode?: boolean;
|
||||
enable_auto_launch?: boolean;
|
||||
enable_silent_start?: boolean;
|
||||
enable_system_proxy?: boolean;
|
||||
enable_global_hotkey?: boolean;
|
||||
proxy_auto_config?: boolean;
|
||||
pac_file_content?: string;
|
||||
enable_random_port?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user