Compare commits
54 Commits
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ dist-ssr
|
||||
*.local
|
||||
update.json
|
||||
scripts/_env.sh
|
||||
.vscode
|
||||
|
||||
32
UPDATELOG.md
32
UPDATELOG.md
@@ -1,3 +1,35 @@
|
||||
## 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
|
||||
|
||||
- optimize clash config feedback
|
||||
- hide macOS dock icon
|
||||
- use clash meta compatible version (Linux)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.1.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.1.0",
|
||||
"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"
|
||||
|
||||
@@ -28,6 +28,7 @@ function resolveClash() {
|
||||
"darwin-x64": "clash-darwin-amd64",
|
||||
"darwin-arm64": "clash-darwin-arm64",
|
||||
"linux-x64": "clash-linux-amd64",
|
||||
"linux-arm64": "clash-linux-armv8",
|
||||
};
|
||||
|
||||
const name = map[`${platform}-${arch}`];
|
||||
@@ -58,7 +59,8 @@ async function resolveClashMeta() {
|
||||
"win32-x64": "Clash.Meta-windows-amd64",
|
||||
"darwin-x64": "Clash.Meta-darwin-amd64",
|
||||
"darwin-arm64": "Clash.Meta-darwin-arm64",
|
||||
"linux-x64": "Clash.Meta-linux-amd64",
|
||||
"linux-x64": "Clash.Meta-linux-amd64-compatible",
|
||||
"linux-arm64": "Clash.Meta-linux-arm64",
|
||||
};
|
||||
|
||||
const name = map[`${platform}-${arch}`];
|
||||
@@ -258,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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)]
|
||||
@@ -40,62 +39,28 @@ 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");
|
||||
pub fn notice_message(&self, status: String, msg: String) {
|
||||
if let Some(window) = self.get_window() {
|
||||
log_if_err!(window.emit("verge://notice-message", (status, msg)));
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
@@ -284,9 +289,14 @@ impl Core {
|
||||
match Service::set_config(clash_info, config).await {
|
||||
Ok(_) => {
|
||||
let handle = handle.lock();
|
||||
handle.refresh_clash()
|
||||
handle.refresh_clash();
|
||||
handle.notice_message("set_config::ok".into(), "ok".into());
|
||||
}
|
||||
Err(err) => {
|
||||
let handle = handle.lock();
|
||||
handle.notice_message("set_config::error".into(), format!("{err}"));
|
||||
log::error!(target: "app", "last {err}")
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -129,8 +129,15 @@ impl Service {
|
||||
let app_dir = dirs::app_home_dir();
|
||||
let app_dir = app_dir.as_os_str().to_str().unwrap();
|
||||
|
||||
// fix #212
|
||||
let args = match clash_core.as_str() {
|
||||
"clash-meta" => vec!["-m", "-d", app_dir],
|
||||
_ => vec!["-d", app_dir],
|
||||
};
|
||||
|
||||
let cmd = Command::new_sidecar(clash_core)?;
|
||||
let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?;
|
||||
|
||||
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
|
||||
|
||||
// 将pid写入文件中
|
||||
let pid = cmd_child.pid();
|
||||
@@ -165,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,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -212,8 +221,17 @@ impl Service {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
||||
|
||||
macro_rules! report_err {
|
||||
($i: expr, $e: expr) => {
|
||||
match $i {
|
||||
4 => bail!($e),
|
||||
_ => log::error!(target: "app", $e),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// retry 5 times
|
||||
for _ in 0..5 {
|
||||
for i in 0..5 {
|
||||
let headers = headers.clone();
|
||||
match reqwest::ClientBuilder::new().no_proxy().build() {
|
||||
Ok(client) => {
|
||||
@@ -223,14 +241,12 @@ impl Service {
|
||||
204 => break,
|
||||
// 配置有问题不重试
|
||||
400 => bail!("failed to update clash config with status 400"),
|
||||
status @ _ => {
|
||||
log::error!(target: "app", "failed to activate clash with status \"{status}\"");
|
||||
}
|
||||
status @ _ => report_err!(i, "failed to activate clash with status \"{status}\""),
|
||||
},
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => report_err!(i, "{err}"),
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => report_err!(i, "{err}"),
|
||||
}
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
@@ -170,6 +169,12 @@ impl Sysopt {
|
||||
|
||||
self.auto_launch = Some(auto);
|
||||
|
||||
// 避免在开发时将自启动关了
|
||||
#[cfg(feature = "verge-dev")]
|
||||
if !enable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto = self.auto_launch.as_ref().unwrap();
|
||||
|
||||
// macos每次启动都更新登录项,避免重复设置登录项
|
||||
|
||||
124
src-tauri/src/core/tray.rs
Normal file
124
src-tauri/src/core/tray.rs
Normal 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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -10,81 +10,29 @@ 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 mut context = tauri::generate_context!();
|
||||
|
||||
let verge = Verge::new();
|
||||
|
||||
if server::check_singleton(verge.app_singleton_port).is_err() {
|
||||
// 单例检测
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for win in context.config_mut().tauri.windows.iter_mut() {
|
||||
if verge.enable_silent_start.unwrap_or(false) {
|
||||
win.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
use crate::utils::dirs;
|
||||
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,
|
||||
@@ -132,23 +80,28 @@ 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));
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.build(context)
|
||||
#[allow(unused_mut)]
|
||||
let mut app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let app_handle = app.app_handle();
|
||||
ctrlc::set_handler(move || {
|
||||
resolve::resolve_reset();
|
||||
@@ -164,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, .. } => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
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();
|
||||
let verge = global.verge.lock();
|
||||
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)
|
||||
};
|
||||
|
||||
// core should be initialized after init_app fix #122
|
||||
let core = Core::global();
|
||||
core.init(app.app_handle());
|
||||
|
||||
resolve_window(app);
|
||||
if !silent_start {
|
||||
create_window(&app.app_handle());
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy
|
||||
@@ -33,39 +45,6 @@ pub fn resolve_reset() {
|
||||
crate::log_if_err!(service.stop());
|
||||
}
|
||||
|
||||
/// customize the window theme
|
||||
fn resolve_window(app: &App) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::utils::winhelp;
|
||||
use window_shadows::set_shadow;
|
||||
use window_vibrancy::apply_blur;
|
||||
|
||||
let _ = window.set_decorations(false);
|
||||
let _ = set_shadow(&window, true);
|
||||
|
||||
// todo
|
||||
// win11 disable this feature temporarily due to lag
|
||||
if !winhelp::is_win11() {
|
||||
let _ = apply_blur(&window, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::LogicalSize;
|
||||
use tauri::Size::Logical;
|
||||
|
||||
let _ = window.set_decorations(true);
|
||||
let _ = window.set_size(Logical(LogicalSize {
|
||||
width: 800.0,
|
||||
height: 620.0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
@@ -120,7 +99,7 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::log_if_err!(builder.decorations(true).inner_size(800.0, 620.0).build());
|
||||
crate::log_if_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::log_if_err!(builder
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.2"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -81,20 +81,7 @@
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Clash Verge",
|
||||
"width": 800,
|
||||
"height": 636,
|
||||
"center": true,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"minWidth": 600,
|
||||
"minHeight": 520
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
||||
}
|
||||
|
||||
@@ -31,3 +31,9 @@ body {
|
||||
|
||||
@import "./layout.scss";
|
||||
@import "./page.scss";
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
background-color: rgba(18, 18, 18, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function useCustomTheme() {
|
||||
const scrollColor = mode === "light" ? "#90939980" : "#54545480";
|
||||
|
||||
const rootEle = document.documentElement;
|
||||
rootEle.style.background = "transparent";
|
||||
rootEle.style.setProperty("--selection-color", selectColor);
|
||||
rootEle.style.setProperty("--scroller-color", scrollColor);
|
||||
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
|
||||
|
||||
@@ -33,7 +33,7 @@ const EnhancedMode = (props: Props) => {
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
mutateLogs();
|
||||
Notice.success("Refresh clash config", 1000);
|
||||
// Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
src/components/proxy/use-filter-sort.ts
Normal file
114
src/components/proxy/use-filter-sort.ts
Normal 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,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;
|
||||
|
||||
@@ -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) return 1;
|
||||
if (bd === -1) return -1;
|
||||
|
||||
return ad - bd;
|
||||
});
|
||||
} else {
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [proxies, groupName, sortType]);
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const ClashFieldViewer = ({ handler }: Props) => {
|
||||
try {
|
||||
await changeProfileValid([...curSet]);
|
||||
mutateProfile();
|
||||
Notice.success("Refresh clash config", 1000);
|
||||
// Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getAxios } from "@/services/api";
|
||||
import { atomCurrentProfile } from "@/services/states";
|
||||
import { getVergeConfig, getProfiles } from "@/services/cmds";
|
||||
import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
import LayoutItem from "@/components/layout/layout-item";
|
||||
import LayoutControl from "@/components/layout/layout-control";
|
||||
import LayoutTraffic from "@/components/layout/layout-traffic";
|
||||
@@ -59,6 +60,21 @@ const Layout = () => {
|
||||
// update the verge config
|
||||
listen("verge://refresh-verge-config", () => mutate("getVergeConfig"));
|
||||
|
||||
// 设置提示监听
|
||||
listen("verge://notice-message", ({ payload }) => {
|
||||
const [status, msg] = payload as [string, string];
|
||||
switch (status) {
|
||||
case "set_config::ok":
|
||||
Notice.success("Refresh clash config");
|
||||
break;
|
||||
case "set_config::error":
|
||||
Notice.error(msg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// set current profile uid
|
||||
getProfiles().then((data) => setCurrentProfile(data.current ?? ""));
|
||||
}, []);
|
||||
|
||||
@@ -44,12 +44,12 @@ const ConnectionsPage = () => {
|
||||
|
||||
const filterConn = useMemo(() => {
|
||||
const orderFunc = orderOpts[curOrderOpt];
|
||||
const connetions = connData.connections.filter((conn) =>
|
||||
const connections = connData.connections.filter((conn) =>
|
||||
(conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText)
|
||||
);
|
||||
|
||||
if (orderFunc) return orderFunc(connetions);
|
||||
return connetions;
|
||||
if (orderFunc) return orderFunc(connections);
|
||||
return connections;
|
||||
}, [connData, filterText, curOrderOpt]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -108,6 +108,7 @@ const ConnectionsPage = () => {
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() =>
|
||||
@@ -167,6 +168,7 @@ const ConnectionsPage = () => {
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
@@ -93,6 +94,7 @@ const LogPage = () => {
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
|
||||
@@ -133,7 +133,7 @@ const ProfilePage = () => {
|
||||
setCurrentProfile(uid);
|
||||
mutate("getProfiles", { ...profiles, current: uid }, true);
|
||||
mutate("getRuntimeLogs");
|
||||
if (force) Notice.success("Refresh clash config", 1000);
|
||||
// if (force) Notice.success("Refresh clash config", 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
@@ -149,6 +149,7 @@ const ProfilePage = () => {
|
||||
value={url}
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
placeholder={t("Profile URL")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -37,6 +37,7 @@ const RulesPage = () => {
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
|
||||
@@ -88,13 +88,9 @@ export async function updateProxy(group: string, proxy: string) {
|
||||
|
||||
// get proxy
|
||||
async function getProxiesInner() {
|
||||
try {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/proxies");
|
||||
return (response?.proxies || {}) as Record<string, ApiType.ProxyItem>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/proxies");
|
||||
return (response?.proxies || {}) as Record<string, ApiType.ProxyItem>;
|
||||
}
|
||||
|
||||
/// Get the Proxy information
|
||||
@@ -146,29 +142,30 @@ 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
|
||||
export async function getProviders() {
|
||||
try {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
ApiType.ProviderItem
|
||||
>;
|
||||
const providers = (response.providers || {}) as Record<
|
||||
string,
|
||||
ApiType.ProviderItem
|
||||
>;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, item]) => {
|
||||
const type = item.vehicleType.toLowerCase();
|
||||
return type === "http" || type === "file";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// proxy providers health check
|
||||
|
||||
@@ -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
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@@ -124,6 +124,7 @@ declare namespace CmdType {
|
||||
interface ProfileOption {
|
||||
user_agent?: string;
|
||||
with_proxy?: boolean;
|
||||
self_proxy?: boolean;
|
||||
update_interval?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user