Compare commits

..
Author SHA1 Message Date
wonfen 396a3e81d8 fix: alpha build_2 2025-02-17 09:22:02 +08:00
wonfen 17fd1248d0 fix: alpha build 2025-02-17 06:50:52 +08:00
Akioe Yu 0190702616 fix: check script error (#2453)
(cherry picked from commit 97bde64fd6516019a190d52f05af54ade623d57c)
2025-02-17 03:10:19 +08:00
wonfen c35bf38420 chore: Add macOS-specific test for format_bytes_speed function 2025-02-17 03:10:03 +08:00
wonfen 89d20e564a Revert "perf: Improve kernel management logic & add more dev mode logs"
This reverts commit 18c7ed1ccc.
2025-02-15 05:51:46 +08:00
Christine. 3c24ff3afc add: i18n text for lightweight mode (#2692) 2025-02-13 14:27:12 +08:00
Tunglies daf0398750 fix: build front cannot find IvergeConfig.enable_lite_mode and macos port switching causes crash (#2691)
* fix: macos switch protocol port or enable protocol port causes crash

* fix: build time front cannot find IVergeConfig attribute enable_lite_mode
2025-02-12 18:14:13 -08:00
wonfen 6b14c2b763 feat: add lightweight mode 2025-02-13 02:18:17 +08:00
wonfen 5bf2f9b8ed fix(macos): add missing required dependencies 2025-02-12 15:06:42 +08:00
wonfen 10a2655288 fix: remove unused imports to resolve compile warnings 2025-02-12 14:35:49 +08:00
wonfen ac07397818 refactor: improve hotkey management, logging, and error handling; fix tray freeze and hotkey failure on silent startup 2025-02-12 14:23:42 +08:00
wonfen 30d061d00f fix(tray): resolve blank icon issue on Windows and optimize creation logic 2025-02-12 13:56:33 +08:00
Christine. 360d8a5143 fix: use remoteDestination replace DestinationIP in connection page, #2668 (#2679)
* fix: use remoteDestination replace DestinationIP  in connection page, #2668

* add: missing i18n text

* fix: display the target address details in connection page
2025-02-11 14:52:27 +08:00
wonfen 70e0a5adc8 refactor: Improve tray icon and event handling across platforms & unify click behavior 2025-02-11 14:48:31 +08:00
wonfen ef52f81494 fix: app crash without a default global_hotkey vaule 2025-02-11 01:50:48 +08:00
wonfen e5c1de3ad3 fix: deprecated CLI warnings 2025-02-11 01:05:51 +08:00
wonfen 576fd700ae feat: Add independent alpha update channel 2025-02-11 00:53:44 +08:00
wonfen 476df65a1b fix: try to fix linux CI updater problem 2025-02-11 00:53:44 +08:00
Christine. bd0a4863a8 add: i18n text (#2675) 2025-02-10 00:29:43 -08:00
wonfen d25fbc05e2 perf: Optimize kernel shutdown speed & logic 2025-02-10 13:18:00 +08:00
wonfen b8a0b6f1f4 fix: crash caused by global_hotkey 2025-02-10 12:39:07 +08:00
wonfen 18c7ed1ccc perf: Improve kernel management logic & add more dev mode logs 2025-02-09 14:19:15 +08:00
Tunglies b14db06955 perf: imporve clash mode switching performance on the main window (#2667) 2025-02-09 07:45:46 +08:00
Tunglies 215dcee3f1 feat: option to enable global hotkey (#2665) 2025-02-09 07:45:22 +08:00
Tunglies 37e5c22a5a fix: duplicate checked tray menu when profile name are same (#2660) 2025-02-08 09:36:04 +08:00
wonfen bb0b6c3f77 feat: Optimize UI layout and page hierarchy 2025-02-07 09:13:30 +08:00
wonfen a37df46ce7 perf: optimize node latency refresh rate for faster updates 2025-02-06 07:58:42 +08:00
wonfen 73fbec2514 chore: update deps 2025-02-05 14:19:43 +08:00
Tunglies f66fa08b2c Feature: Switch Proxy Profile from Tray Menu (#2644) 2025-02-05 08:52:47 +08:00
wonfen bae606bc9d fix: restore hotkey functionality after silent startup 2025-02-02 11:37:10 +08:00
wonfen 1a2d5e988a feat: allow users to customize enhanced-mode and fakeip-range in tun mode by wonfen 2025-02-02 05:12:16 +08:00
wonfen d20bd62b90 style: refine tray speed display 2025-02-02 03:43:15 +08:00
wonfen 204bf43b9a fix: windows rounded corners 2025-01-19 09:47:43 +08:00
wonfen 3ea0d20e2c add donation 2025-01-18 16:08:24 +08:00
47 changed files with 2897 additions and 2284 deletions
+7 -8
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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"
},
+1111 -1207
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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, {
+807 -752
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -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
View File
@@ -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();
+19 -1
View File
@@ -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,
}
}
}
+14
View File
@@ -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 -1
View File
@@ -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};
+59
View File
@@ -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(())
}
}
+81 -9
View File
@@ -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(())
}
+170
View File
@@ -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();
}
}
}
+94 -35
View File
@@ -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());
}
_ => {}
}
+28 -19
View File
@@ -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,
+25 -9
View File
@@ -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
View File
@@ -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 {
+14
View File
@@ -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")]
+1
View File
@@ -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");
+36 -12
View File
@@ -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<()> {
+12 -8
View File
@@ -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": {
+40
View File
@@ -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
}
}
}
+2 -1
View File
@@ -33,7 +33,8 @@
},
"app": {
"trayIcon": {
"iconPath": "icons/tray-icon.ico"
"iconPath": "icons/tray-icon.ico",
"iconAsTemplate": true
}
}
}
-8
View File
@@ -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)}
+5 -1
View File
@@ -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();
+18 -3
View File
@@ -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>
+10 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+2 -2
View File
@@ -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
View File
@@ -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>
);
};
+4
View File
@@ -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");
}
+3
View File
@@ -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;