Compare commits

...
34 Commits
Author SHA1 Message Date
GyDi afa244dcb0 v1.1.2 2022-11-02 00:54:51 +08:00
GyDi 4ea5bb2390 fix: check remote profile 2022-11-02 00:51:25 +08:00
GyDi e545d552f6 chore: update log 2022-11-02 00:44:45 +08:00
GyDi 56fe7b3596 feat: add version on tray 2022-11-01 23:29:59 +08:00
GyDi eb28ec866a feat: add animation 2022-10-29 23:36:10 +08:00
GyDi 4649454282 fix: remove smoother 2022-10-29 20:05:55 +08:00
angrylid a45dc6efda feat: add animation to ProfileNew component (#252)
* chore: add .vscode to .gitignore

* feat: add animation to ProfileNew component
2022-10-29 20:02:51 +08:00
GyDi 9e56b9fbb5 fix: icon button color 2022-10-29 19:22:15 +08:00
GyDi 5504994cb9 feat: check remote profile field 2022-10-28 13:00:52 +08:00
GyDi acff6d0432 fix: init system proxy correctly 2022-10-28 01:33:21 +08:00
GyDi eea9cb7c5b fix: open file 2022-10-28 01:26:45 +08:00
GyDi c0ddddfb1f fix: reset proxy 2022-10-28 01:06:54 +08:00
GyDi f7dab3ca56 fix: init config error 2022-10-28 01:02:47 +08:00
GyDi e11b4038a3 feat: system tray support zh language 2022-10-28 00:40:29 +08:00
GyDi b635e64803 fix: adjust reset proxy 2022-10-27 21:14:14 +08:00
GyDi 20a194b49a fix: adjust code 2022-10-27 21:13:27 +08:00
GyDi 33a5fb8837 fix: add https proxy 2022-10-26 17:11:54 +08:00
GyDi 59dae640db fix: auto scroll into view when sorted proxies changed 2022-10-26 01:24:06 +08:00
GyDi a6ac75e97b feat: display delay check result timely 2022-10-26 01:11:02 +08:00
GyDi cc5b33a8ec fix: refresh proxies interval, close #235 2022-10-26 01:08:34 +08:00
GyDi 6e1a627b84 fix: style 2022-10-25 23:54:21 +08:00
GyDi 62d4c65e1c chore: update deps 2022-10-25 23:54:10 +08:00
GyDi 600134a3ac fix: fetch profile with system proxy, close #249 2022-10-25 23:45:24 +08:00
LooSheng df14af7337 fix: The profile is replaced when the request fails. (#246) 2022-10-23 17:22:26 +08:00
GyDi 2f740b570d fix: default dns config 2022-10-22 22:26:40 +08:00
GyDi 294d980b52 fix: kill clash when exit in service mode, close #241 2022-10-22 13:55:06 +08:00
GyDi c9d9909d74 chore: update mmdb 2022-10-22 13:30:08 +08:00
GyDi 90eeabae7b feat: update profile with system proxy/clash proxy 2022-10-18 23:19:21 +08:00
GyDi a32c77c5f1 chore: adjust code 2022-10-17 23:26:25 +08:00
GyDi 910846f2ce fix: icon button color inherit 2022-10-16 14:40:45 +08:00
GyDi 35d0438261 fix: app version to string 2022-10-16 14:38:57 +08:00
GyDi 515af472ce fix: break loop when core terminated 2022-10-14 14:46:15 +08:00
GyDi f062f7f9fe feat: change global mode ui, close #226 2022-10-13 11:54:52 +08:00
GyDi f68378041f feat: default user agent same with app version 2022-10-12 11:28:47 +08:00
39 changed files with 743 additions and 454 deletions
+1
View File
@@ -5,3 +5,4 @@ dist-ssr
*.local
update.json
scripts/_env.sh
.vscode
+18
View File
@@ -1,3 +1,21 @@
## v1.1.2
### Features
- the system tray follows i18n
- change the proxy group ui of global mode
- support to update profile with the system proxy/clash proxy
- check the remote profile more strictly
### Bug Fixes
- use app version as default user agent
- the clash not exit in service mode
- reset the system proxy when quit the app
- fix some other glitches
---
## v1.1.1
### Features
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.1.1",
"version": "1.1.2",
"license": "GPL-3.0",
"scripts": {
"dev": "tauri dev",
@@ -33,7 +33,7 @@
"react-dom": "^17.0.2",
"react-i18next": "^11.17.4",
"react-router-dom": "^6.3.0",
"react-virtuoso": "^2.17.2",
"react-virtuoso": "^3.1.0",
"recoil": "^0.7.5",
"snarkdown": "^2.0.0",
"swr": "^1.3.0"
+1 -1
View File
@@ -260,7 +260,7 @@ async function resolveService() {
*/
async function resolveMmdb() {
const url =
"https://github.com/Dreamacro/maxmind-geoip/releases/download/20220812/Country.mmdb";
"https://github.com/Dreamacro/maxmind-geoip/releases/download/20221012/Country.mmdb";
const resDir = path.join(cwd, "src-tauri", "resources");
const resPath = path.join(resDir, "Country.mmdb");
+1 -1
View File
@@ -57,8 +57,8 @@ fn use_dns_for_tun(mut config: Mapping) -> Mapping {
// 开启tun将同时开启dns
revise!(dns_val, "enable", true);
// 借鉴cfw的默认配置
append!(dns_val, "enhanced-mode", "fake-ip");
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
append!(
dns_val,
"nameserver",
+11 -52
View File
@@ -1,7 +1,6 @@
use crate::data::*;
use super::tray::Tray;
use crate::log_if_err;
use anyhow::{bail, Result};
use serde_yaml::Value;
use tauri::{AppHandle, Manager, Window};
#[derive(Debug, Default, Clone)]
@@ -46,62 +45,22 @@ impl Handle {
}
}
// update system tray state (clash config)
pub fn update_systray_clash(&self) -> Result<()> {
if self.app_handle.is_none() {
bail!("update_systray_clash unhandle error");
}
let app_handle = self.app_handle.as_ref().unwrap();
let global = Data::global();
let clash = global.clash.lock();
let mode = clash
.config
.get(&Value::from("mode"))
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule");
let tray = app_handle.tray_handle();
tray.get_item("rule_mode").set_selected(mode == "rule")?;
tray
.get_item("global_mode")
.set_selected(mode == "global")?;
tray
.get_item("direct_mode")
.set_selected(mode == "direct")?;
tray
.get_item("script_mode")
.set_selected(mode == "script")?;
Ok(())
}
/// update the system tray state (verge config)
pub fn update_systray(&self) -> Result<()> {
if self.app_handle.is_none() {
bail!("update_systray unhandle error");
}
let app_handle = self.app_handle.as_ref().unwrap();
let tray = app_handle.tray_handle();
let global = Data::global();
let verge = global.verge.lock();
let system_proxy = verge.enable_system_proxy.as_ref();
let tun_mode = verge.enable_tun_mode.as_ref();
tray
.get_item("system_proxy")
.set_selected(*system_proxy.unwrap_or(&false))?;
tray
.get_item("tun_mode")
.set_selected(*tun_mode.unwrap_or(&false))?;
// update verge config
self.refresh_verge();
Tray::update_systray(app_handle)?;
Ok(())
}
/// update the system tray state
pub fn update_systray_part(&self) -> Result<()> {
if self.app_handle.is_none() {
bail!("update_systray unhandle error");
}
let app_handle = self.app_handle.as_ref().unwrap();
Tray::update_part(app_handle)?;
Ok(())
}
}
+22 -17
View File
@@ -6,7 +6,7 @@ use crate::config::enhance_config;
use crate::data::*;
use crate::log_if_err;
use anyhow::{bail, Result};
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use serde_yaml::{Mapping, Value};
use std::sync::Arc;
@@ -16,18 +16,10 @@ mod hotkey;
mod service;
mod sysopt;
mod timer;
pub mod tray;
pub use self::service::*;
static CORE: Lazy<Core> = Lazy::new(|| Core {
service: Arc::new(Mutex::new(Service::new())),
sysopt: Arc::new(Mutex::new(Sysopt::new())),
timer: Arc::new(Mutex::new(Timer::new())),
hotkey: Arc::new(Mutex::new(Hotkey::new())),
runtime: Arc::new(Mutex::new(RuntimeResult::default())),
handle: Arc::new(Mutex::new(Handle::default())),
});
#[derive(Clone)]
pub struct Core {
pub service: Arc<Mutex<Service>>,
@@ -39,8 +31,17 @@ pub struct Core {
}
impl Core {
pub fn global() -> Core {
CORE.clone()
pub fn global() -> &'static Core {
static CORE: OnceCell<Core> = OnceCell::new();
CORE.get_or_init(|| Core {
service: Arc::new(Mutex::new(Service::new())),
sysopt: Arc::new(Mutex::new(Sysopt::new())),
timer: Arc::new(Mutex::new(Timer::new())),
hotkey: Arc::new(Mutex::new(Hotkey::new())),
runtime: Arc::new(Mutex::new(RuntimeResult::default())),
handle: Arc::new(Mutex::new(Handle::default())),
})
}
/// initialize the core state
@@ -64,8 +65,7 @@ impl Core {
drop(sysopt);
let handle = self.handle.lock();
log_if_err!(handle.update_systray());
log_if_err!(handle.update_systray_clash());
log_if_err!(handle.update_systray_part());
drop(handle);
let mut hotkey = self.hotkey.lock();
@@ -136,7 +136,7 @@ impl Core {
if has_mode {
let handle = self.handle.lock();
handle.update_systray_clash()?;
handle.update_systray_part()?;
}
Ok(())
@@ -155,6 +155,7 @@ impl Core {
let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let proxy_guard = patch.enable_proxy_guard;
let language = patch.language;
#[cfg(target_os = "windows")]
{
@@ -197,9 +198,13 @@ impl Core {
sysopt.guard_proxy();
}
if system_proxy.is_some() || tun_mode.is_some() {
// 更新tray
if language.is_some() {
let handle = self.handle.lock();
handle.update_systray()?;
} else if system_proxy.is_some() || tun_mode.is_some() {
let handle = self.handle.lock();
handle.update_systray_part()?;
}
if patch.hotkeys.is_some() {
@@ -232,7 +237,7 @@ impl Core {
// update tray
let handle = handle.lock();
handle.refresh_clash();
log_if_err!(handle.update_systray_clash());
log_if_err!(handle.update_systray_part());
});
Ok(())
+10 -8
View File
@@ -22,6 +22,9 @@ pub struct Service {
sidecar: Option<CommandChild>,
logs: Arc<RwLock<VecDeque<String>>>,
#[allow(unused)]
use_service_mode: bool,
}
impl Service {
@@ -31,6 +34,7 @@ impl Service {
Service {
sidecar: None,
logs: Arc::new(RwLock::new(queue)),
use_service_mode: false,
}
}
@@ -46,6 +50,8 @@ impl Service {
verge.enable_service_mode.clone().unwrap_or(false)
};
self.use_service_mode = enable;
if !enable {
return self.start_clash_by_sidecar();
}
@@ -74,14 +80,8 @@ impl Service {
{
let _ = self.stop_clash_by_sidecar();
let enable = {
let data = Data::global();
let verge = data.verge.lock();
verge.enable_service_mode.clone().unwrap_or(false)
};
if enable {
tauri::async_runtime::spawn(async move {
if self.use_service_mode {
tauri::async_runtime::block_on(async move {
log_if_err!(Self::stop_clash_by_service().await);
});
}
@@ -172,6 +172,8 @@ impl Service {
log::error!(target: "app" ,"[clash error]: {}", err);
write_log(err);
}
CommandEvent::Error(err) => log::error!(target: "app" ,"{err}"),
CommandEvent::Terminated(_) => break,
_ => {}
}
}
+21 -22
View File
@@ -68,14 +68,14 @@ impl Sysopt {
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
}
// launchs the system proxy guard
// run the system proxy guard
self.guard_proxy();
Ok(())
}
/// update the system proxy
pub fn update_sysproxy(&mut self) -> Result<()> {
if self.cur_sysproxy.is_none() {
if self.cur_sysproxy.is_none() || self.old_sysproxy.is_none() {
return self.init_sysproxy();
}
@@ -99,29 +99,28 @@ impl Sysopt {
/// reset the sysproxy
pub fn reset_sysproxy(&mut self) -> Result<()> {
if self.cur_sysproxy.is_none() {
return Ok(());
}
let cur = self.cur_sysproxy.take();
let mut cur = self.cur_sysproxy.take().unwrap();
if let Some(mut old) = self.old_sysproxy.take() {
// 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur.map_or(true, |cur| old.port == cur.port);
match self.old_sysproxy.take() {
Some(old) => {
// 如果原代理设置和当前的设置是一样的,需要关闭
// 否则就恢复原代理设置
if old.enable && old.host == cur.host && old.port == cur.port {
cur.enable = false;
cur.set_system_proxy()?;
} else {
old.set_system_proxy()?;
}
}
None => {
if cur.enable {
cur.enable = false;
cur.set_system_proxy()?;
}
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur {
// 没有原代理,就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
+124
View File
@@ -0,0 +1,124 @@
use crate::{data::Data, feat, utils::resolve};
use anyhow::{Ok, Result};
use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
};
pub struct Tray {}
impl Tray {
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
let data = Data::global();
let zh = {
let verge = data.verge.lock();
verge.language == Some("zh".into())
};
let version = app_handle.package_info().version.to_string();
if zh {
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "打开面板"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("rule_mode", "规则模式"))
.add_item(CustomMenuItem::new("global_mode", "全局模式"))
.add_item(CustomMenuItem::new("direct_mode", "直连模式"))
.add_item(CustomMenuItem::new("script_mode", "脚本模式"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("system_proxy", "系统代理"))
.add_item(CustomMenuItem::new("tun_mode", "TUN 模式"))
.add_submenu(SystemTraySubmenu::new(
"更多",
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("restart_clash", "重启 Clash"))
.add_item(CustomMenuItem::new("restart_app", "重启应用"))
.add_item(CustomMenuItem::new("app_version", format!("Version {version}")).disabled()),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"))
} else {
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "Dashboard"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
.add_item(CustomMenuItem::new("global_mode", "Global Mode"))
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
.add_item(CustomMenuItem::new("script_mode", "Script Mode"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
.add_submenu(SystemTraySubmenu::new(
"More",
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
.add_item(CustomMenuItem::new("restart_app", "Restart App"))
.add_item(CustomMenuItem::new("app_version", format!("Version {version}")).disabled()),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
}
}
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
app_handle
.tray_handle()
.set_menu(Tray::tray_menu(app_handle))?;
Tray::update_part(app_handle)?;
Ok(())
}
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
let global = Data::global();
let clash = global.clash.lock();
let mode = clash
.config
.get(&serde_yaml::Value::from("mode"))
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule");
let tray = app_handle.tray_handle();
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
let _ = tray.get_item("script_mode").set_selected(mode == "script");
let verge = global.verge.lock();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
Ok(())
}
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode);
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()),
"quit" => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
_ => {}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
}
_ => {}
}
}
}
+9 -9
View File
@@ -8,16 +8,10 @@ pub use self::prfitem::*;
pub use self::profiles::*;
pub use self::verge::*;
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::Arc;
static DATA: Lazy<Data> = Lazy::new(|| Data {
clash: Arc::new(Mutex::new(Clash::new())),
verge: Arc::new(Mutex::new(Verge::new())),
profiles: Arc::new(Mutex::new(Profiles::new())),
});
#[derive(Debug, Clone)]
pub struct Data {
pub clash: Arc<Mutex<Clash>>,
@@ -26,7 +20,13 @@ pub struct Data {
}
impl Data {
pub fn global() -> Data {
DATA.clone()
pub fn global() -> &'static Data {
static DATA: OnceCell<Data> = OnceCell::new();
DATA.get_or_init(|| Data {
clash: Arc::new(Mutex::new(Clash::new())),
verge: Arc::new(Mutex::new(Verge::new())),
profiles: Arc::new(Mutex::new(Profiles::new())),
})
}
}
+72 -38
View File
@@ -1,8 +1,10 @@
use crate::utils::{config, dirs, help, tmpl};
use anyhow::{bail, Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::fs;
use sysproxy::Sysproxy;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrfItem {
@@ -69,39 +71,31 @@ pub struct PrfOption {
pub user_agent: Option<String>,
/// for `remote` profile
/// use system proxy
#[serde(skip_serializing_if = "Option::is_none")]
pub with_proxy: Option<bool>,
/// for `remote` profile
/// use self proxy
#[serde(skip_serializing_if = "Option::is_none")]
pub self_proxy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_interval: Option<u64>,
}
impl PrfOption {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
if one.is_none() {
return other;
match (one, other) {
(Some(mut a), Some(b)) => {
a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy);
a.update_interval = b.update_interval.or(a.update_interval);
Some(a)
}
t @ _ => t.0.or(t.1),
}
if one.is_some() && other.is_some() {
let mut one = one.unwrap();
let other = other.unwrap();
if let Some(val) = other.user_agent {
one.user_agent = Some(val);
}
if let Some(val) = other.with_proxy {
one.with_proxy = Some(val);
}
if let Some(val) = other.update_interval {
one.update_interval = Some(val);
}
return Some(one);
}
return one;
}
}
@@ -189,27 +183,64 @@ impl PrfItem {
desc: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let with_proxy = match option.as_ref() {
Some(opt) => opt.with_proxy.unwrap_or(false),
None => false,
};
let user_agent = match option.as_ref() {
Some(opt) => opt.user_agent.clone(),
None => None,
};
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
let mut builder = reqwest::ClientBuilder::new();
let mut builder = reqwest::ClientBuilder::new().no_proxy();
if !with_proxy {
builder = builder.no_proxy();
// 使用软件自己的代理
if self_proxy {
let data = super::Data::global();
let port = data.clash.lock().info.port.clone();
let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?;
let proxy_scheme = format!("http://127.0.0.1:{port}");
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
// 使用系统代理
else if with_proxy {
match Sysproxy::get_system_proxy() {
Ok(p @ Sysproxy { enable: true, .. }) => {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
_ => {}
};
}
builder = builder.user_agent(user_agent.unwrap_or("clash-verge/v1.0.0".into()));
let version = unsafe { dirs::APP_VERSION };
let version = format!("clash-verge/{version}");
builder = builder.user_agent(user_agent.unwrap_or(version));
let resp = builder.build()?.get(url).send().await?;
let status_code = resp.status();
if !StatusCode::is_success(&status_code) {
bail!("failed to fetch remote profile with status {status_code}")
}
let header = resp.headers();
// parse the Subscription Userinfo
// parse the Subscription UserInfo
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
@@ -251,8 +282,11 @@ impl PrfItem {
let data = resp.text_with_charset("utf-8").await?;
// check the data whether the valid yaml format
if !serde_yaml::from_str::<Mapping>(&data).is_ok() {
bail!("the remote profile data is invalid yaml");
let yaml = serde_yaml::from_str::<Mapping>(&data) //
.context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
bail!("profile does not contain `proxies` or `proxy-providers`");
}
Ok(PrfItem {
+9
View File
@@ -2,6 +2,15 @@ use crate::core::*;
use crate::data::*;
use crate::log_if_err;
// 重启clash
pub fn restart_clash_core() {
let core = Core::global();
let mut service = core.service.lock();
log_if_err!(service.restart());
drop(service);
log_if_err!(core.activate());
}
// 切换模式
pub fn change_clash_mode(mode: &str) {
let core = Core::global();
+21 -65
View File
@@ -10,22 +10,14 @@ mod data;
mod feat;
mod utils;
use crate::{
data::Verge,
utils::{resolve, server},
};
use tauri::{
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
};
use crate::utils::{init, resolve, server};
use tauri::{api, Manager, SystemTray};
fn main() -> std::io::Result<()> {
{
let verge = Verge::new();
if server::check_singleton(verge.app_singleton_port).is_err() {
println!("app exists");
return Ok(());
}
// 单例检测
if server::check_singleton().is_err() {
println!("app exists");
return Ok(());
}
#[cfg(target_os = "windows")]
@@ -34,51 +26,13 @@ fn main() -> std::io::Result<()> {
dirs::init_portable_flag();
}
let tray_menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "Show"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
.add_item(CustomMenuItem::new("global_mode", "Global Mode"))
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
.add_item(CustomMenuItem::new("script_mode", "Script Mode"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
.add_item(CustomMenuItem::new("restart_app", "Restart App"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"));
crate::log_if_err!(init::init_config());
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.setup(|app| Ok(resolve::resolve_setup(app)))
.system_tray(SystemTray::new().with_menu(tray_menu))
.on_system_tray_event(move |app_handle, event| match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"open_window" => {
resolve::create_window(app_handle);
}
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode);
}
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"restart_app" => {
api::process::restart(&app_handle.env());
}
"quit" => {
resolve::resolve_reset();
app_handle.exit(0);
}
_ => {}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
}
_ => {}
})
.system_tray(SystemTray::new())
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
@@ -126,17 +80,18 @@ fn main() -> std::io::Result<()> {
{
use tauri::{Menu, MenuItem, Submenu};
let submenu_file = Submenu::new(
"File",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll),
builder = builder.menu(
Menu::new().add_submenu(Submenu::new(
"File",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll),
)),
);
builder = builder.menu(Menu::new().add_submenu(submenu_file));
}
#[allow(unused_mut)]
@@ -162,6 +117,7 @@ fn main() -> std::io::Result<()> {
tauri::RunEvent::Exit => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
#[cfg(target_os = "macos")]
tauri::RunEvent::WindowEvent { label, event, .. } => {
+6
View File
@@ -21,6 +21,8 @@ static mut RESOURCE_DIR: Option<PathBuf> = None;
#[allow(unused)]
static mut PORTABLE_FLAG: bool = false;
pub static mut APP_VERSION: &str = "v1.1.1";
/// initialize portable flag
#[allow(unused)]
pub unsafe fn init_portable_flag() {
@@ -66,6 +68,10 @@ pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
unsafe {
RESOURCE_DIR = Some(res_dir.clone());
let ver = package_info.version.to_string();
let ver_str = format!("v{ver}");
APP_VERSION = Box::leak(Box::new(ver_str));
}
res_dir
+18 -23
View File
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::Result;
use nanoid::nanoid;
use std::path::PathBuf;
use std::process::Command;
@@ -28,20 +28,16 @@ pub fn get_uid(prefix: &str) -> String {
/// parse the string
/// xxx=123123; => 123123
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
match target.find(key) {
Some(idx) => {
let idx = idx + key.len();
let value = &target[idx..];
match match value.split(';').nth(0) {
Some(value) => value.trim().parse(),
None => value.trim().parse(),
} {
Ok(r) => Some(r),
Err(_) => None,
}
target.find(key).and_then(|idx| {
let idx = idx + key.len();
let value = &target[idx..];
match value.split(';').nth(0) {
Some(value) => value.trim().parse(),
None => value.trim().parse(),
}
None => None,
}
.ok()
})
}
/// open file
@@ -49,22 +45,21 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
pub fn open_file(path: PathBuf) -> Result<()> {
// use vscode first
if let Ok(code) = which::which("code") {
let mut command = Command::new(&code);
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
if let Err(err) = Command::new(code)
.creation_flags(0x08000000)
.arg(path)
.spawn()
{
bail!("failed to open file by VScode for `{err}`");
if let Err(err) = command.creation_flags(0x08000000).arg(&path).spawn() {
log::error!(target: "app", "failed to open with VScode `{err}`");
open::that(path)?;
}
}
#[cfg(not(target_os = "windows"))]
if let Err(err) = Command::new(code).arg(path).spawn() {
bail!("failed to open file by VScode for `{err}`");
if let Err(err) = command.arg(&path).spawn() {
log::error!(target: "app", "failed to open with VScode `{err}`");
open::that(path)?;
}
return Ok(());
+20 -17
View File
@@ -8,11 +8,15 @@ use log4rs::config::{Appender, Config, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use tauri::PackageInfo;
/// initialize this instance's log file
fn init_log(log_dir: &PathBuf) -> Result<()> {
fn init_log() -> Result<()> {
let log_dir = dirs::app_logs_dir();
if !log_dir.exists() {
let _ = fs::create_dir_all(&log_dir);
}
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file);
@@ -42,7 +46,19 @@ fn init_log(log_dir: &PathBuf) -> Result<()> {
}
/// Initialize all the files from resources
fn init_config(app_dir: &PathBuf) -> Result<()> {
pub fn init_config() -> Result<()> {
let _ = init_log();
let app_dir = dirs::app_home_dir();
let profiles_dir = dirs::app_profiles_dir();
if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir);
}
if !profiles_dir.exists() {
let _ = fs::create_dir_all(&profiles_dir);
}
// target path
let clash_path = app_dir.join("config.yaml");
let verge_path = app_dir.join("verge.yaml");
@@ -61,27 +77,14 @@ fn init_config(app_dir: &PathBuf) -> Result<()> {
}
/// initialize app
pub fn init_app(package_info: &PackageInfo) {
pub fn init_resources(package_info: &PackageInfo) {
// create app dir
let app_dir = dirs::app_home_dir();
let log_dir = dirs::app_logs_dir();
let profiles_dir = dirs::app_profiles_dir();
let res_dir = dirs::app_resources_dir(package_info);
if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir);
}
if !log_dir.exists() {
let _ = fs::create_dir_all(&log_dir);
}
if !profiles_dir.exists() {
let _ = fs::create_dir_all(&profiles_dir);
}
crate::log_if_err!(init_log(&log_dir));
crate::log_if_err!(init_config(&app_dir));
// copy the resource file
let mmdb_path = app_dir.join("Country.mmdb");
+12 -4
View File
@@ -1,10 +1,18 @@
use crate::{core::Core, data::Data, utils::init, utils::server};
use crate::{
core::{tray, Core},
data::Data,
utils::init,
utils::server,
};
use tauri::{App, AppHandle, Manager};
/// handle something when start app
pub fn resolve_setup(app: &App) {
// init app config
init::init_app(app.package_info());
let _ = app
.tray_handle()
.set_menu(tray::Tray::tray_menu(&app.app_handle()));
init::init_resources(app.package_info());
let silent_start = {
let global = Data::global();
@@ -12,7 +20,7 @@ pub fn resolve_setup(app: &App) {
let singleton = verge.app_singleton_port.clone();
// setup a simple http server for singleton
server::embed_server(&app.handle(), singleton);
server::embed_server(&app.app_handle(), singleton);
verge.enable_silent_start.clone().unwrap_or(false)
};
+4 -2
View File
@@ -1,6 +1,7 @@
extern crate warp;
use super::resolve;
use crate::data::Verge;
use port_scanner::local_port_available;
use tauri::AppHandle;
use warp::Filter;
@@ -11,8 +12,9 @@ const SERVER_PORT: u16 = 33331;
const SERVER_PORT: u16 = 11233;
/// check whether there is already exists
pub fn check_singleton(port: Option<u16>) -> Result<(), ()> {
let port = port.unwrap_or(SERVER_PORT);
pub fn check_singleton() -> Result<(), ()> {
let verge = Verge::new();
let port = verge.app_singleton_port.unwrap_or(SERVER_PORT);
if !local_port_available(port) {
tauri::async_runtime::block_on(async {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "Clash Verge",
"version": "1.1.1"
"version": "1.1.2"
},
"build": {
"distDir": "../dist",
+2 -2
View File
@@ -7,7 +7,7 @@
> header {
flex: 0 0 58px;
width: 90%;
max-width: 850px;
// max-width: 850px;
margin: 0 auto;
padding-right: 4px;
box-sizing: border-box;
@@ -28,7 +28,7 @@
.base-content {
width: 90%;
max-width: 850px;
// max-width: 850px;
margin: 0 auto;
}
}
+62 -14
View File
@@ -4,15 +4,19 @@ import { useLockFn, useSetState } from "ahooks";
import { useTranslation } from "react-i18next";
import {
Button,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
IconButton,
Switch,
TextField,
} from "@mui/material";
import { Settings } from "@mui/icons-material";
import { patchProfile } from "@/services/cmds";
import { version } from "@root/package.json";
import Notice from "../base/base-notice";
interface Props {
@@ -33,11 +37,15 @@ const InfoEditor = (props: Props) => {
useEffect(() => {
if (itemData) {
const { option } = itemData;
setForm({ ...itemData });
setOption(itemData.option ?? {});
setOption(option ?? {});
setShowOpt(
itemData.type === "remote" &&
(!!itemData.option?.user_agent || !!itemData.option?.update_interval)
(!!option?.user_agent ||
!!option?.update_interval ||
!!option?.self_proxy ||
!!option?.with_proxy)
);
}
}, [itemData]);
@@ -114,18 +122,7 @@ const InfoEditor = (props: Props) => {
/>
)}
{showOpt && (
<TextField
{...textFieldProps}
label="User Agent"
value={option.user_agent}
placeholder="clash-verge/v1.0.0"
onChange={(e) => setOption({ user_agent: e.target.value })}
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
/>
)}
{((type === "remote" && showOpt) || type === "local") && (
{(type === "remote" || type === "local") && (
<TextField
{...textFieldProps}
label={t("Update Interval(mins)")}
@@ -137,6 +134,57 @@ const InfoEditor = (props: Props) => {
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
/>
)}
<Collapse
in={type === "remote" && showOpt}
timeout="auto"
unmountOnExit
>
<TextField
{...textFieldProps}
label="User Agent"
value={option.user_agent}
placeholder={`clash-verge/v${version}`}
onChange={(e) => setOption({ user_agent: e.target.value })}
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
/>
<FormControlLabel
label={t("Use System Proxy")}
labelPlacement="start"
sx={{ ml: 0, my: 1 }}
control={
<Switch
color="primary"
checked={option.with_proxy ?? false}
onChange={(_e, c) =>
setOption((o) => ({
self_proxy: c ? false : o.self_proxy ?? false,
with_proxy: c,
}))
}
/>
}
/>
<FormControlLabel
label={t("Use Clash Proxy")}
labelPlacement="start"
sx={{ ml: 0, my: 1 }}
control={
<Switch
color="primary"
checked={option.self_proxy ?? false}
onChange={(_e, c) =>
setOption((o) => ({
with_proxy: c ? false : o.with_proxy ?? false,
self_proxy: c,
}))
}
/>
}
/>
</Collapse>
</DialogContent>
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
+25 -5
View File
@@ -110,12 +110,32 @@ const ProfileItem = (props: Props) => {
}
});
const onUpdate = useLockFn(async (withProxy: boolean) => {
/// 0 不使用任何代理
/// 1 使用配置好的代理
/// 2 至少使用一个代理,根据配置,如果没配置,默认使用系统代理
const onUpdate = useLockFn(async (type: 0 | 1 | 2) => {
setAnchorEl(null);
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
const option: Partial<CmdType.ProfileOption> = {};
if (type === 0) {
option.with_proxy = false;
option.self_proxy = false;
} else if (type === 1) {
// nothing
} else if (type === 2) {
if (itemData.option?.self_proxy) {
option.with_proxy = false;
option.self_proxy = true;
} else {
option.with_proxy = true;
option.self_proxy = false;
}
}
try {
await updateProfile(itemData.uid, { with_proxy: withProxy });
await updateProfile(itemData.uid, option);
mutate("getProfiles");
} catch (err: any) {
const errmsg = err?.message || err.toString();
@@ -142,8 +162,8 @@ const ProfileItem = (props: Props) => {
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(false) },
{ label: "Update(Proxy)", handler: () => onUpdate(true) },
{ label: "Update", handler: () => onUpdate(0) },
{ label: "Update(Proxy)", handler: () => onUpdate(2) },
{ label: "Delete", handler: onDelete },
];
const fileModeMenu = [
@@ -199,7 +219,7 @@ const ProfileItem = (props: Props) => {
disabled={loading}
onClick={(e) => {
e.stopPropagation();
onUpdate(false);
onUpdate(1);
}}
>
<RefreshRounded color="inherit" />
+48 -3
View File
@@ -4,15 +4,18 @@ import { useTranslation } from "react-i18next";
import { useLockFn, useSetState } from "ahooks";
import {
Button,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
MenuItem,
Select,
Switch,
TextField,
} from "@mui/material";
import { Settings } from "@mui/icons-material";
@@ -40,7 +43,11 @@ const ProfileNew = (props: Props) => {
const [showOpt, setShowOpt] = useState(false);
// can add more option
const [option, setOption] = useSetState({ user_agent: "" });
const [option, setOption] = useSetState({
user_agent: "",
with_proxy: false,
self_proxy: false,
});
// file input
const fileDataRef = useRef<string | null>(null);
@@ -132,7 +139,11 @@ const ProfileNew = (props: Props) => {
<FileInput onChange={(val) => (fileDataRef.current = val)} />
)}
{showOpt && (
<Collapse
in={form.type === "remote" && showOpt}
timeout="auto"
unmountOnExit
>
<TextField
{...textFieldProps}
label="User Agent"
@@ -140,7 +151,41 @@ const ProfileNew = (props: Props) => {
value={option.user_agent}
onChange={(e) => setOption({ user_agent: e.target.value })}
/>
)}
<FormControlLabel
label={t("Use System Proxy")}
labelPlacement="start"
sx={{ ml: 0, my: 1 }}
control={
<Switch
color="primary"
checked={option.with_proxy}
onChange={(_e, c) =>
setOption((o) => ({
self_proxy: c ? false : o.self_proxy,
with_proxy: c,
}))
}
/>
}
/>
<FormControlLabel
label={t("Use Clash Proxy")}
labelPlacement="start"
sx={{ ml: 0, my: 1 }}
control={
<Switch
color="primary"
checked={option.self_proxy}
onChange={(_e, c) =>
setOption((o) => ({
with_proxy: c ? false : o.with_proxy,
self_proxy: c,
}))
}
/>
}
/>
</Collapse>
</DialogContent>
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
+8 -14
View File
@@ -5,9 +5,8 @@ import { Virtuoso } from "react-virtuoso";
import { providerHealthCheck, updateProxy } from "@/services/api";
import { getProfiles, patchProfile } from "@/services/cmds";
import delayManager from "@/services/delay";
import useSortProxy from "./use-sort-proxy";
import useHeadState from "./use-head-state";
import useFilterProxy from "./use-filter-proxy";
import useFilterSort from "./use-filter-sort";
import ProxyHead from "./proxy-head";
import ProxyItem from "./proxy-item";
@@ -27,14 +26,10 @@ const ProxyGlobal = (props: Props) => {
const [headState, setHeadState] = useHeadState(groupName);
const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(
const sortedProxies = useFilterSort(
proxies,
groupName,
headState.filterText
);
const sortedProxies = useSortProxy(
filterProxies,
groupName,
headState.filterText,
headState.sortType
);
@@ -85,13 +80,12 @@ const ProxyGlobal = (props: Props) => {
}
await delayManager.checkListDelay(
{
names: sortedProxies.filter((p) => !p.provider).map((p) => p.name),
groupName,
skipNum: 16,
},
() => mutate("getProxies")
sortedProxies.filter((p) => !p.provider).map((p) => p.name),
groupName,
16
);
mutate("getProxies");
});
useEffect(() => onLocation(false), [groupName]);
+19 -16
View File
@@ -18,9 +18,8 @@ import {
import { providerHealthCheck, updateProxy } from "@/services/api";
import { getProfiles, patchProfile } from "@/services/cmds";
import delayManager from "@/services/delay";
import useSortProxy from "./use-sort-proxy";
import useHeadState from "./use-head-state";
import useFilterProxy from "./use-filter-proxy";
import useFilterSort from "./use-filter-sort";
import ProxyHead from "./proxy-head";
import ProxyItem from "./proxy-item";
@@ -35,14 +34,10 @@ const ProxyGroup = ({ group }: Props) => {
const [headState, setHeadState] = useHeadState(group.name);
const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(
const sortedProxies = useFilterSort(
group.all,
group.name,
headState.filterText
);
const sortedProxies = useSortProxy(
filterProxies,
group.name,
headState.filterText,
headState.sortType
);
@@ -105,21 +100,29 @@ const ProxyGroup = ({ group }: Props) => {
}
await delayManager.checkListDelay(
{
names: sortedProxies.filter((p) => !p.provider).map((p) => p.name),
groupName: group.name,
skipNum: 16,
},
() => mutate("getProxies")
sortedProxies.filter((p) => !p.provider).map((p) => p.name),
group.name,
16
);
mutate("getProxies");
});
// auto scroll to current index
useEffect(() => {
if (headState.open) {
setTimeout(() => onLocation(false), 5);
setTimeout(() => onLocation(false), 10);
}
}, [headState.open]);
}, [headState.open, sortedProxies]);
// auto scroll when sorted changed
const timerRef = useRef<any>();
useEffect(() => {
if (headState.open) {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => onLocation(false), 500);
}
}, [headState.open, sortedProxies]);
return (
<>
+1 -1
View File
@@ -16,7 +16,7 @@ import {
} from "@mui/icons-material";
import delayManager from "@/services/delay";
import type { HeadState } from "./use-head-state";
import type { ProxySortType } from "./use-sort-proxy";
import type { ProxySortType } from "./use-filter-sort";
interface Props {
sx?: SxProps;
+9 -4
View File
@@ -49,6 +49,14 @@ const ProxyItem = (props: Props) => {
// -2 为 loading
const [delay, setDelay] = useState(-1);
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
};
}, [proxy.name, groupName]);
useEffect(() => {
if (!proxy) return;
@@ -66,10 +74,7 @@ const ProxyItem = (props: Props) => {
const onDelay = useLockFn(async () => {
setDelay(-2);
return delayManager
.checkDelay(proxy.name, groupName)
.then((result) => setDelay(result))
.catch(() => setDelay(1e6));
setDelay(await delayManager.checkDelay(proxy.name, groupName));
});
return (
-49
View File
@@ -1,49 +0,0 @@
import { useMemo } from "react";
import delayManager from "@/services/delay";
const regex1 = /delay([=<>])(\d+|timeout|error)/i;
const regex2 = /type=(.*)/i;
/**
* filter the proxy
* according to the regular conditions
*/
export default function useFilterProxy(
proxies: ApiType.ProxyItem[],
groupName: string,
filterText: string
) {
return useMemo(() => {
if (!proxies) return [];
if (!filterText) return proxies;
const res1 = regex1.exec(filterText);
if (res1) {
const symbol = res1[1];
const symbol2 = res1[2].toLowerCase();
const value =
symbol2 === "error" ? 1e5 : symbol2 === "timeout" ? 3000 : +symbol2;
return proxies.filter((p) => {
const delay = delayManager.getDelay(p.name, groupName);
if (delay < 0) return false;
if (symbol === "=" && symbol2 === "error") return delay >= 1e5;
if (symbol === "=" && symbol2 === "timeout")
return delay < 1e5 && delay >= 3000;
if (symbol === "=") return delay == value;
if (symbol === "<") return delay <= value;
if (symbol === ">") return delay >= value;
return false;
});
}
const res2 = regex2.exec(filterText);
if (res2) {
const type = res2[1].toLowerCase();
return proxies.filter((p) => p.type.toLowerCase().includes(type));
}
return proxies.filter((p) => p.name.includes(filterText.trim()));
}, [proxies, groupName, filterText]);
}
+114
View File
@@ -0,0 +1,114 @@
import { useEffect, useMemo, useState } from "react";
import delayManager from "@/services/delay";
// default | delay | alphabet
export type ProxySortType = 0 | 1 | 2;
export default function useFilterSort(
proxies: ApiType.ProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType
) {
const [refresh, setRefresh] = useState({});
useEffect(() => {
let last = 0;
delayManager.setGroupListener(groupName, () => {
// 简单节流
const now = Date.now();
if (now - last > 666) {
last = now;
setRefresh({});
}
});
return () => {
delayManager.removeGroupListener(groupName);
};
}, [groupName]);
return useMemo(() => {
const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType);
return sp;
}, [proxies, groupName, filterText, sortType, refresh]);
}
/**
* 可以通过延迟数/节点类型 过滤
*/
const regex1 = /delay([=<>])(\d+|timeout|error)/i;
const regex2 = /type=(.*)/i;
/**
* filter the proxy
* according to the regular conditions
*/
function filterProxies(
proxies: ApiType.ProxyItem[],
groupName: string,
filterText: string
) {
if (!filterText) return proxies;
const res1 = regex1.exec(filterText);
if (res1) {
const symbol = res1[1];
const symbol2 = res1[2].toLowerCase();
const value =
symbol2 === "error" ? 1e5 : symbol2 === "timeout" ? 3000 : +symbol2;
return proxies.filter((p) => {
const delay = delayManager.getDelay(p.name, groupName);
if (delay < 0) return false;
if (symbol === "=" && symbol2 === "error") return delay >= 1e5;
if (symbol === "=" && symbol2 === "timeout")
return delay < 1e5 && delay >= 3000;
if (symbol === "=") return delay == value;
if (symbol === "<") return delay <= value;
if (symbol === ">") return delay >= value;
return false;
});
}
const res2 = regex2.exec(filterText);
if (res2) {
const type = res2[1].toLowerCase();
return proxies.filter((p) => p.type.toLowerCase().includes(type));
}
return proxies.filter((p) => p.name.includes(filterText.trim()));
}
/**
* sort the proxy
*/
function sortProxies(
proxies: ApiType.ProxyItem[],
groupName: string,
sortType: ProxySortType
) {
if (!proxies) return [];
if (sortType === 0) return proxies;
const list = proxies.slice();
if (sortType === 1) {
list.sort((a, b) => {
const ad = delayManager.getDelay(a.name, groupName);
const bd = delayManager.getDelay(b.name, groupName);
if (ad === -1 || ad === -2) return 1;
if (bd === -1 || bd === -2) return -1;
return ad - bd;
});
} else {
list.sort((a, b) => a.name.localeCompare(b.name));
}
return list;
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { useRecoilValue } from "recoil";
import { atomCurrentProfile } from "@/services/states";
import { ProxySortType } from "./use-sort-proxy";
import { ProxySortType } from "./use-filter-sort";
export interface HeadState {
open?: boolean;
-37
View File
@@ -1,37 +0,0 @@
import { useMemo } from "react";
import delayManager from "@/services/delay";
// default | delay | alpha
export type ProxySortType = 0 | 1 | 2;
/**
* sort the proxy
*/
export default function useSortProxy(
proxies: ApiType.ProxyItem[],
groupName: string,
sortType: ProxySortType
) {
return useMemo(() => {
if (!proxies) return [];
if (sortType === 0) return proxies;
const list = proxies.slice();
if (sortType === 1) {
list.sort((a, b) => {
const ad = delayManager.getDelay(a.name, groupName);
const bd = delayManager.getDelay(b.name, groupName);
if (ad === -1 || ad === -2) return 1;
if (bd === -1 || bd === -2) return -1;
return ad - bd;
});
} else {
list.sort((a, b) => a.name.localeCompare(b.name));
}
return list;
}, [proxies, groupName, sortType]);
}
+1
View File
@@ -108,6 +108,7 @@ const ConnectionsPage = () => {
header={
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
<IconButton
color="inherit"
size="small"
sx={{ mr: 2 }}
onClick={() =>
+1
View File
@@ -45,6 +45,7 @@ const LogPage = () => {
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
<IconButton
size="small"
color="inherit"
sx={{ mr: 2 }}
onClick={() => setEnableLog((e) => !e)}
>
+20 -23
View File
@@ -1,5 +1,5 @@
import useSWR, { useSWRConfig } from "swr";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Button, ButtonGroup, List, Paper } from "@mui/material";
@@ -7,18 +7,20 @@ import { getClashConfig, updateConfigs } from "@/services/api";
import { patchClashConfig } from "@/services/cmds";
import { getProxies } from "@/services/api";
import BasePage from "@/components/base/base-page";
import BaseEmpty from "@/components/base/base-empty";
import ProxyGroup from "@/components/proxy/proxy-group";
import ProxyGlobal from "@/components/proxy/proxy-global";
const ProxyPage = () => {
const { t } = useTranslation();
const { mutate } = useSWRConfig();
const { data: proxiesData } = useSWR("getProxies", getProxies);
const { data: proxiesData } = useSWR("getProxies", getProxies, {
refreshInterval: 45000, // 45s
});
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
const modeList = ["rule", "global", "direct", "script"];
const curMode = clashConfig?.mode.toLowerCase();
const { groups = [], proxies = [] } = proxiesData ?? {};
const { global, groups = [], proxies = [] } = proxiesData ?? {};
// make sure that fetch the proxies successfully
useEffect(() => {
@@ -37,9 +39,16 @@ const ProxyPage = () => {
mutate("getClashConfig");
});
// 仅mode为全局和直连的时候展示global分组
const displayGroups = useMemo(() => {
if (!global) return groups;
if (curMode === "global" || curMode === "direct")
return [global, ...groups];
return groups;
}, [global, groups, curMode]);
// difference style
const showGroup =
(curMode === "rule" || curMode === "script") && !!groups.length;
const showGroup = displayGroups.length > 0;
const pageStyle = showGroup ? {} : { height: "100%" };
const paperStyle: any = showGroup
? { mb: 0.5 }
@@ -48,7 +57,7 @@ const ProxyPage = () => {
return (
<BasePage
contentStyle={pageStyle}
title={showGroup ? t("Proxy Groups") : t("Proxies")}
title={t("Proxy Groups")}
header={
<ButtonGroup size="small">
{modeList.map((mode) => (
@@ -65,26 +74,14 @@ const ProxyPage = () => {
}
>
<Paper sx={{ borderRadius: 1, boxShadow: 2, ...paperStyle }}>
{(curMode === "rule" || curMode === "script") && !!groups.length && (
{displayGroups.length > 0 ? (
<List>
{groups.map((group) => (
{displayGroups.map((group) => (
<ProxyGroup key={group.name} group={group} />
))}
</List>
)}
{((curMode === "rule" && !groups.length) || curMode === "global") && (
<ProxyGlobal
groupName="GLOBAL"
curProxy={proxiesData?.global?.now}
proxies={proxies}
/>
)}
{curMode === "direct" && (
<ProxyGlobal
groupName="DIRECT"
curProxy="DIRECT"
proxies={[proxiesData?.direct!].filter(Boolean)}
/>
) : (
<BaseEmpty />
)}
</Paper>
</BasePage>
+6 -1
View File
@@ -142,7 +142,12 @@ export async function getProxies() {
)
);
return { global, direct, groups, records: proxyRecord, proxies };
const _global: ApiType.ProxyGroupItem = {
...global,
all: global?.all?.map((item) => generateItem(item)) || [],
};
return { global: _global, direct, groups, records: proxyRecord, proxies };
}
// get proxy providers
+38 -18
View File
@@ -6,6 +6,12 @@ class DelayManager {
private cache = new Map<string, [number, number]>();
private urlMap = new Map<string, string>();
// 每个item的监听
private listenerMap = new Map<string, (time: number) => void>();
// 每个分组的监听
private groupListenerMap = new Map<string, () => void>();
setUrl(group: string, url: string) {
this.urlMap.set(group, url);
}
@@ -14,8 +20,29 @@ class DelayManager {
return this.urlMap.get(group);
}
setListener(name: string, group: string, listener: (time: number) => void) {
const key = hashKey(name, group);
this.listenerMap.set(key, listener);
}
removeListener(name: string, group: string) {
const key = hashKey(name, group);
this.listenerMap.delete(key);
}
setGroupListener(group: string, listener: () => void) {
this.groupListenerMap.set(group, listener);
}
removeGroupListener(group: string) {
this.groupListenerMap.delete(group);
}
setDelay(name: string, group: string, delay: number) {
this.cache.set(hashKey(name, group), [Date.now(), delay]);
const key = hashKey(name, group);
this.cache.set(key, [Date.now(), delay]);
this.listenerMap.get(key)?.(delay);
this.groupListenerMap.get(group)?.();
}
getDelay(name: string, group: string) {
@@ -44,19 +71,13 @@ class DelayManager {
}
async checkListDelay(
options: {
names: readonly string[];
groupName: string;
skipNum: number;
},
callback: Function
nameList: readonly string[],
groupName: string,
concurrency: number
) {
const { groupName, skipNum } = options;
const names = [...nameList];
const names = [...options.names];
const total = names.length;
let count = 0;
let total = names.length;
let current = 0;
// 设置正在延迟测试中
@@ -64,7 +85,7 @@ class DelayManager {
return new Promise((resolve) => {
const help = async (): Promise<void> => {
if (current >= skipNum) return;
if (current >= concurrency) return;
const task = names.shift();
if (!task) return;
@@ -72,14 +93,13 @@ class DelayManager {
current += 1;
await this.checkDelay(task, groupName);
current -= 1;
total -= 1;
if (count++ % skipNum === 0 || count === total) callback();
if (count === total) resolve(null);
return help();
if (total <= 0) resolve(null);
else return help();
};
for (let i = 0; i < skipNum; ++i) help();
for (let i = 0; i < concurrency; ++i) help();
});
}
}
+1
View File
@@ -124,6 +124,7 @@ declare namespace CmdType {
interface ProfileOption {
user_agent?: string;
with_proxy?: boolean;
self_proxy?: boolean;
update_interval?: number;
}
+4 -4
View File
@@ -1992,10 +1992,10 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-virtuoso@^2.17.2:
version "2.17.2"
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.17.2.tgz#cc7323f300d5d80311bf828efcda95f6cc1edd51"
integrity sha512-5xl0mCCVEANzY0SbVbUjQgRrc2it3vdGCn72kUQo0wy4s3QGVR8S3QcLZj3lBrHocGIf/ONEJ3g/uqs0KEl9eA==
react-virtuoso@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/react-virtuoso/-/react-virtuoso-3.1.0.tgz#e358a39b9fd99895bcc72671a585091b756a1c92"
integrity sha512-Rur0d7xiZthRxy3f7Z217Q+U6k1sTsPZZU/kKT73GPB3ROtJCr4Y0Qehg/WxeKhochQPnSuT8VfcsAasdpX2ig==
dependencies:
"@virtuoso.dev/react-urx" "^0.2.12"
"@virtuoso.dev/urx" "^0.2.12"