mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ef3057ac | ||
|
|
48f3a934c9 | ||
|
|
38d1fde84f | ||
|
|
dd78670a4b | ||
|
|
b8a8190a43 | ||
|
|
2d0989342f | ||
|
|
15ff9b06a1 | ||
|
|
41b19f69de | ||
|
|
f2bd6f1fce | ||
|
|
ec94218a4b | ||
|
|
c2e5c7cf38 | ||
|
|
bac5734527 | ||
|
|
bef4033d94 | ||
|
|
64216cba67 | ||
|
|
a9d6167a9f | ||
|
|
6c6b40548f | ||
|
|
a916d88e85 | ||
|
|
ab2f0548a3 | ||
|
|
e30ba07285 | ||
|
|
1b336d973d | ||
|
|
7b1866737f | ||
|
|
eedc4ab648 | ||
|
|
5e429c7a94 | ||
|
|
57d23eb043 | ||
|
|
5ebd9be89a | ||
|
|
5a743779e2 | ||
|
|
0495062110 | ||
|
|
d522191f69 | ||
|
|
bbbdc8b7a6 | ||
|
|
c00ed8aa5a | ||
|
|
0beaa94068 | ||
|
|
96e76665d6 | ||
|
|
6423a29600 | ||
|
|
8bddf30dcf | ||
|
|
a6b2db182d | ||
|
|
b9f3f9d859 | ||
|
|
6331447dcd | ||
|
|
4213ee660f | ||
|
|
3bd9287c5d | ||
|
|
b23d3f7c8b | ||
|
|
f8d9e5e027 | ||
|
|
8fa7fb3b1f | ||
|
|
63d92a0872 | ||
|
|
509d83365e | ||
|
|
d6ab73c905 | ||
|
|
5a93ba05d5 | ||
|
|
2d2fdf0b1e | ||
|
|
cf96622261 | ||
|
|
47c8ccb0e5 | ||
|
|
02fdb8778b | ||
|
|
6bed7f0e66 | ||
|
|
7597d335b9 | ||
|
|
f32c5ba244 | ||
|
|
ab53ab21e2 | ||
|
|
acfe5dbb49 | ||
|
|
f9b91fa189 | ||
|
|
2462e68ba1 | ||
|
|
019b2a1681 | ||
|
|
e94a07b677 | ||
|
|
c058c29755 | ||
|
|
9e7c7ac163 | ||
|
|
fcee41f00d | ||
|
|
ff2c1bf8ed | ||
|
|
a2cf26e7ed | ||
|
|
71e6900375 |
@@ -10,7 +10,7 @@ jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-18.04, macos-latest]
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: |
|
||||
startsWith(github.repository, 'zzzgydi') &&
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
## 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
|
||||
|
||||
- add rule page
|
||||
- supports proxy providers delay check
|
||||
- add proxy delay check loading status
|
||||
- supports hotkey/shortcut management
|
||||
- supports displaying connections data in table layout(refer to yacd)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- supports yaml merge key in clash config
|
||||
- detect the network interface and set the system proxy(macOS)
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.0.6
|
||||
|
||||
### Features
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.0.6",
|
||||
"version": "1.1.1",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
@@ -22,7 +22,8 @@
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/icons-material": "^5.10.3",
|
||||
"@mui/material": "^5.10.3",
|
||||
"@tauri-apps/api": "^1.0.2",
|
||||
"@mui/x-data-grid": "^5.17.4",
|
||||
"@tauri-apps/api": "^1.1.0",
|
||||
"ahooks": "^3.7.0",
|
||||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.11.5",
|
||||
@@ -39,7 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.3",
|
||||
"@tauri-apps/cli": "^1.0.5",
|
||||
"@tauri-apps/cli": "^1.1.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
@@ -66,4 +67,4 @@
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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/20220512/Country.mmdb";
|
||||
"https://github.com/Dreamacro/maxmind-geoip/releases/download/20220812/Country.mmdb";
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri", "resources");
|
||||
const resPath = path.join(resDir, "Country.mmdb");
|
||||
|
||||
Generated
+748
-488
File diff suppressed because it is too large
Load Diff
+12
-8
@@ -10,32 +10,36 @@ edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0", features = [] }
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
which = "4.2.2"
|
||||
anyhow = "1.0"
|
||||
dirs = "4.0.0"
|
||||
open = "2.1.1"
|
||||
open = "3.0.3"
|
||||
log = "0.4.14"
|
||||
ctrlc = "3.2.3"
|
||||
dunce = "1.0.2"
|
||||
log4rs = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
chrono = "0.4.19"
|
||||
sysinfo = "0.26.2"
|
||||
sysproxy = "0.1"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
auto-launch = "0.3"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.4"
|
||||
once_cell = "1.14.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.1"
|
||||
parking_lot = "0.12.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tauri = { version = "1.0.0", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
rquickjs = { version = "0.1.6" }
|
||||
window-shadows = { version = "0.1" }
|
||||
window-vibrancy = { version = "0.1" }
|
||||
tauri = { version = "1.1.1", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
rquickjs = { version = "0.1.7" }
|
||||
window-shadows = { version = "0.2.0" }
|
||||
window-vibrancy = { version = "0.3.0" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "0.2.1"
|
||||
|
||||
+123
-104
@@ -1,39 +1,39 @@
|
||||
use crate::{
|
||||
core::{ClashInfo, Core, PrfItem, PrfOption, Profiles, Verge},
|
||||
utils::{dirs, help, sysopt::SysProxyConfig},
|
||||
core::Core,
|
||||
data::{ClashInfo, Data, PrfItem, PrfOption, Profiles, Verge},
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use crate::{log_if_err, ret_err, wrap_err};
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{api, State};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
/// get all profiles from `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub fn get_profiles(core: State<'_, Core>) -> CmdResult<Profiles> {
|
||||
let profiles = core.profiles.lock();
|
||||
pub fn get_profiles() -> CmdResult<Profiles> {
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
Ok(profiles.clone())
|
||||
}
|
||||
|
||||
/// manually exec enhanced profile
|
||||
#[tauri::command]
|
||||
pub fn enhance_profiles(core: State<'_, Core>) -> CmdResult {
|
||||
pub fn enhance_profiles() -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.activate())
|
||||
}
|
||||
|
||||
/// import the profile from url
|
||||
/// and save to `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(
|
||||
url: String,
|
||||
option: Option<PrfOption>,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
|
||||
let mut profiles = core.profiles.lock();
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.append_item(item))
|
||||
}
|
||||
|
||||
@@ -41,60 +41,66 @@ pub async fn import_profile(
|
||||
/// append a temp profile item file to the `profiles` dir
|
||||
/// view the temp profile file by using vscode or other editor
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(
|
||||
item: PrfItem, // partial
|
||||
file_data: Option<String>,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
|
||||
let mut profiles = core.profiles.lock();
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.append_item(item))
|
||||
}
|
||||
|
||||
/// Update the profile
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(
|
||||
index: String,
|
||||
option: Option<PrfOption>,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.update_profile_item(index, option).await)
|
||||
}
|
||||
|
||||
/// change the current profile
|
||||
#[tauri::command]
|
||||
pub fn select_profile(index: String, core: State<'_, Core>) -> CmdResult {
|
||||
let mut profiles = core.profiles.lock();
|
||||
pub fn select_profile(index: String) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.put_current(index))?;
|
||||
drop(profiles);
|
||||
|
||||
let core = Core::global();
|
||||
wrap_err!(core.activate())
|
||||
}
|
||||
|
||||
/// change the profile chain
|
||||
#[tauri::command]
|
||||
pub fn change_profile_chain(chain: Option<Vec<String>>, core: State<'_, Core>) -> CmdResult {
|
||||
let mut profiles = core.profiles.lock();
|
||||
pub fn change_profile_chain(chain: Option<Vec<String>>) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.put_chain(chain))?;
|
||||
drop(profiles);
|
||||
|
||||
let core = Core::global();
|
||||
wrap_err!(core.activate())
|
||||
}
|
||||
|
||||
/// change the profile valid fields
|
||||
#[tauri::command]
|
||||
pub fn change_profile_valid(valid: Option<Vec<String>>, core: State<Core>) -> CmdResult {
|
||||
let mut profiles = core.profiles.lock();
|
||||
pub fn change_profile_valid(valid: Option<Vec<String>>) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.put_valid(valid))?;
|
||||
drop(profiles);
|
||||
|
||||
let core = Core::global();
|
||||
wrap_err!(core.activate())
|
||||
}
|
||||
|
||||
/// delete profile item
|
||||
#[tauri::command]
|
||||
pub fn delete_profile(index: String, core: State<'_, Core>) -> CmdResult {
|
||||
let mut profiles = core.profiles.lock();
|
||||
pub fn delete_profile(index: String) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
if wrap_err!(profiles.delete_item(index))? {
|
||||
drop(profiles);
|
||||
|
||||
let core = Core::global();
|
||||
log_if_err!(core.activate());
|
||||
}
|
||||
Ok(())
|
||||
@@ -102,30 +108,33 @@ pub fn delete_profile(index: String, core: State<'_, Core>) -> CmdResult {
|
||||
|
||||
/// patch the profile config
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem, core: State<'_, Core>) -> CmdResult {
|
||||
let mut profiles = core.profiles.lock();
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let mut profiles = global.profiles.lock();
|
||||
wrap_err!(profiles.patch_item(index, profile))?;
|
||||
drop(profiles);
|
||||
|
||||
// update cron task
|
||||
let core = Core::global();
|
||||
let mut timer = core.timer.lock();
|
||||
wrap_err!(timer.refresh())
|
||||
}
|
||||
|
||||
/// run vscode command to edit the profile
|
||||
#[tauri::command]
|
||||
pub fn view_profile(index: String, core: State<'_, Core>) -> CmdResult {
|
||||
let profiles = core.profiles.lock();
|
||||
pub fn view_profile(index: String) -> CmdResult {
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
|
||||
let file = item.file.clone();
|
||||
if file.is_none() {
|
||||
ret_err!("the file is null");
|
||||
ret_err!("file is null");
|
||||
}
|
||||
|
||||
let path = dirs::app_profiles_dir().join(file.unwrap());
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
ret_err!("file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(path))
|
||||
@@ -133,8 +142,9 @@ pub fn view_profile(index: String, core: State<'_, Core>) -> CmdResult {
|
||||
|
||||
/// read the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String, core: State<'_, Core>) -> CmdResult<String> {
|
||||
let profiles = core.profiles.lock();
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
@@ -142,16 +152,13 @@ pub fn read_profile_file(index: String, core: State<'_, Core>) -> CmdResult<Stri
|
||||
|
||||
/// save the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(
|
||||
index: String,
|
||||
file_data: Option<String>,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let profiles = core.profiles.lock();
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
}
|
||||
@@ -159,37 +166,40 @@ pub fn save_profile_file(
|
||||
/// get the clash core info from the state
|
||||
/// the caller can also get the infomation by clash's api
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info(core: State<'_, Core>) -> CmdResult<ClashInfo> {
|
||||
let clash = core.clash.lock();
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
let global = Data::global();
|
||||
let clash = global.clash.lock();
|
||||
Ok(clash.info.clone())
|
||||
}
|
||||
|
||||
/// get the runtime clash config mapping
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config(core: State<'_, Core>) -> CmdResult<Option<Mapping>> {
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
let core = Core::global();
|
||||
let rt = core.runtime.lock();
|
||||
Ok(rt.config.clone())
|
||||
}
|
||||
|
||||
/// get the runtime clash config yaml string
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml(core: State<'_, Core>) -> CmdResult<Option<String>> {
|
||||
pub fn get_runtime_yaml() -> CmdResult<Option<String>> {
|
||||
let core = Core::global();
|
||||
let rt = core.runtime.lock();
|
||||
Ok(rt.config_yaml.clone())
|
||||
}
|
||||
|
||||
/// get the runtime config exists keys
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists(core: State<'_, Core>) -> CmdResult<Vec<String>> {
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
let core = Core::global();
|
||||
let rt = core.runtime.lock();
|
||||
Ok(rt.exists_keys.clone())
|
||||
}
|
||||
|
||||
/// get the runtime enhanced chain log
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs(
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
let core = Core::global();
|
||||
let rt = core.runtime.lock();
|
||||
Ok(rt.chain_logs.clone())
|
||||
}
|
||||
@@ -198,81 +208,93 @@ pub fn get_runtime_logs(
|
||||
/// after putting the change to the clash core
|
||||
/// then we should save the latest config
|
||||
#[tauri::command]
|
||||
pub fn patch_clash_config(
|
||||
payload: Mapping,
|
||||
app_handle: tauri::AppHandle,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
wrap_err!(core.patch_clash(payload, &app_handle))
|
||||
pub fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.patch_clash(payload))
|
||||
}
|
||||
|
||||
/// get the verge config
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config(core: State<'_, Core>) -> CmdResult<Verge> {
|
||||
let verge = core.verge.lock();
|
||||
pub fn get_verge_config() -> CmdResult<Verge> {
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
Ok(verge.clone())
|
||||
}
|
||||
|
||||
/// patch the verge config
|
||||
/// this command only save the config and not responsible for other things
|
||||
#[tauri::command]
|
||||
pub fn patch_verge_config(
|
||||
payload: Verge,
|
||||
app_handle: tauri::AppHandle,
|
||||
core: State<'_, Core>,
|
||||
) -> CmdResult {
|
||||
wrap_err!(core.patch_verge(payload, &app_handle))
|
||||
pub fn patch_verge_config(payload: Verge) -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.patch_verge(payload))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_hotkeys(hotkeys: Vec<String>) -> CmdResult {
|
||||
let core = Core::global();
|
||||
let mut hotkey = core.hotkey.lock();
|
||||
wrap_err!(hotkey.update(hotkeys))
|
||||
}
|
||||
|
||||
/// change clash core
|
||||
#[tauri::command]
|
||||
pub fn change_clash_core(core: State<'_, Core>, clash_core: Option<String>) -> CmdResult {
|
||||
pub fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.change_core(clash_core))
|
||||
}
|
||||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub fn restart_sidecar(core: State<'_, Core>) -> CmdResult {
|
||||
pub fn restart_sidecar() -> CmdResult {
|
||||
let core = Core::global();
|
||||
wrap_err!(core.restart_clash())
|
||||
}
|
||||
|
||||
/// kill all sidecars when update app
|
||||
#[tauri::command]
|
||||
pub fn kill_sidecar() {
|
||||
api::process::kill_children();
|
||||
tauri::api::process::kill_children();
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
||||
wrap_err!(SysProxyConfig::get_sys())
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the current proxy config
|
||||
/// which may not the same as system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_cur_proxy(core: State<'_, Core>) -> CmdResult<Option<SysProxyConfig>> {
|
||||
let sysopt = core.sysopt.lock();
|
||||
wrap_err!(sysopt.get_sysproxy())
|
||||
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||
let core = Core::global();
|
||||
let service = core.service.lock();
|
||||
Ok(service.get_logs())
|
||||
}
|
||||
|
||||
/// open app config dir
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> Result<(), String> {
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = dirs::app_home_dir();
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// open logs dir
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> Result<(), String> {
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = dirs::app_logs_dir();
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
/// open url
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> Result<(), String> {
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
@@ -283,17 +305,17 @@ pub mod service {
|
||||
use crate::core::win_service::JsonResponse;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_service() -> Result<(), String> {
|
||||
pub async fn start_service() -> CmdResult<()> {
|
||||
wrap_err!(crate::core::Service::start_service().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_service() -> Result<(), String> {
|
||||
pub async fn stop_service() -> CmdResult<()> {
|
||||
wrap_err!(crate::core::Service::stop_service().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> Result<JsonResponse, String> {
|
||||
pub async fn check_service() -> CmdResult<JsonResponse> {
|
||||
// no log
|
||||
match crate::core::Service::check_service().await {
|
||||
Ok(res) => Ok(res),
|
||||
@@ -302,12 +324,12 @@ pub mod service {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> Result<(), String> {
|
||||
pub async fn install_service() -> CmdResult<()> {
|
||||
wrap_err!(crate::core::Service::install_service().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> Result<(), String> {
|
||||
pub async fn uninstall_service() -> CmdResult<()> {
|
||||
wrap_err!(crate::core::Service::uninstall_service().await)
|
||||
}
|
||||
}
|
||||
@@ -317,26 +339,23 @@ pub mod service {
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_service() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_service() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> Result<(), String> {
|
||||
pub async fn start_service() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> Result<(), String> {
|
||||
pub async fn stop_service() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ pub(self) use self::field::*;
|
||||
use self::merge::*;
|
||||
use self::script::*;
|
||||
use self::tun::*;
|
||||
use crate::core::ChainItem;
|
||||
use crate::core::ChainType;
|
||||
use crate::data::ChainItem;
|
||||
use crate::data::ChainType;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use crate::data::*;
|
||||
use crate::log_if_err;
|
||||
use anyhow::{bail, Result};
|
||||
use serde_yaml::Value;
|
||||
use tauri::{AppHandle, Manager, Window};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Option<AppHandle>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
pub fn set_inner(&mut self, app_handle: AppHandle) {
|
||||
self.app_handle = Some(app_handle);
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<Window> {
|
||||
self
|
||||
.app_handle
|
||||
.as_ref()
|
||||
.map_or(None, |a| a.get_window("main"))
|
||||
}
|
||||
|
||||
pub fn refresh_clash(&self) {
|
||||
if let Some(window) = self.get_window() {
|
||||
log_if_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_verge(&self) {
|
||||
if let Some(window) = self.get_window() {
|
||||
log_if_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn refresh_profiles(&self) {
|
||||
if let Some(window) = self.get_window() {
|
||||
log_if_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use crate::{data::*, feat, log_if_err};
|
||||
use anyhow::{bail, Result};
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Vec<String>, // 保存当前的热键设置
|
||||
manager: Option<AppHandle>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
pub fn new() -> Hotkey {
|
||||
Hotkey {
|
||||
current: Vec::new(),
|
||||
manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, app_handle: AppHandle) -> Result<()> {
|
||||
self.manager = Some(app_handle);
|
||||
let data = Data::global();
|
||||
let verge = data.verge.lock();
|
||||
|
||||
if let Some(hotkeys) = verge.hotkeys.as_ref() {
|
||||
for hotkey in hotkeys.iter() {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
if func.is_some() && key.is_some() {
|
||||
log_if_err!(self.register(key.unwrap(), func.unwrap()));
|
||||
} else {
|
||||
log::error!(target: "app", "invalid hotkey \"{}\":\"{}\"", key.unwrap_or("None"), func.unwrap_or("None"));
|
||||
}
|
||||
}
|
||||
self.current = hotkeys.clone();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
|
||||
if self.manager.is_none() {
|
||||
bail!("failed to get hotkey manager");
|
||||
}
|
||||
Ok(self.manager.as_ref().unwrap().global_shortcut_manager())
|
||||
}
|
||||
|
||||
fn register(&mut self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let mut manager = self.get_manager()?;
|
||||
|
||||
if manager.is_registered(hotkey)? {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule"),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global"),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct"),
|
||||
"clash_mode_script" => || feat::change_clash_mode("script"),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"enable_system_proxy" => || feat::enable_system_proxy(),
|
||||
"disable_system_proxy" => || feat::disable_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
||||
"enable_tun_mode" => || feat::enable_tun_mode(),
|
||||
"disable_tun_mode" => || feat::disable_tun_mode(),
|
||||
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
};
|
||||
|
||||
manager.register(hotkey, f)?;
|
||||
log::info!(target: "app", "register hotkey {hotkey} {func}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&mut self, hotkey: &str) -> Result<()> {
|
||||
self.get_manager()?.unregister(&hotkey)?;
|
||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||
let current = self.current.to_owned();
|
||||
let old_map = Self::get_map_from_vec(¤t);
|
||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||
|
||||
let (del, add) = Self::get_diff(old_map, new_map);
|
||||
|
||||
del.iter().for_each(|key| {
|
||||
let _ = self.unregister(key);
|
||||
});
|
||||
|
||||
add.iter().for_each(|(key, func)| {
|
||||
log_if_err!(self.register(key, func));
|
||||
});
|
||||
|
||||
self.current = new_hotkeys;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
hotkeys.iter().for_each(|hotkey| {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
if func.is_some() && key.is_some() {
|
||||
let func = func.unwrap().trim();
|
||||
let key = key.unwrap().trim();
|
||||
map.insert(key, func);
|
||||
}
|
||||
});
|
||||
map
|
||||
}
|
||||
|
||||
fn get_diff<'a>(
|
||||
old_map: HashMap<&'a str, &'a str>,
|
||||
new_map: HashMap<&'a str, &'a str>,
|
||||
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
|
||||
let mut del_list = vec![];
|
||||
let mut add_list = vec![];
|
||||
|
||||
old_map.iter().for_each(|(&key, func)| {
|
||||
match new_map.get(key) {
|
||||
Some(new_func) => {
|
||||
if new_func != func {
|
||||
del_list.push(key);
|
||||
add_list.push((key, *new_func));
|
||||
}
|
||||
}
|
||||
None => del_list.push(key),
|
||||
};
|
||||
});
|
||||
|
||||
new_map.iter().for_each(|(&key, &func)| {
|
||||
if old_map.get(key).is_none() {
|
||||
add_list.push((key, func));
|
||||
}
|
||||
});
|
||||
|
||||
(del_list, add_list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut manager) = self.get_manager() {
|
||||
let _ = manager.unregister_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
+153
-199
@@ -1,105 +1,82 @@
|
||||
use self::notice::Notice;
|
||||
use self::handle::Handle;
|
||||
use self::hotkey::Hotkey;
|
||||
use self::sysopt::Sysopt;
|
||||
use self::timer::Timer;
|
||||
use crate::config::enhance_config;
|
||||
use crate::data::*;
|
||||
use crate::log_if_err;
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml::Value;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, Window};
|
||||
|
||||
mod clash;
|
||||
mod notice;
|
||||
mod prfitem;
|
||||
mod profiles;
|
||||
mod handle;
|
||||
mod hotkey;
|
||||
mod service;
|
||||
mod sysopt;
|
||||
mod timer;
|
||||
mod verge;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::prfitem::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::service::*;
|
||||
pub use self::verge::*;
|
||||
|
||||
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 clash: Arc<Mutex<Clash>>,
|
||||
pub verge: Arc<Mutex<Verge>>,
|
||||
pub profiles: Arc<Mutex<Profiles>>,
|
||||
pub service: Arc<Mutex<Service>>,
|
||||
pub sysopt: Arc<Mutex<Sysopt>>,
|
||||
pub timer: Arc<Mutex<Timer>>,
|
||||
pub hotkey: Arc<Mutex<Hotkey>>,
|
||||
pub runtime: Arc<Mutex<RuntimeResult>>,
|
||||
pub window: Arc<Mutex<Option<Window>>>,
|
||||
pub handle: Arc<Mutex<Handle>>,
|
||||
}
|
||||
|
||||
impl Core {
|
||||
pub fn new() -> Core {
|
||||
Core {
|
||||
clash: Arc::new(Mutex::new(Clash::new())),
|
||||
verge: Arc::new(Mutex::new(Verge::new())),
|
||||
profiles: Arc::new(Mutex::new(Profiles::new())),
|
||||
service: Arc::new(Mutex::new(Service::new())),
|
||||
sysopt: Arc::new(Mutex::new(Sysopt::new())),
|
||||
timer: Arc::new(Mutex::new(Timer::new())),
|
||||
runtime: Arc::new(Mutex::new(RuntimeResult::default())),
|
||||
window: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
pub fn global() -> Core {
|
||||
CORE.clone()
|
||||
}
|
||||
|
||||
/// initialize the core state
|
||||
pub fn init(&self, app_handle: tauri::AppHandle) {
|
||||
let verge = self.verge.lock();
|
||||
let clash_core = verge.clash_core.clone();
|
||||
// kill old clash process
|
||||
Service::kill_old_clash();
|
||||
|
||||
let mut handle = self.handle.lock();
|
||||
handle.set_inner(app_handle.clone());
|
||||
drop(handle);
|
||||
|
||||
let mut service = self.service.lock();
|
||||
service.set_core(clash_core);
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let enable = verge.enable_service_mode.clone();
|
||||
service.set_mode(enable.unwrap_or(false));
|
||||
}
|
||||
|
||||
log_if_err!(service.start());
|
||||
drop(verge);
|
||||
drop(service);
|
||||
|
||||
log_if_err!(self.activate());
|
||||
|
||||
let clash = self.clash.lock();
|
||||
let verge = self.verge.lock();
|
||||
|
||||
// let silent_start = verge.enable_silent_start.clone();
|
||||
let auto_launch = verge.enable_auto_launch.clone();
|
||||
let mut sysopt = self.sysopt.lock();
|
||||
log_if_err!(sysopt.init_launch());
|
||||
log_if_err!(sysopt.init_sysproxy());
|
||||
drop(sysopt);
|
||||
|
||||
sysopt.init_sysproxy(clash.info.port.clone(), &verge);
|
||||
let handle = self.handle.lock();
|
||||
log_if_err!(handle.update_systray());
|
||||
log_if_err!(handle.update_systray_clash());
|
||||
drop(handle);
|
||||
|
||||
drop(clash);
|
||||
drop(verge);
|
||||
|
||||
log_if_err!(sysopt.init_launch(auto_launch));
|
||||
|
||||
log_if_err!(self.update_systray(&app_handle));
|
||||
log_if_err!(self.update_systray_clash(&app_handle));
|
||||
let mut hotkey = self.hotkey.lock();
|
||||
log_if_err!(hotkey.init(app_handle));
|
||||
drop(hotkey);
|
||||
|
||||
// timer initialize
|
||||
let mut timer = self.timer.lock();
|
||||
timer.set_core(self.clone());
|
||||
log_if_err!(timer.restore());
|
||||
}
|
||||
|
||||
/// save the window instance
|
||||
pub fn set_win(&self, win: Option<Window>) {
|
||||
let mut window = self.window.lock();
|
||||
*window = win;
|
||||
}
|
||||
|
||||
/// restart the clash sidecar
|
||||
pub fn restart_clash(&self) -> Result<()> {
|
||||
let mut service = self.service.lock();
|
||||
@@ -116,7 +93,8 @@ impl Core {
|
||||
bail!("invalid clash core name \"{clash_core}\"");
|
||||
}
|
||||
|
||||
let mut verge = self.verge.lock();
|
||||
let global = Data::global();
|
||||
let mut verge = global.verge.lock();
|
||||
verge.patch_config(Verge {
|
||||
clash_core: Some(clash_core.clone()),
|
||||
..Verge::default()
|
||||
@@ -124,9 +102,8 @@ impl Core {
|
||||
drop(verge);
|
||||
|
||||
let mut service = self.service.lock();
|
||||
service.stop()?;
|
||||
service.set_core(Some(clash_core));
|
||||
service.start()?;
|
||||
service.clear_logs();
|
||||
service.restart()?;
|
||||
drop(service);
|
||||
|
||||
self.activate()
|
||||
@@ -134,12 +111,13 @@ impl Core {
|
||||
|
||||
/// Patch Clash
|
||||
/// handle the clash config changed
|
||||
pub fn patch_clash(&self, patch: Mapping, app_handle: &AppHandle) -> Result<()> {
|
||||
pub fn patch_clash(&self, patch: Mapping) -> Result<()> {
|
||||
let has_port = patch.contains_key(&Value::from("mixed-port"));
|
||||
let has_mode = patch.contains_key(&Value::from("mode"));
|
||||
|
||||
let port = {
|
||||
let mut clash = self.clash.lock();
|
||||
let global = Data::global();
|
||||
let mut clash = global.clash.lock();
|
||||
clash.patch_config(patch)?;
|
||||
clash.info.port.clone()
|
||||
};
|
||||
@@ -153,175 +131,132 @@ impl Core {
|
||||
self.activate()?;
|
||||
|
||||
let mut sysopt = self.sysopt.lock();
|
||||
let verge = self.verge.lock();
|
||||
sysopt.init_sysproxy(port, &verge);
|
||||
sysopt.init_sysproxy()?;
|
||||
}
|
||||
|
||||
if has_mode {
|
||||
self.update_systray_clash(app_handle)?;
|
||||
let handle = self.handle.lock();
|
||||
handle.update_systray_clash()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patch Verge
|
||||
pub fn patch_verge(&self, patch: Verge, app_handle: &AppHandle) -> Result<()> {
|
||||
let tun_mode = patch.enable_tun_mode.clone();
|
||||
let auto_launch = patch.enable_auto_launch.clone();
|
||||
let system_proxy = patch.enable_system_proxy.clone();
|
||||
let proxy_bypass = patch.system_proxy_bypass.clone();
|
||||
let proxy_guard = patch.enable_proxy_guard.clone();
|
||||
pub fn patch_verge(&self, patch: Verge) -> Result<()> {
|
||||
// save the patch
|
||||
let global = Data::global();
|
||||
let mut verge = global.verge.lock();
|
||||
verge.patch_config(patch.clone())?;
|
||||
drop(verge);
|
||||
|
||||
#[cfg(windows)]
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let proxy_guard = patch.enable_proxy_guard;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let service_mode = patch.enable_service_mode.clone();
|
||||
let service_mode = patch.enable_service_mode;
|
||||
|
||||
// 重启服务
|
||||
if service_mode.is_some() {
|
||||
let service_mode = service_mode.unwrap();
|
||||
|
||||
let mut service = self.service.lock();
|
||||
service.stop()?;
|
||||
service.set_mode(service_mode);
|
||||
service.start()?;
|
||||
service.restart()?;
|
||||
drop(service);
|
||||
}
|
||||
|
||||
// self.activate_enhanced(false)?;
|
||||
if tun_mode.is_some() && *tun_mode.as_ref().unwrap_or(&false) {
|
||||
let wintun_dll = crate::utils::dirs::app_home_dir().join("wintun.dll");
|
||||
if !wintun_dll.exists() {
|
||||
bail!("failed to enable TUN for missing `wintun.dll`");
|
||||
}
|
||||
}
|
||||
|
||||
if service_mode.is_some() || tun_mode.is_some() {
|
||||
self.activate()?;
|
||||
}
|
||||
}
|
||||
|
||||
if auto_launch.is_some() {
|
||||
let mut sysopt = self.sysopt.lock();
|
||||
sysopt.update_launch(auto_launch)?;
|
||||
}
|
||||
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
||||
let mut sysopt = self.sysopt.lock();
|
||||
sysopt.update_sysproxy(system_proxy.clone(), proxy_bypass)?;
|
||||
sysopt.guard_proxy();
|
||||
}
|
||||
|
||||
if proxy_guard.unwrap_or(false) {
|
||||
let sysopt = self.sysopt.lock();
|
||||
sysopt.guard_proxy();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if tun_mode.is_some() && *tun_mode.as_ref().unwrap_or(&false) {
|
||||
let wintun_dll = crate::utils::dirs::app_home_dir().join("wintun.dll");
|
||||
if !wintun_dll.exists() {
|
||||
bail!("failed to enable TUN for missing `wintun.dll`");
|
||||
}
|
||||
}
|
||||
|
||||
// save the patch
|
||||
let mut verge = self.verge.lock();
|
||||
verge.patch_config(patch)?;
|
||||
drop(verge);
|
||||
|
||||
if system_proxy.is_some() || tun_mode.is_some() {
|
||||
self.update_systray(app_handle)?;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if tun_mode.is_some() {
|
||||
self.activate()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let mut sysopt = self.sysopt.lock();
|
||||
|
||||
// update system tray state (clash config)
|
||||
pub fn update_systray_clash(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let clash = self.clash.lock();
|
||||
let mode = clash
|
||||
.config
|
||||
.get(&Value::from("mode"))
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
.unwrap_or("rule");
|
||||
if auto_launch.is_some() {
|
||||
sysopt.update_launch()?;
|
||||
}
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
||||
sysopt.update_sysproxy()?;
|
||||
sysopt.guard_proxy();
|
||||
}
|
||||
if proxy_guard.unwrap_or(false) {
|
||||
sysopt.guard_proxy();
|
||||
}
|
||||
|
||||
let tray = app_handle.tray_handle();
|
||||
if system_proxy.is_some() || tun_mode.is_some() {
|
||||
let handle = self.handle.lock();
|
||||
handle.update_systray()?;
|
||||
}
|
||||
|
||||
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, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = self.verge.lock();
|
||||
let tray = app_handle.tray_handle();
|
||||
|
||||
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
|
||||
let window = app_handle.get_window("main");
|
||||
let notice = Notice::from(window);
|
||||
notice.refresh_verge();
|
||||
if patch.hotkeys.is_some() {
|
||||
let mut hotkey = self.hotkey.lock();
|
||||
hotkey.update(patch.hotkeys.unwrap())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// update rule/global/direct/script mode
|
||||
pub fn update_mode(&self, app_handle: &AppHandle, mode: &str) -> Result<()> {
|
||||
pub fn update_mode(&self, mode: &str) -> Result<()> {
|
||||
// save config to file
|
||||
let mut clash = self.clash.lock();
|
||||
clash.config.insert(Value::from("mode"), Value::from(mode));
|
||||
clash.save_config()?;
|
||||
|
||||
let info = clash.info.clone();
|
||||
drop(clash);
|
||||
|
||||
let notice = {
|
||||
let window = self.window.lock();
|
||||
Notice::from(window.clone())
|
||||
let info = {
|
||||
let global = Data::global();
|
||||
let mut clash = global.clash.lock();
|
||||
clash.config.insert(Value::from("mode"), Value::from(mode));
|
||||
clash.save_config()?;
|
||||
clash.info.clone()
|
||||
};
|
||||
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), Value::from(mode));
|
||||
|
||||
let service = self.service.lock();
|
||||
service.patch_config(info, mapping, notice)?;
|
||||
let handle = self.handle.clone();
|
||||
|
||||
// update tray
|
||||
self.update_systray_clash(app_handle)?;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log_if_err!(Service::patch_config(info, mapping.to_owned()).await);
|
||||
|
||||
// update tray
|
||||
let handle = handle.lock();
|
||||
handle.refresh_clash();
|
||||
log_if_err!(handle.update_systray_clash());
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// activate the profile
|
||||
/// auto activate enhanced profile
|
||||
/// 触发clash配置更新
|
||||
pub fn activate(&self) -> Result<()> {
|
||||
let profile_activate = {
|
||||
let profiles = self.profiles.lock();
|
||||
profiles.gen_activate()?
|
||||
};
|
||||
let global = Data::global();
|
||||
|
||||
let (clash_config, clash_info) = {
|
||||
let clash = self.clash.lock();
|
||||
(clash.config.clone(), clash.info.clone())
|
||||
};
|
||||
let verge = global.verge.lock();
|
||||
let clash = global.clash.lock();
|
||||
let profiles = global.profiles.lock();
|
||||
|
||||
let tun_mode = {
|
||||
let verge = self.verge.lock();
|
||||
verge.enable_tun_mode.unwrap_or(false)
|
||||
};
|
||||
let tun_mode = verge.enable_tun_mode.clone().unwrap_or(false);
|
||||
let profile_activate = profiles.gen_activate()?;
|
||||
|
||||
let clash_config = clash.config.clone();
|
||||
let clash_info = clash.info.clone();
|
||||
|
||||
drop(clash);
|
||||
drop(verge);
|
||||
drop(profiles);
|
||||
|
||||
let (config, exists_keys, logs) = enhance_config(
|
||||
clash_config,
|
||||
@@ -332,25 +267,44 @@ impl Core {
|
||||
);
|
||||
|
||||
let mut runtime = self.runtime.lock();
|
||||
runtime.config = Some(config.clone());
|
||||
runtime.config_yaml = Some(serde_yaml::to_string(&config).unwrap_or("".into()));
|
||||
runtime.exists_keys = exists_keys;
|
||||
runtime.chain_logs = logs;
|
||||
|
||||
let notice = {
|
||||
let window = self.window.lock();
|
||||
Notice::from(window.clone())
|
||||
*runtime = RuntimeResult {
|
||||
config: Some(config.clone()),
|
||||
config_yaml: Some(serde_yaml::to_string(&config).unwrap_or("".into())),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
};
|
||||
drop(runtime);
|
||||
|
||||
let service = self.service.lock();
|
||||
service.set_config(clash_info, config, notice)
|
||||
let mut service = self.service.lock();
|
||||
service.check_start()?;
|
||||
drop(service);
|
||||
|
||||
let handle = self.handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match Service::set_config(clash_info, config).await {
|
||||
Ok(_) => {
|
||||
let handle = handle.lock();
|
||||
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}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Static function
|
||||
/// update profile item
|
||||
pub async fn update_profile_item(&self, uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
let global = Data::global();
|
||||
|
||||
let (url, opt) = {
|
||||
let profiles = self.profiles.lock();
|
||||
let profiles = global.profiles.lock();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
|
||||
if let Some(typ) = item.itype.as_ref() {
|
||||
@@ -359,7 +313,7 @@ impl Core {
|
||||
// reactivate the config
|
||||
if Some(uid) == profiles.get_current() {
|
||||
drop(profiles);
|
||||
return self.activate();
|
||||
self.activate()?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -373,7 +327,7 @@ impl Core {
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
let mut profiles = self.profiles.lock();
|
||||
let mut profiles = global.profiles.lock();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
// reactivate the profile
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use crate::log_if_err;
|
||||
use tauri::Window;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Notice {
|
||||
win: Option<Window>,
|
||||
}
|
||||
|
||||
impl Notice {
|
||||
pub fn from(win: Option<Window>) -> Notice {
|
||||
Notice { win }
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn set_win(&mut self, win: Option<Window>) {
|
||||
self.win = win;
|
||||
}
|
||||
|
||||
pub fn refresh_clash(&self) {
|
||||
if let Some(window) = self.win.as_ref() {
|
||||
log_if_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_verge(&self) {
|
||||
if let Some(window) = self.win.as_ref() {
|
||||
log_if_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn refresh_profiles(&self) {
|
||||
if let Some(window) = self.win.as_ref() {
|
||||
log_if_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
-100
@@ -1,83 +1,92 @@
|
||||
use super::{notice::Notice, ClashInfo};
|
||||
use crate::data::{ClashInfo, Data};
|
||||
use crate::log_if_err;
|
||||
use crate::utils::{config, dirs};
|
||||
use anyhow::{bail, Result};
|
||||
use parking_lot::RwLock;
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tokio::time::sleep;
|
||||
|
||||
static mut CLASH_CORE: &str = "clash";
|
||||
const LOGS_QUEUE_LEN: usize = 100;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Service {
|
||||
sidecar: Option<CommandChild>,
|
||||
|
||||
#[allow(unused)]
|
||||
service_mode: bool,
|
||||
logs: Arc<RwLock<VecDeque<String>>>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new() -> Service {
|
||||
let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10);
|
||||
|
||||
Service {
|
||||
sidecar: None,
|
||||
service_mode: false,
|
||||
logs: Arc::new(RwLock::new(queue)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_core(&mut self, clash_core: Option<String>) {
|
||||
unsafe {
|
||||
CLASH_CORE = Box::leak(clash_core.unwrap_or("clash".into()).into_boxed_str());
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn set_mode(&mut self, enable: bool) {
|
||||
self.service_mode = enable;
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
self.start_clash_by_sidecar()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
self.start_clash_by_sidecar()?;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
if !self.service_mode {
|
||||
return self.start_clash_by_sidecar();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let enable = {
|
||||
let data = Data::global();
|
||||
let verge = data.verge.lock();
|
||||
verge.enable_service_mode.clone().unwrap_or(false)
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match Self::check_service().await {
|
||||
Ok(status) => {
|
||||
// 未启动clash
|
||||
if status.code != 0 {
|
||||
log_if_err!(Self::start_clash_by_service().await);
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
if !enable {
|
||||
return self.start_clash_by_sidecar();
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match Self::check_service().await {
|
||||
Ok(status) => {
|
||||
// 未启动clash
|
||||
if status.code != 0 {
|
||||
log_if_err!(Self::start_clash_by_service().await);
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
self.stop_clash_by_sidecar()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
self.stop_clash_by_sidecar()?;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
if !self.service_mode {
|
||||
return self.stop_clash_by_sidecar();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
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 {
|
||||
log_if_err!(Self::stop_clash_by_service().await);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log_if_err!(Self::stop_clash_by_service().await);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -86,30 +95,83 @@ impl Service {
|
||||
self.start()
|
||||
}
|
||||
|
||||
pub fn get_logs(&self) -> VecDeque<String> {
|
||||
self.logs.read().clone()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn set_logs(&self, text: String) {
|
||||
let mut logs = self.logs.write();
|
||||
if logs.len() > LOGS_QUEUE_LEN {
|
||||
(*logs).pop_front();
|
||||
}
|
||||
(*logs).push_back(text);
|
||||
}
|
||||
|
||||
pub fn clear_logs(&self) {
|
||||
let mut logs = self.logs.write();
|
||||
(*logs).clear();
|
||||
}
|
||||
|
||||
/// start the clash sidecar
|
||||
fn start_clash_by_sidecar(&mut self) -> Result<()> {
|
||||
if self.sidecar.is_some() {
|
||||
bail!("could not run clash sidecar twice");
|
||||
let sidecar = self.sidecar.take().unwrap();
|
||||
let _ = sidecar.kill();
|
||||
}
|
||||
|
||||
let clash_core: String = {
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
verge.clash_core.clone().unwrap_or("clash".into())
|
||||
};
|
||||
|
||||
let app_dir = dirs::app_home_dir();
|
||||
let app_dir = app_dir.as_os_str().to_str().unwrap();
|
||||
|
||||
let clash_core = unsafe { CLASH_CORE };
|
||||
// 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();
|
||||
log_if_err!(|| -> Result<()> {
|
||||
let path = dirs::clash_pid_path();
|
||||
fs::File::create(path)?.write(format!("{pid}").as_bytes())?;
|
||||
Ok(())
|
||||
}());
|
||||
|
||||
self.sidecar = Some(cmd_child);
|
||||
|
||||
// clash log
|
||||
let logs = self.logs.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let write_log = |text: String| {
|
||||
let mut logs = logs.write();
|
||||
if logs.len() >= LOGS_QUEUE_LEN {
|
||||
(*logs).pop_front();
|
||||
}
|
||||
(*logs).push_back(text);
|
||||
};
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
let stdout = if line.len() > 33 { &line[33..] } else { &line };
|
||||
let can_short = line.starts_with("time=") && line.len() > 33;
|
||||
let stdout = if can_short { &line[33..] } else { &line };
|
||||
log::info!(target: "app" ,"[clash]: {}", stdout);
|
||||
write_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(err) => {
|
||||
log::error!(target: "app" ,"[clash error]: {}", err);
|
||||
write_log(err);
|
||||
}
|
||||
CommandEvent::Stderr(err) => log::error!(target: "app" ,"[clash error]: {}", err),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -126,70 +188,77 @@ impl Service {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update clash config
|
||||
/// using PUT methods
|
||||
pub fn set_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
|
||||
if !self.service_mode && self.sidecar.is_none() {
|
||||
bail!("did not start sidecar");
|
||||
pub fn check_start(&mut self) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
let service_mode = verge.enable_service_mode.unwrap_or(false);
|
||||
|
||||
if !service_mode && self.sidecar.is_none() {
|
||||
self.start()?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if self.sidecar.is_none() {
|
||||
self.start()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update clash config
|
||||
/// using PUT methods
|
||||
pub async fn set_config(info: ClashInfo, config: Mapping) -> Result<()> {
|
||||
let temp_path = dirs::profiles_temp_path();
|
||||
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
|
||||
|
||||
let (server, headers) = Self::clash_client_info(info)?;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
||||
|
||||
// retry 5 times
|
||||
for _ in 0..5 {
|
||||
match reqwest::ClientBuilder::new().no_proxy().build() {
|
||||
Ok(client) => {
|
||||
let builder = client.put(&server).headers(headers.clone()).json(&data);
|
||||
|
||||
match builder.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status() != 204 {
|
||||
log::error!(target: "app", "failed to activate clash with status \"{}\"", resp.status());
|
||||
}
|
||||
|
||||
notice.refresh_clash();
|
||||
|
||||
// do not retry
|
||||
break;
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "failed to activate for `{err}`"),
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "failed to activate for `{err}`"),
|
||||
macro_rules! report_err {
|
||||
($i: expr, $e: expr) => {
|
||||
match $i {
|
||||
4 => bail!($e),
|
||||
_ => log::error!(target: "app", $e),
|
||||
}
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
};
|
||||
}
|
||||
|
||||
// retry 5 times
|
||||
for i in 0..5 {
|
||||
let headers = headers.clone();
|
||||
match reqwest::ClientBuilder::new().no_proxy().build() {
|
||||
Ok(client) => {
|
||||
let builder = client.put(&server).headers(headers).json(&data);
|
||||
match builder.send().await {
|
||||
Ok(resp) => match resp.status().as_u16() {
|
||||
204 => break,
|
||||
// 配置有问题不重试
|
||||
400 => bail!("failed to update clash config with status 400"),
|
||||
status @ _ => report_err!(i, "failed to activate clash with status \"{status}\""),
|
||||
},
|
||||
Err(err) => report_err!(i, "{err}"),
|
||||
}
|
||||
}
|
||||
Err(err) => report_err!(i, "{err}"),
|
||||
}
|
||||
});
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// patch clash config
|
||||
pub fn patch_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
|
||||
if !self.service_mode && self.sidecar.is_none() {
|
||||
bail!("did not start sidecar");
|
||||
}
|
||||
|
||||
pub async fn patch_config(info: ClashInfo, config: Mapping) -> Result<()> {
|
||||
let (server, headers) = Self::clash_client_info(info)?;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Ok(client) = reqwest::ClientBuilder::new().no_proxy().build() {
|
||||
let builder = client.patch(&server).headers(headers.clone()).json(&config);
|
||||
|
||||
match builder.send().await {
|
||||
Ok(_) => notice.refresh_clash(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.patch(&server).headers(headers.clone()).json(&config);
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -217,6 +286,23 @@ impl Service {
|
||||
|
||||
Ok((server, headers))
|
||||
}
|
||||
|
||||
/// kill old clash process
|
||||
pub fn kill_old_clash() {
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
|
||||
if let Ok(pid) = fs::read(dirs::clash_pid_path()) {
|
||||
if let Ok(pid) = String::from_utf8_lossy(&pid).parse() {
|
||||
let mut system = System::new();
|
||||
system.refresh_all();
|
||||
|
||||
let proc = system.process(Pid::from_u32(pid));
|
||||
if let Some(proc) = proc {
|
||||
proc.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Service {
|
||||
@@ -227,7 +313,7 @@ impl Drop for Service {
|
||||
|
||||
/// ### Service Mode
|
||||
///
|
||||
#[cfg(windows)]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod win_service {
|
||||
use super::*;
|
||||
use anyhow::Context;
|
||||
@@ -384,7 +470,12 @@ pub mod win_service {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
let clash_core = unsafe { CLASH_CORE };
|
||||
let clash_core = {
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
verge.clash_core.clone().unwrap_or("clash".into())
|
||||
};
|
||||
|
||||
let clash_bin = format!("{clash_core}.exe");
|
||||
let bin_path = current_exe().unwrap().with_file_name(clash_bin);
|
||||
let bin_path = bin_path.as_os_str().to_str().unwrap();
|
||||
|
||||
+158
-87
@@ -1,17 +1,17 @@
|
||||
use super::{Clash, Verge};
|
||||
use crate::{log_if_err, utils::sysopt::SysProxyConfig};
|
||||
use anyhow::{bail, Result};
|
||||
use crate::{data::*, log_if_err};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||
use std::sync::Arc;
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::{async_runtime::Mutex, utils::platform::current_exe};
|
||||
|
||||
pub struct Sysopt {
|
||||
/// current system proxy setting
|
||||
cur_sysproxy: Option<SysProxyConfig>,
|
||||
cur_sysproxy: Option<Sysproxy>,
|
||||
|
||||
/// record the original system proxy
|
||||
/// recover it when exit
|
||||
old_sysproxy: Option<SysProxyConfig>,
|
||||
old_sysproxy: Option<Sysproxy>,
|
||||
|
||||
/// helps to auto launch the app
|
||||
auto_launch: Option<AutoLaunch>,
|
||||
@@ -20,6 +20,13 @@ pub struct Sysopt {
|
||||
guard_state: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
|
||||
|
||||
impl Sysopt {
|
||||
pub fn new() -> Sysopt {
|
||||
Sysopt {
|
||||
@@ -31,124 +38,181 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
pub fn init_sysproxy(&mut self, port: Option<String>, verge: &Verge) {
|
||||
if let Some(port) = port {
|
||||
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
|
||||
pub fn init_sysproxy(&mut self) -> Result<()> {
|
||||
let data = Data::global();
|
||||
let clash = data.clash.lock();
|
||||
let port = clash.info.port.clone();
|
||||
|
||||
self.old_sysproxy = match SysProxyConfig::get_sys() {
|
||||
Ok(proxy) => Some(proxy),
|
||||
Err(_) => None,
|
||||
};
|
||||
if port.is_none() {
|
||||
bail!("clash port is none");
|
||||
}
|
||||
|
||||
let bypass = verge.system_proxy_bypass.clone();
|
||||
let sysproxy = SysProxyConfig::new(enable, port, bypass);
|
||||
let verge = data.verge.lock();
|
||||
|
||||
if enable {
|
||||
if let Err(err) = sysproxy.set_sys() {
|
||||
log::error!(target: "app", "failed to set system proxy for `{err}`");
|
||||
}
|
||||
}
|
||||
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
|
||||
let bypass = verge.system_proxy_bypass.clone();
|
||||
let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
let port = port.unwrap().parse::<u16>()?;
|
||||
let host = String::from("127.0.0.1");
|
||||
|
||||
self.cur_sysproxy = Some(Sysproxy {
|
||||
enable,
|
||||
host,
|
||||
port,
|
||||
bypass,
|
||||
});
|
||||
|
||||
if enable {
|
||||
self.old_sysproxy = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
|
||||
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
|
||||
}
|
||||
|
||||
// launchs the system proxy guard
|
||||
self.guard_proxy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the system proxy
|
||||
/// when the verge config is changed
|
||||
pub fn update_sysproxy(&mut self, enable: Option<bool>, bypass: Option<String>) -> Result<()> {
|
||||
let sysproxy = self.cur_sysproxy.take();
|
||||
|
||||
if sysproxy.is_none() {
|
||||
bail!("unhandle error for sysproxy is none");
|
||||
pub fn update_sysproxy(&mut self) -> Result<()> {
|
||||
if self.cur_sysproxy.is_none() {
|
||||
return self.init_sysproxy();
|
||||
}
|
||||
|
||||
let mut sysproxy = sysproxy.unwrap();
|
||||
let data = Data::global();
|
||||
let verge = data.verge.lock();
|
||||
|
||||
if let Some(enable) = enable {
|
||||
sysproxy.enable = enable;
|
||||
}
|
||||
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
|
||||
let bypass = verge.system_proxy_bypass.clone();
|
||||
let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||
|
||||
if let Some(bypass) = bypass {
|
||||
sysproxy.bypass = bypass;
|
||||
}
|
||||
let mut sysproxy = self.cur_sysproxy.take().unwrap();
|
||||
|
||||
sysproxy.enable = enable;
|
||||
sysproxy.bypass = bypass;
|
||||
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
|
||||
if self.cur_sysproxy.as_ref().unwrap().set_sys().is_err() {
|
||||
bail!("failed to set system proxy");
|
||||
}
|
||||
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
pub fn reset_sysproxy(&mut self) {
|
||||
if let Some(sysproxy) = self.old_sysproxy.take() {
|
||||
// 如果原代理设置是开启的,且域名端口设置和当前的一致,就不恢复原设置
|
||||
// https://github.com/zzzgydi/clash-verge/issues/157
|
||||
if let Some(cur) = self.cur_sysproxy.as_ref() {
|
||||
if sysproxy.enable && cur.server == sysproxy.server {
|
||||
return;
|
||||
pub fn reset_sysproxy(&mut self) -> Result<()> {
|
||||
if self.cur_sysproxy.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cur = self.cur_sysproxy.take().unwrap();
|
||||
|
||||
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()?;
|
||||
}
|
||||
}
|
||||
|
||||
match sysproxy.set_sys() {
|
||||
Ok(_) => self.cur_sysproxy = None,
|
||||
Err(_) => log::error!(target: "app", "failed to reset proxy"),
|
||||
None => {
|
||||
if cur.enable {
|
||||
cur.enable = false;
|
||||
cur.set_system_proxy()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get current proxy
|
||||
pub fn get_sysproxy(&self) -> Result<Option<SysProxyConfig>> {
|
||||
Ok(self.cur_sysproxy.clone())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
pub fn init_launch(&mut self, enable: Option<bool>) -> Result<()> {
|
||||
let app_exe = current_exe().unwrap();
|
||||
let app_exe = dunce::canonicalize(app_exe).unwrap();
|
||||
let app_name = app_exe.file_stem().unwrap().to_str().unwrap();
|
||||
let app_path = app_exe.as_os_str().to_str().unwrap();
|
||||
pub fn init_launch(&mut self) -> Result<()> {
|
||||
let data = Data::global();
|
||||
let verge = data.verge.lock();
|
||||
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_name = app_exe
|
||||
.file_stem()
|
||||
.and_then(|f| f.to_str())
|
||||
.ok_or(anyhow!("failed to get file stem"))?;
|
||||
|
||||
let app_path = app_exe
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow!("failed to get app_path"))?
|
||||
.to_string();
|
||||
|
||||
// fix issue #26
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = app_path.as_str();
|
||||
|
||||
// use the /Applications/Clash Verge.app path
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
let path = path.parent()?.parent()?.parent()?;
|
||||
let extension = path.extension()?.to_str()?;
|
||||
match extension == "app" {
|
||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||
false => None,
|
||||
}
|
||||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(app_path)
|
||||
.set_app_path(&app_path)
|
||||
.build()?;
|
||||
|
||||
if let Some(enable) = enable {
|
||||
// fix issue #26
|
||||
self.auto_launch = Some(auto);
|
||||
|
||||
// 避免在开发时将自启动关了
|
||||
#[cfg(feature = "verge-dev")]
|
||||
if !enable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto = self.auto_launch.as_ref().unwrap();
|
||||
|
||||
// macos每次启动都更新登录项,避免重复设置登录项
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = auto.disable();
|
||||
if enable {
|
||||
auto.enable()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.auto_launch = Some(auto);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
match enable {
|
||||
true => auto.enable()?,
|
||||
false => auto.disable()?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the startup
|
||||
pub fn update_launch(&mut self, enable: Option<bool>) -> Result<()> {
|
||||
if enable.is_none() {
|
||||
return Ok(());
|
||||
pub fn update_launch(&mut self) -> Result<()> {
|
||||
if self.auto_launch.is_none() {
|
||||
return self.init_launch();
|
||||
}
|
||||
|
||||
let enable = enable.unwrap();
|
||||
let data = Data::global();
|
||||
let verge = data.verge.lock();
|
||||
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
|
||||
|
||||
let auto_launch = self.auto_launch.as_ref().unwrap();
|
||||
|
||||
match enable {
|
||||
true => auto_launch.enable()?,
|
||||
false => auto_launch.disable()?,
|
||||
false => crate::log_if_err!(auto_launch.disable()), // 忽略关闭的错误
|
||||
};
|
||||
|
||||
Ok(())
|
||||
@@ -176,35 +240,42 @@ impl Sysopt {
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
let verge = Verge::new();
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
|
||||
let enable_proxy = verge.enable_system_proxy.unwrap_or(false);
|
||||
let enable_guard = verge.enable_proxy_guard.unwrap_or(false);
|
||||
let guard_duration = verge.proxy_guard_duration.unwrap_or(10);
|
||||
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
|
||||
let guard = verge.enable_proxy_guard.clone().unwrap_or(false);
|
||||
let guard_duration = verge.proxy_guard_duration.clone().unwrap_or(10);
|
||||
let bypass = verge.system_proxy_bypass.clone();
|
||||
drop(verge);
|
||||
|
||||
// stop loop
|
||||
if !enable || !guard {
|
||||
break;
|
||||
}
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
// stop loop
|
||||
if !enable_guard || !enable_proxy {
|
||||
break;
|
||||
}
|
||||
let clash = global.clash.lock();
|
||||
let port = clash.info.port.clone();
|
||||
let port = port.unwrap_or("".into()).parse::<u16>();
|
||||
drop(clash);
|
||||
|
||||
log::debug!(target: "app", "try to guard the system proxy");
|
||||
|
||||
let clash = Clash::new();
|
||||
match port {
|
||||
Ok(port) => {
|
||||
let sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
host: "127.0.0.1".into(),
|
||||
port,
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
};
|
||||
|
||||
match &clash.info.port {
|
||||
Some(port) => {
|
||||
let bypass = verge.system_proxy_bypass.clone();
|
||||
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
|
||||
|
||||
log_if_err!(sysproxy.set_sys());
|
||||
}
|
||||
None => {
|
||||
let status = &clash.info.status;
|
||||
log::error!(target: "app", "failed to parse clash port with status {status}")
|
||||
log_if_err!(sysproxy.set_system_proxy());
|
||||
}
|
||||
Err(_) => log::error!(target: "app", "failed to parse clash port"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::Core;
|
||||
use crate::log_if_err;
|
||||
use crate::utils::help::get_now;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::{data::Data, log_if_err};
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -16,9 +16,6 @@ pub struct Timer {
|
||||
|
||||
/// increment id
|
||||
timer_count: TaskID,
|
||||
|
||||
/// save the instance of the app
|
||||
core: Option<Core>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
@@ -27,20 +24,11 @@ impl Timer {
|
||||
delay_timer: DelayTimerBuilder::default().build(),
|
||||
timer_map: HashMap::new(),
|
||||
timer_count: 1,
|
||||
core: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_core(&mut self, core: Core) {
|
||||
self.core = Some(core);
|
||||
}
|
||||
|
||||
/// Correctly update all cron tasks
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
if self.core.is_none() {
|
||||
bail!("unhandle error for core is none");
|
||||
}
|
||||
|
||||
let diff_map = self.gen_diff();
|
||||
|
||||
for (uid, diff) in diff_map.into_iter() {
|
||||
@@ -69,7 +57,9 @@ impl Timer {
|
||||
self.refresh()?;
|
||||
|
||||
let cur_timestamp = get_now(); // seconds
|
||||
let profiles = self.core.as_ref().unwrap().profiles.lock();
|
||||
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
|
||||
profiles
|
||||
.get_items()
|
||||
@@ -94,7 +84,8 @@ impl Timer {
|
||||
|
||||
/// generate a uid -> update_interval map
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let profiles = self.core.as_ref().unwrap().profiles.lock();
|
||||
let global = Data::global();
|
||||
let profiles = global.profiles.lock();
|
||||
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
@@ -148,14 +139,14 @@ impl Timer {
|
||||
|
||||
/// add a cron task
|
||||
fn add_task(&self, uid: String, tid: TaskID, minutes: u64) -> Result<()> {
|
||||
let core = self.core.clone().unwrap();
|
||||
let core = Core::global();
|
||||
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_repeated_by_minutes(minutes)
|
||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||
.spawn_async_routine(move || Self::async_task(core.clone(), uid.clone()))
|
||||
.spawn_async_routine(move || Self::async_task(core.to_owned(), uid.to_owned()))
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
self
|
||||
|
||||
@@ -106,6 +106,7 @@ impl ClashInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Clash {
|
||||
/// maintain the clash config
|
||||
pub config: Mapping,
|
||||
@@ -124,7 +125,7 @@ impl Clash {
|
||||
|
||||
/// get clash config
|
||||
pub fn read_config() -> Mapping {
|
||||
config::read_yaml::<Mapping>(dirs::clash_path())
|
||||
config::read_merge_mapping(dirs::clash_path())
|
||||
}
|
||||
|
||||
/// save the clash config
|
||||
@@ -0,0 +1,32 @@
|
||||
mod clash;
|
||||
mod prfitem;
|
||||
mod profiles;
|
||||
mod verge;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::prfitem::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::verge::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
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>>,
|
||||
pub verge: Arc<Mutex<Verge>>,
|
||||
pub profiles: Arc<Mutex<Profiles>>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn global() -> Data {
|
||||
DATA.clone()
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ impl PrfItem {
|
||||
}),
|
||||
"merge" => Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Merge(config::read_yaml::<Mapping>(path)),
|
||||
data: ChainType::Merge(config::read_merge_mapping(path)),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
@@ -283,7 +283,7 @@ impl Profiles {
|
||||
bail!("failed to read the file \"{}\"", file_path.display());
|
||||
}
|
||||
|
||||
return Ok(config::read_yaml::<Mapping>(file_path.clone()));
|
||||
return Ok(config::read_merge_mapping(file_path.clone()));
|
||||
}
|
||||
}
|
||||
bail!("failed to find current profile \"uid:{current}\"");
|
||||
@@ -5,10 +5,14 @@ use serde::{Deserialize, Serialize};
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Verge {
|
||||
/// app listening port
|
||||
/// for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
/// `light` or `dark`
|
||||
/// `light` or `dark` or `system`
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
/// enable blur mode
|
||||
@@ -52,6 +56,10 @@ pub struct Verge {
|
||||
/// clash core path
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub clash_core: Option<String>,
|
||||
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -112,6 +120,7 @@ impl Verge {
|
||||
patch!(theme_setting);
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
|
||||
self.save_file()
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::core::*;
|
||||
use crate::data::*;
|
||||
use crate::log_if_err;
|
||||
|
||||
// 切换模式
|
||||
pub fn change_clash_mode(mode: &str) {
|
||||
let core = Core::global();
|
||||
log_if_err!(core.update_mode(mode));
|
||||
}
|
||||
|
||||
// 切换系统代理
|
||||
pub fn toggle_system_proxy() {
|
||||
let core = Core::global();
|
||||
let data = Data::global();
|
||||
|
||||
let verge = data.verge.lock();
|
||||
let enable = !verge.enable_system_proxy.clone().unwrap_or(false);
|
||||
drop(verge);
|
||||
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_system_proxy: Some(enable),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
|
||||
// 打开系统代理
|
||||
pub fn enable_system_proxy() {
|
||||
let core = Core::global();
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_system_proxy: Some(true),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
|
||||
// 关闭系统代理
|
||||
pub fn disable_system_proxy() {
|
||||
let core = Core::global();
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_system_proxy: Some(false),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let core = Core::global();
|
||||
let data = Data::global();
|
||||
|
||||
let verge = data.verge.lock();
|
||||
let enable = !verge.enable_tun_mode.clone().unwrap_or(false);
|
||||
drop(verge);
|
||||
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_tun_mode: Some(enable),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
|
||||
// 打开tun模式
|
||||
pub fn enable_tun_mode() {
|
||||
let core = Core::global();
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_tun_mode: Some(true),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
|
||||
// 关闭tun模式
|
||||
pub fn disable_tun_mode() {
|
||||
let core = Core::global();
|
||||
log_if_err!(core.patch_verge(Verge {
|
||||
enable_tun_mode: Some(false),
|
||||
..Verge::default()
|
||||
}));
|
||||
|
||||
let handle = core.handle.lock();
|
||||
let _ = handle.refresh_verge();
|
||||
}
|
||||
+54
-61
@@ -6,10 +6,12 @@
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod core;
|
||||
mod data;
|
||||
mod feat;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
core::Verge,
|
||||
data::Verge,
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use tauri::{
|
||||
@@ -17,15 +19,18 @@ use tauri::{
|
||||
};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
{
|
||||
let verge = Verge::new();
|
||||
|
||||
if server::check_singleton(verge.app_singleton_port).is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
use crate::utils::dirs;
|
||||
|
||||
dirs::init_portable_flag();
|
||||
}
|
||||
|
||||
@@ -55,48 +60,15 @@ fn main() -> std::io::Result<()> {
|
||||
}
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
let core = app_handle.state::<core::Core>();
|
||||
crate::log_if_err!(core.update_mode(app_handle, mode));
|
||||
}
|
||||
"system_proxy" => {
|
||||
let core = app_handle.state::<core::Core>();
|
||||
|
||||
let new_value = {
|
||||
let verge = core.verge.lock();
|
||||
!verge.enable_system_proxy.clone().unwrap_or(false)
|
||||
};
|
||||
|
||||
let patch = Verge {
|
||||
enable_system_proxy: Some(new_value),
|
||||
..Verge::default()
|
||||
};
|
||||
|
||||
crate::log_if_err!(core.patch_verge(patch, app_handle));
|
||||
}
|
||||
"tun_mode" => {
|
||||
let core = app_handle.state::<core::Core>();
|
||||
|
||||
let new_value = {
|
||||
let verge = core.verge.lock();
|
||||
!verge.enable_tun_mode.clone().unwrap_or(false)
|
||||
};
|
||||
|
||||
let patch = Verge {
|
||||
enable_tun_mode: Some(new_value),
|
||||
..Verge::default()
|
||||
};
|
||||
|
||||
crate::log_if_err!(core.patch_verge(patch, app_handle));
|
||||
}
|
||||
"restart_clash" => {
|
||||
let core = app_handle.state::<core::Core>();
|
||||
crate::log_if_err!(core.restart_clash());
|
||||
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);
|
||||
resolve::resolve_reset();
|
||||
app_handle.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
@@ -110,7 +82,6 @@ fn main() -> std::io::Result<()> {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmds::get_sys_proxy,
|
||||
cmds::get_cur_proxy,
|
||||
cmds::open_app_dir,
|
||||
cmds::open_logs_dir,
|
||||
cmds::open_web_url,
|
||||
@@ -118,6 +89,7 @@ fn main() -> std::io::Result<()> {
|
||||
cmds::restart_sidecar,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::get_clash_logs,
|
||||
cmds::patch_clash_config,
|
||||
cmds::change_clash_core,
|
||||
cmds::get_runtime_config,
|
||||
@@ -127,6 +99,7 @@ fn main() -> std::io::Result<()> {
|
||||
// verge
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
cmds::update_hotkeys,
|
||||
// profile
|
||||
cmds::view_profile,
|
||||
cmds::patch_profile,
|
||||
@@ -166,26 +139,46 @@ fn main() -> std::io::Result<()> {
|
||||
builder = builder.menu(Menu::new().add_submenu(submenu_file));
|
||||
}
|
||||
|
||||
let mut context = tauri::generate_context!();
|
||||
let verge = Verge::new();
|
||||
for win in context.config_mut().tauri.windows.iter_mut() {
|
||||
if verge.enable_silent_start.unwrap_or(false) {
|
||||
win.visible = false;
|
||||
#[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();
|
||||
app_handle.exit(0);
|
||||
})
|
||||
.expect("error while exiting.");
|
||||
|
||||
#[allow(unused)]
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
}
|
||||
builder
|
||||
.build(context)
|
||||
.expect("error while running tauri application")
|
||||
.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
app_handle.get_window("main").map(|win| {
|
||||
let _ = win.hide();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset(app_handle);
|
||||
api::process::kill_children();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
/// read data from yaml as struct T
|
||||
@@ -20,6 +21,29 @@ pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
|
||||
}
|
||||
}
|
||||
|
||||
/// read mapping from yaml fix #165
|
||||
pub fn read_merge_mapping(path: PathBuf) -> Mapping {
|
||||
let map = Mapping::new();
|
||||
|
||||
if !path.exists() {
|
||||
log::error!(target: "app", "file not found \"{}\"", path.display());
|
||||
return map;
|
||||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(&path).unwrap_or("".into());
|
||||
|
||||
match serde_yaml::from_str::<Value>(&yaml_str) {
|
||||
Ok(mut val) => {
|
||||
crate::log_if_err!(val.apply_merge());
|
||||
val.as_mapping().unwrap_or(&map).to_owned()
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!(target: "app", "failed to read yaml file \"{}\"", path.display());
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// save the data to the file
|
||||
/// can set `prefix` string to add some comments
|
||||
pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||
|
||||
@@ -15,7 +15,6 @@ static VERGE_CONFIG: &str = "verge.yaml";
|
||||
static PROFILE_YAML: &str = "profiles.yaml";
|
||||
static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
|
||||
|
||||
#[cfg(windows)]
|
||||
static mut RESOURCE_DIR: Option<PathBuf> = None;
|
||||
|
||||
/// portable flag
|
||||
@@ -65,7 +64,6 @@ pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
|
||||
.unwrap()
|
||||
.join("resources");
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
RESOURCE_DIR = Some(res_dir.clone());
|
||||
}
|
||||
@@ -103,6 +101,10 @@ pub fn profiles_temp_path() -> PathBuf {
|
||||
return app_home_dir().join(PROFILE_TEMP);
|
||||
}
|
||||
|
||||
pub fn clash_pid_path() -> PathBuf {
|
||||
unsafe { RESOURCE_DIR.clone().unwrap().join("clash.pid") }
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
static SERVICE_PATH: &str = "clash-verge-service.exe";
|
||||
|
||||
|
||||
@@ -58,22 +58,20 @@ pub fn open_file(path: PathBuf) -> Result<()> {
|
||||
.arg(path)
|
||||
.spawn()
|
||||
{
|
||||
bail!(format!("failed to open file by VScode for `{err}`"));
|
||||
bail!("failed to open file by VScode for `{err}`");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err(err) = Command::new(code).arg(path).spawn() {
|
||||
bail!(format!("failed to open file by VScode for `{err}`"));
|
||||
bail!("failed to open file by VScode for `{err}`");
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match open::that(path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => bail!(format!("failed to open file for `{err}`")),
|
||||
}
|
||||
open::that(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
@@ -4,6 +4,5 @@ pub mod help;
|
||||
pub mod init;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod sysopt;
|
||||
pub mod tmpl;
|
||||
mod winhelp;
|
||||
|
||||
@@ -1,71 +1,42 @@
|
||||
use crate::{core::Core, utils::init, utils::server};
|
||||
use crate::{core::Core, data::Data, utils::init, utils::server};
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
|
||||
/// handle something when start app
|
||||
pub fn resolve_setup(app: &App) {
|
||||
// setup a simple http server for singleton
|
||||
server::embed_server(&app.handle());
|
||||
|
||||
// init app config
|
||||
init::init_app(app.package_info());
|
||||
|
||||
// init core
|
||||
// should be initialized after init_app fix #122
|
||||
let core = Core::new();
|
||||
let silent_start = {
|
||||
let global = Data::global();
|
||||
let verge = global.verge.lock();
|
||||
let singleton = verge.app_singleton_port.clone();
|
||||
|
||||
core.set_win(app.get_window("main"));
|
||||
// setup a simple http server for singleton
|
||||
server::embed_server(&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());
|
||||
|
||||
// fix #122
|
||||
app.manage(core);
|
||||
|
||||
resolve_window(app);
|
||||
if !silent_start {
|
||||
create_window(&app.app_handle());
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy
|
||||
pub fn resolve_reset(app_handle: &AppHandle) {
|
||||
let core = app_handle.state::<Core>();
|
||||
pub fn resolve_reset() {
|
||||
let core = Core::global();
|
||||
let mut sysopt = core.sysopt.lock();
|
||||
sysopt.reset_sysproxy();
|
||||
crate::log_if_err!(sysopt.reset_sysproxy());
|
||||
drop(sysopt);
|
||||
|
||||
let mut service = core.service.lock();
|
||||
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 +91,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,19 +1,22 @@
|
||||
extern crate warp;
|
||||
|
||||
use super::resolve;
|
||||
use port_scanner::local_port_available;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use warp::Filter;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const SERVER_PORT: u16 = 33333;
|
||||
const SERVER_PORT: u16 = 33331;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
const SERVER_PORT: u16 = 11233;
|
||||
|
||||
/// check whether there is already exists
|
||||
pub fn check_singleton() -> Result<(), ()> {
|
||||
if !local_port_available(SERVER_PORT) {
|
||||
pub fn check_singleton(port: Option<u16>) -> Result<(), ()> {
|
||||
let port = port.unwrap_or(SERVER_PORT);
|
||||
|
||||
if !local_port_available(port) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
|
||||
let url = format!("http://127.0.0.1:{}/commands/visible", port);
|
||||
reqwest::get(url).await.unwrap();
|
||||
Err(())
|
||||
})
|
||||
@@ -24,18 +27,16 @@ pub fn check_singleton() -> Result<(), ()> {
|
||||
|
||||
/// The embed server only be used to implement singleton process
|
||||
/// maybe it can be used as pac server later
|
||||
pub fn embed_server(app: &AppHandle) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
pub fn embed_server(app_handle: &AppHandle, port: Option<u16>) {
|
||||
let app_handle = app_handle.clone();
|
||||
let port = port.unwrap_or(SERVER_PORT);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let commands = warp::path!("commands" / "visible").map(move || {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
resolve::create_window(&app_handle);
|
||||
return format!("ok");
|
||||
});
|
||||
|
||||
warp::serve(commands)
|
||||
.bind(([127, 0, 0, 1], SERVER_PORT))
|
||||
.await;
|
||||
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"192.168.0.0/16\n10.0.0.0/8\n172.16.0.0/12\n127.0.0.1\nlocalhost\n*.local\ntimestamp.apple.com\n";
|
||||
#[cfg(target_os = "macos")]
|
||||
static MACOS_SERVICE: &str = "Wi-Fi";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct SysProxyConfig {
|
||||
pub enable: bool,
|
||||
pub server: String,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
impl Default for SysProxyConfig {
|
||||
fn default() -> Self {
|
||||
SysProxyConfig {
|
||||
enable: false,
|
||||
server: String::from(""),
|
||||
bypass: String::from(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SysProxyConfig {
|
||||
pub fn new(enable: bool, port: String, bypass: Option<String>) -> Self {
|
||||
SysProxyConfig {
|
||||
enable,
|
||||
server: format!("127.0.0.1:{}", port),
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the windows system proxy config
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let cur_var = hkcu.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||
KEY_READ,
|
||||
)?;
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable: cur_var.get_value::<u32, _>("ProxyEnable")? == 1u32,
|
||||
server: cur_var.get_value("ProxyServer")?,
|
||||
bypass: cur_var.get_value("ProxyOverride")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the windows system proxy config
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let cur_var = hkcu.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||
KEY_SET_VALUE,
|
||||
)?;
|
||||
|
||||
let enable: u32 = if self.enable { 1u32 } else { 0u32 };
|
||||
|
||||
cur_var.set_value("ProxyEnable", &enable)?;
|
||||
cur_var.set_value("ProxyServer", &self.server)?;
|
||||
cur_var.set_value("ProxyOverride", &self.bypass)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the macos system proxy config
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use std::process::Command;
|
||||
|
||||
let http = macproxy::get_proxy(&["-getwebproxy", MACOS_SERVICE])?;
|
||||
let https = macproxy::get_proxy(&["-getsecurewebproxy", MACOS_SERVICE])?;
|
||||
let sock = macproxy::get_proxy(&["-getsocksfirewallproxy", MACOS_SERVICE])?;
|
||||
|
||||
let mut enable = false;
|
||||
let mut server = "".into();
|
||||
|
||||
if sock.0 == "Yes" {
|
||||
enable = true;
|
||||
server = sock.1;
|
||||
}
|
||||
if https.0 == "Yes" {
|
||||
enable = true;
|
||||
server = https.1;
|
||||
}
|
||||
if http.0 == "Yes" || !enable {
|
||||
enable = http.0 == "Yes";
|
||||
server = http.1;
|
||||
}
|
||||
|
||||
let bypass_output = Command::new("networksetup")
|
||||
.args(["-getproxybypassdomains", MACOS_SERVICE])
|
||||
.output()?;
|
||||
|
||||
// change the format to xxx,xxx
|
||||
let bypass = std::str::from_utf8(&bypass_output.stdout)
|
||||
.unwrap_or(DEFAULT_BYPASS)
|
||||
.to_string()
|
||||
.split('\n')
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable,
|
||||
server,
|
||||
bypass,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the macos system proxy config
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use std::process::Command;
|
||||
|
||||
let enable = self.enable;
|
||||
let server = self.server.as_str();
|
||||
let bypass = self.bypass.clone();
|
||||
macproxy::set_proxy("-setwebproxy", MACOS_SERVICE, enable, server)?;
|
||||
macproxy::set_proxy("-setsecurewebproxy", MACOS_SERVICE, enable, server)?;
|
||||
macproxy::set_proxy("-setsocksfirewallproxy", MACOS_SERVICE, enable, server)?;
|
||||
|
||||
let domains = bypass.split(",").collect::<Vec<_>>();
|
||||
Command::new("networksetup")
|
||||
.args([["-setproxybypassdomains", MACOS_SERVICE].to_vec(), domains].concat())
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macproxy {
|
||||
use super::*;
|
||||
use anyhow::bail;
|
||||
use std::process::Command;
|
||||
|
||||
/// use networksetup
|
||||
/// get the target proxy config
|
||||
pub(super) fn get_proxy(args: &[&str; 2]) -> Result<(String, String)> {
|
||||
let output = Command::new("networksetup").args(args).output()?;
|
||||
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
let enable = parse(stdout, "Enabled:");
|
||||
let server = parse(stdout, "Server:");
|
||||
let port = parse(stdout, "Port:");
|
||||
let server = format!("{server}:{port}");
|
||||
Ok((enable.into(), server))
|
||||
}
|
||||
|
||||
/// use networksetup
|
||||
/// set the target proxy config
|
||||
pub(super) fn set_proxy(
|
||||
target: &str, // like: -setwebproxy
|
||||
device: &str,
|
||||
enable: bool,
|
||||
server: &str,
|
||||
) -> Result<()> {
|
||||
let mut split = server.split(":");
|
||||
let host = split.next();
|
||||
let port = split.next();
|
||||
|
||||
// can not parse the field
|
||||
if host.is_none() || port.is_none() {
|
||||
bail!("failed to parse the server into host:port");
|
||||
}
|
||||
|
||||
let args = vec![target, device, host.unwrap(), port.unwrap()];
|
||||
Command::new("networksetup").args(&args).status()?;
|
||||
|
||||
let target_state = String::from(target) + "state";
|
||||
let enable = if enable { "on" } else { "off" };
|
||||
let args = vec![target_state.as_str(), device, enable];
|
||||
Command::new("networksetup").args(&args).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// parse the networksetup output
|
||||
fn parse<'a>(target: &'a str, key: &'a str) -> &'a str {
|
||||
match target.find(key) {
|
||||
Some(idx) => {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
let value = match value.find("\n") {
|
||||
Some(end) => &value[..end],
|
||||
None => value,
|
||||
};
|
||||
value.trim()
|
||||
}
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("networksetup")
|
||||
.args(["-getwebproxy", "Wi-Fi"])
|
||||
.output();
|
||||
|
||||
let output = output.unwrap();
|
||||
let stdout = std::str::from_utf8(&output.stdout).unwrap();
|
||||
let enable = parse(stdout, "Enabled:");
|
||||
let server = parse(stdout, "Server:");
|
||||
let port = parse(stdout, "Port:");
|
||||
|
||||
println!("enable: {}, server: {}, port: {}", enable, server, port);
|
||||
|
||||
dbg!(SysProxyConfig::get_sys().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set() {
|
||||
let sysproxy = SysProxyConfig::new(true, "7890".into(), None);
|
||||
dbg!(sysproxy.set_sys().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Linux Desktop System Proxy Supports
|
||||
/// by using `gsettings`
|
||||
#[cfg(target_os = "linux")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the system proxy config [http/https/socks]
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use std::process::Command;
|
||||
|
||||
let schema = "org.gnome.system.proxy";
|
||||
|
||||
// get enable
|
||||
let mode = Command::new("gsettings")
|
||||
.args(["get", schema, "mode"])
|
||||
.output()?;
|
||||
let mode = std::str::from_utf8(&mode.stdout)?;
|
||||
let enable = mode == "manual";
|
||||
|
||||
// get bypass
|
||||
// Todo: parse the ignore-hosts
|
||||
// ['aaa', 'bbb'] -> aaa,bbb
|
||||
let ignore = Command::new("gsettings")
|
||||
.args(["get", schema, "ignore-hosts"])
|
||||
.output()?;
|
||||
let ignore = std::str::from_utf8(&ignore.stdout)?;
|
||||
let bypass = ignore.to_string();
|
||||
|
||||
let http = Self::get_proxy("http")?;
|
||||
let https = Self::get_proxy("https")?;
|
||||
let socks = Self::get_proxy("socks")?;
|
||||
|
||||
let mut server = "".into();
|
||||
|
||||
if socks.len() > 0 {
|
||||
server = socks;
|
||||
}
|
||||
if https.len() > 0 {
|
||||
server = https;
|
||||
}
|
||||
if http.len() > 0 {
|
||||
server = http;
|
||||
}
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable,
|
||||
server,
|
||||
bypass,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the system proxy config [http/https/socks]
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use anyhow::bail;
|
||||
use std::process::Command;
|
||||
|
||||
let enable = self.enable;
|
||||
let server = self.server.as_str();
|
||||
let bypass = self.bypass.clone();
|
||||
let schema = "org.gnome.system.proxy";
|
||||
|
||||
if enable {
|
||||
let mut split = server.split(":");
|
||||
let host = split.next();
|
||||
let port = split.next();
|
||||
|
||||
if port.is_none() {
|
||||
bail!("failed to parse the port");
|
||||
}
|
||||
|
||||
let host = format!("'{}'", host.unwrap_or("127.0.0.1"));
|
||||
let host = host.as_str();
|
||||
let port = port.unwrap();
|
||||
|
||||
let http = format!("{schema}.http");
|
||||
Command::new("gsettings")
|
||||
.args(["set", http.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", http.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
let https = format!("{schema}.https");
|
||||
Command::new("gsettings")
|
||||
.args(["set", https.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", https.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
let socks = format!("{schema}.socks");
|
||||
Command::new("gsettings")
|
||||
.args(["set", socks.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", socks.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
// set bypass
|
||||
// Todo: parse the ignore-hosts
|
||||
// aaa,bbb,cccc -> ['aaa', 'bbb', 'ccc']
|
||||
Command::new("gsettings")
|
||||
.args(["set", schema, "ignore-hosts", bypass.as_str()]) // todo
|
||||
.status()?;
|
||||
}
|
||||
|
||||
let mode = if enable { "'manual'" } else { "'none'" };
|
||||
Command::new("gsettings")
|
||||
.args(["set", schema, "mode", mode])
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// help function
|
||||
fn get_proxy(typ: &str) -> Result<String> {
|
||||
use std::process::Command;
|
||||
|
||||
let schema = format!("org.gnome.system.proxy.{typ}");
|
||||
let schema = schema.as_str();
|
||||
|
||||
let host = Command::new("gsettings")
|
||||
.args(["get", schema, "host"])
|
||||
.output()?;
|
||||
let host = std::str::from_utf8(&host.stdout)?;
|
||||
|
||||
let port = Command::new("gsettings")
|
||||
.args(["get", schema, "port"])
|
||||
.output()?;
|
||||
let port = std::str::from_utf8(&port.stdout)?;
|
||||
|
||||
Ok(format!("{host}:{port}"))
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ enable_self_startup: false
|
||||
enable_system_proxy: false
|
||||
enable_proxy_guard: false
|
||||
proxy_guard_duration: 10
|
||||
system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
|
||||
";
|
||||
|
||||
/// template for new a profile item
|
||||
@@ -60,7 +59,7 @@ append-proxy-groups:
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
|
||||
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
||||
// The argument to this function is the clash config
|
||||
// or the result of the previous handler
|
||||
// so you should return the config after processing
|
||||
|
||||
+17
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.0.6"
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -25,8 +25,13 @@
|
||||
"icons/icon-new.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"externalBin": ["sidecar/clash", "sidecar/clash-meta"],
|
||||
"resources": [
|
||||
"resources"
|
||||
],
|
||||
"externalBin": [
|
||||
"sidecar/clash",
|
||||
"sidecar/clash-meta"
|
||||
],
|
||||
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "A Clash GUI based on tauri.",
|
||||
@@ -46,7 +51,10 @@
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US"]
|
||||
"language": [
|
||||
"zh-CN",
|
||||
"en-US"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68,23 +76,14 @@
|
||||
},
|
||||
"process": {
|
||||
"all": true
|
||||
},
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Clash Verge",
|
||||
"width": 800,
|
||||
"height": 636,
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
$maxLogo: 100px;
|
||||
|
||||
.the-logo {
|
||||
position: relative;
|
||||
flex: 0 1 168px;
|
||||
flex: 0 1 $maxLogo;
|
||||
width: 100%;
|
||||
max-width: 168px;
|
||||
max-height: 168px;
|
||||
max-width: $maxLogo + 32px;
|
||||
max-height: $maxLogo;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
@@ -29,15 +31,15 @@
|
||||
|
||||
img,
|
||||
svg {
|
||||
width: 96%;
|
||||
height: 96%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.the-newbtn {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 12px;
|
||||
right: 10px;
|
||||
bottom: 0px;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +89,13 @@
|
||||
.windows,
|
||||
.unknown {
|
||||
&.layout {
|
||||
$maxLogo: 115px;
|
||||
.layout__left .the-logo {
|
||||
flex: 0 1 $maxLogo;
|
||||
max-width: $maxLogo + 32px;
|
||||
max-height: $maxLogo;
|
||||
}
|
||||
|
||||
.layout__right .the-content {
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { styled } from "@mui/material";
|
||||
|
||||
const Loading = styled("div")`
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 18px;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
box-sizing: border-box;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 2px;
|
||||
border-radius: 100%;
|
||||
animation: loading 0.7s -0.15s infinite linear;
|
||||
}
|
||||
|
||||
& > div:nth-child(2n-1) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingItem = styled("div")(({ theme }) => ({
|
||||
background: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const BaseLoading = () => {
|
||||
return (
|
||||
<Loading>
|
||||
<LoadingItem />
|
||||
<LoadingItem />
|
||||
<LoadingItem />
|
||||
</Loading>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseLoading;
|
||||
@@ -1,19 +1,25 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { styled, ListItem, IconButton, ListItemText } from "@mui/material";
|
||||
import {
|
||||
styled,
|
||||
ListItem,
|
||||
IconButton,
|
||||
ListItemText,
|
||||
Box,
|
||||
alpha,
|
||||
} from "@mui/material";
|
||||
import { CloseRounded } from "@mui/icons-material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
const Tag = styled("span")(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
fontSize: "12px",
|
||||
fontSize: "10px",
|
||||
padding: "0 4px",
|
||||
lineHeight: 1.375,
|
||||
border: "1px solid #ccc",
|
||||
border: "1px solid",
|
||||
borderRadius: 4,
|
||||
marginRight: "0.1em",
|
||||
transform: "scale(0.92)",
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.35),
|
||||
marginRight: "4px",
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
@@ -26,7 +32,7 @@ const ConnectionItem = (props: Props) => {
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(id));
|
||||
const showTraffic = curUpload! > 1024 || curDownload! > 1024;
|
||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -38,15 +44,18 @@ const ConnectionItem = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ userSelect: "text" }}
|
||||
primary={metadata.host || metadata.destinationIP}
|
||||
secondary={
|
||||
<>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||
{metadata.network}
|
||||
</Tag>
|
||||
|
||||
<Tag>{metadata.type}</Tag>
|
||||
|
||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||
|
||||
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
|
||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||
@@ -56,7 +65,7 @@ const ConnectionItem = (props: Props) => {
|
||||
{parseTraffic(curUpload!)} / {parseTraffic(curDownload!)}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
interface Props {
|
||||
connections: ApiType.ConnectionsItem[];
|
||||
}
|
||||
|
||||
const ConnectionTable = (props: Props) => {
|
||||
const { connections } = props;
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "host",
|
||||
headerName: "Host",
|
||||
flex: 200,
|
||||
minWidth: 200,
|
||||
resizable: false,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "download",
|
||||
headerName: "Download",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
headerName: "Upload",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: "DL Speed",
|
||||
align: "right",
|
||||
width: 88,
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
headerName: "UL Speed",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: "Chains",
|
||||
width: 360,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: "Rule",
|
||||
width: 225,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: "Process",
|
||||
width: 120,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
width: 120,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: "Source",
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: "Destination IP",
|
||||
width: 125,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 160,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
];
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
return connections.map((each) => {
|
||||
const { metadata, rulePayload } = each;
|
||||
const chains = [...each.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
|
||||
return {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
chains,
|
||||
rule,
|
||||
process: metadata.process || metadata.processPath,
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
};
|
||||
});
|
||||
}, [connections]);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionTable;
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "@mui/material";
|
||||
import { relaunch } from "@tauri-apps/api/process";
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
import { killSidecar, restartSidecar } from "@/services/cmds";
|
||||
import { atomUpdateState } from "@/services/states";
|
||||
import Notice from "../base/base-notice";
|
||||
|
||||
@@ -43,11 +42,9 @@ const UpdateDialog = (props: Props) => {
|
||||
setUpdateState(true);
|
||||
|
||||
try {
|
||||
await killSidecar();
|
||||
await installUpdate();
|
||||
await relaunch();
|
||||
} catch (err: any) {
|
||||
await restartSidecar();
|
||||
Notice.error(err?.message || err.toString());
|
||||
} finally {
|
||||
setUpdateState(false);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getInformation } from "@/services/api";
|
||||
import { atomLogData } from "@/services/states";
|
||||
import { getClashLogs } from "@/services/cmds";
|
||||
import { atomEnableLog, atomLogData } from "@/services/states";
|
||||
|
||||
const MAX_LOG_NUM = 1000;
|
||||
|
||||
// setup the log websocket
|
||||
export default function useLogSetup() {
|
||||
const [refresh, setRefresh] = useState({});
|
||||
|
||||
const enableLog = useRecoilValue(atomEnableLog);
|
||||
const setLogData = useSetRecoilState(atomLogData);
|
||||
|
||||
useEffect(() => {
|
||||
let ws: WebSocket = null!;
|
||||
if (!enableLog) return;
|
||||
|
||||
getClashLogs().then(setLogData);
|
||||
|
||||
const handler = (event: MessageEvent<any>) => {
|
||||
const data = JSON.parse(event.data) as ApiType.LogItem;
|
||||
@@ -24,10 +29,11 @@ export default function useLogSetup() {
|
||||
});
|
||||
};
|
||||
|
||||
getInformation().then((info) => {
|
||||
const ws = getInformation().then((info) => {
|
||||
const { server = "", secret = "" } = info;
|
||||
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
const ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
ws.addEventListener("message", handler);
|
||||
return ws;
|
||||
});
|
||||
|
||||
const unlisten = listen("verge://refresh-clash-config", () =>
|
||||
@@ -35,8 +41,8 @@ export default function useLogSetup() {
|
||||
);
|
||||
|
||||
return () => {
|
||||
ws?.close();
|
||||
unlisten?.then((fn) => fn());
|
||||
ws.then((ws) => ws?.close());
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, [refresh]);
|
||||
}, [refresh, enableLog]);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const ProfileMore = (props: Props) => {
|
||||
} = props;
|
||||
|
||||
const { uid, type } = itemData;
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
@@ -110,6 +110,7 @@ const ProfileMore = (props: Props) => {
|
||||
<>
|
||||
<ProfileBox
|
||||
aria-selected={selected}
|
||||
onDoubleClick={onEditFile}
|
||||
// onClick={() => onSelect(false)}
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
@@ -172,13 +173,14 @@ const ProfileMore = (props: Props) => {
|
||||
<Typography
|
||||
noWrap
|
||||
title={itemData.desc}
|
||||
sx={{ width: "calc(100% - 75px)" }}
|
||||
sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
|
||||
>
|
||||
{itemData.desc}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
noWrap
|
||||
component="span"
|
||||
title={`Updated Time: ${parseExpire(itemData.updated)}`}
|
||||
style={{ fontSize: 14 }}
|
||||
|
||||
@@ -2,7 +2,7 @@ import useSWR, { useSWRConfig } from "swr";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { updateProxy } from "@/services/api";
|
||||
import { providerHealthCheck, updateProxy } from "@/services/api";
|
||||
import { getProfiles, patchProfile } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
import useSortProxy from "./use-sort-proxy";
|
||||
@@ -74,10 +74,23 @@ const ProxyGlobal = (props: Props) => {
|
||||
};
|
||||
|
||||
const onCheckAll = useLockFn(async () => {
|
||||
const names = sortedProxies.map((p) => p.name);
|
||||
const providers = new Set(
|
||||
sortedProxies.map((p) => p.provider!).filter(Boolean)
|
||||
);
|
||||
|
||||
await delayManager.checkListDelay({ names, groupName, skipNum: 8 }, () =>
|
||||
mutate("getProxies")
|
||||
if (providers.size) {
|
||||
Promise.allSettled(
|
||||
[...providers].map((p) => providerHealthCheck(p))
|
||||
).then(() => mutate("getProxies"));
|
||||
}
|
||||
|
||||
await delayManager.checkListDelay(
|
||||
{
|
||||
names: sortedProxies.filter((p) => !p.provider).map((p) => p.name),
|
||||
groupName,
|
||||
skipNum: 16,
|
||||
},
|
||||
() => mutate("getProxies")
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ExpandLessRounded,
|
||||
ExpandMoreRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { updateProxy } from "@/services/api";
|
||||
import { providerHealthCheck, updateProxy } from "@/services/api";
|
||||
import { getProfiles, patchProfile } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
import useSortProxy from "./use-sort-proxy";
|
||||
@@ -94,11 +94,23 @@ const ProxyGroup = ({ group }: Props) => {
|
||||
};
|
||||
|
||||
const onCheckAll = useLockFn(async () => {
|
||||
const names = sortedProxies.map((p) => p.name);
|
||||
const groupName = group.name;
|
||||
const providers = new Set(
|
||||
sortedProxies.map((p) => p.provider!).filter(Boolean)
|
||||
);
|
||||
|
||||
await delayManager.checkListDelay({ names, groupName, skipNum: 16 }, () =>
|
||||
mutate("getProxies")
|
||||
if (providers.size) {
|
||||
Promise.allSettled(
|
||||
[...providers].map((p) => providerHealthCheck(p))
|
||||
).then(() => mutate("getProxies"));
|
||||
}
|
||||
|
||||
await delayManager.checkListDelay(
|
||||
{
|
||||
names: sortedProxies.filter((p) => !p.provider).map((p) => p.name),
|
||||
groupName: group.name,
|
||||
skipNum: 16,
|
||||
},
|
||||
() => mutate("getProxies")
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Theme,
|
||||
} from "@mui/material";
|
||||
import delayManager from "@/services/delay";
|
||||
import BaseLoading from "../base/base-loading";
|
||||
|
||||
interface Props {
|
||||
groupName: string;
|
||||
@@ -43,15 +44,28 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
||||
|
||||
const ProxyItem = (props: Props) => {
|
||||
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
|
||||
|
||||
// -1/<=0 为 不显示
|
||||
// -2 为 loading
|
||||
const [delay, setDelay] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (proxy) {
|
||||
if (!proxy) return;
|
||||
|
||||
if (!proxy.provider) {
|
||||
setDelay(delayManager.getDelay(proxy.name, groupName));
|
||||
return;
|
||||
}
|
||||
|
||||
const { history = [] } = proxy;
|
||||
if (history.length > 0) {
|
||||
// 0ms以error显示
|
||||
setDelay(history[history.length - 1].delay || 1e6);
|
||||
}
|
||||
}, [proxy]);
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
setDelay(-2);
|
||||
return delayManager
|
||||
.checkDelay(proxy.name, groupName)
|
||||
.then((result) => setDelay(result))
|
||||
@@ -72,14 +86,9 @@ const ProxyItem = (props: Props) => {
|
||||
? alpha(primary.main, 0.15)
|
||||
: alpha(primary.main, 0.35);
|
||||
const color = mode === "light" ? primary.main : primary.light;
|
||||
|
||||
const showDelay = delay > 0;
|
||||
const showIcon = !showDelay && selected;
|
||||
|
||||
return {
|
||||
".the-check": { display: "none" },
|
||||
".the-delay": { display: showDelay ? "block" : "none" },
|
||||
".the-icon": { display: showIcon ? "block" : "none" },
|
||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
||||
"&:hover .the-icon": { display: "none" },
|
||||
@@ -95,6 +104,9 @@ const ProxyItem = (props: Props) => {
|
||||
<>
|
||||
{proxy.name}
|
||||
|
||||
{showType && !!proxy.provider && (
|
||||
<TypeBox component="span">{proxy.provider}</TypeBox>
|
||||
)}
|
||||
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
</>
|
||||
@@ -104,45 +116,64 @@ const ProxyItem = (props: Props) => {
|
||||
<ListItemIcon
|
||||
sx={{ justifyContent: "flex-end", color: "primary.main" }}
|
||||
>
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
":hover": { bgcolor: alpha(theme.palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
</Widget>
|
||||
{delay === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
<Widget
|
||||
className="the-delay"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={
|
||||
delay > 500
|
||||
? "error.main"
|
||||
: delay < 100
|
||||
? "success.main"
|
||||
: "text.secondary"
|
||||
}
|
||||
sx={(theme) => ({
|
||||
":hover": { bgcolor: alpha(theme.palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`}
|
||||
</Widget>
|
||||
{!proxy.provider && delay !== -2 && (
|
||||
// provider的节点不支持检测
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
display: "none", // hover才显示
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
Check
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16 }}
|
||||
/>
|
||||
{delay > 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
onClick={(e) => {
|
||||
if (proxy.provider) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={
|
||||
delay > 500
|
||||
? "error.main"
|
||||
: delay < 100
|
||||
? "success.main"
|
||||
: "text.secondary"
|
||||
}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay !== -2 && delay <= 0 && selected && (
|
||||
// 展示已选择的icon
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16 }}
|
||||
/>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function useSortProxy(
|
||||
const ad = delayManager.getDelay(a.name, groupName);
|
||||
const bd = delayManager.getDelay(b.name, groupName);
|
||||
|
||||
if (ad === -1) return 1;
|
||||
if (bd === -1) return -1;
|
||||
if (ad === -1 || ad === -2) return 1;
|
||||
if (bd === -1 || bd === -2) return -1;
|
||||
|
||||
return ad - bd;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { styled, Box, Typography } from "@mui/material";
|
||||
|
||||
const Item = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
padding: "6px 16px",
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: "6px",
|
||||
}));
|
||||
|
||||
const COLOR = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"info",
|
||||
"warning",
|
||||
"error",
|
||||
"success",
|
||||
"text",
|
||||
];
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
value: ApiType.RuleItem;
|
||||
}
|
||||
|
||||
const parseColor = (text: string) => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
sum += text.charCodeAt(i);
|
||||
}
|
||||
return COLOR[sum % COLOR.length];
|
||||
};
|
||||
|
||||
const RuleItem = (props: Props) => {
|
||||
const { index, value } = props;
|
||||
|
||||
return (
|
||||
<Item>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body2"
|
||||
sx={{ lineHeight: 2, minWidth: 30, mr: 2.25, textAlign: "center" }}
|
||||
>
|
||||
{index}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
<Typography component="h6" variant="subtitle1" color="text.primary">
|
||||
{value.payload || "-"}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mr: 3, minWidth: 120, display: "inline-block" }}
|
||||
>
|
||||
{value.type}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color={parseColor(value.proxy)}
|
||||
>
|
||||
{value.proxy}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleItem;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { atomClashPort } from "@/services/states";
|
||||
import { getClashConfig } from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { ModalHandler } from "@/hooks/use-modal-handler";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
|
||||
interface Props {
|
||||
handler: ModalHandler;
|
||||
}
|
||||
|
||||
const ClashPortViewer = ({ handler }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: config, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [port, setPort] = useState(config?.["mixed-port"] ?? 9090);
|
||||
|
||||
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
||||
|
||||
if (handler) {
|
||||
handler.current = {
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open && config?.["mixed-port"]) {
|
||||
setPort(config["mixed-port"]);
|
||||
}
|
||||
}, [open, config?.["mixed-port"]]);
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
if (port < 1000) {
|
||||
return Notice.error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
return Notice.error("The port should not > 65536");
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
if (port === config?.["mixed-port"]) return;
|
||||
|
||||
await patchClashConfig({ "mixed-port": port });
|
||||
setGlobalClashPort(port);
|
||||
Notice.success("Change Clash port successfully!", 1000);
|
||||
mutateClash();
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<DialogTitle>{t("Clash Port")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 300 }}>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Mixed Port" />
|
||||
<TextField
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
sx={{ width: 135 }}
|
||||
value={port}
|
||||
onChange={(e) =>
|
||||
setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setOpen(false)}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onSave} variant="contained">
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClashPortViewer;
|
||||
@@ -4,7 +4,6 @@ import { useLockFn } from "ahooks";
|
||||
import { Menu, MenuItem } from "@mui/material";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { changeClashCore, getVergeConfig } from "@/services/cmds";
|
||||
import { getVersion } from "@/services/api";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
|
||||
const VALID_CORE = [
|
||||
@@ -28,8 +27,10 @@ const CoreSwitch = () => {
|
||||
try {
|
||||
await changeClashCore(core);
|
||||
mutate("getVergeConfig");
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion", getVersion());
|
||||
setTimeout(() => {
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
}, 100);
|
||||
setAnchorEl(null);
|
||||
Notice.success(`Successfully switch to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from "react";
|
||||
import { alpha, Box, IconButton, styled } from "@mui/material";
|
||||
import { DeleteRounded } from "@mui/icons-material";
|
||||
import parseHotkey from "@/utils/parse-hotkey";
|
||||
|
||||
const KeyWrapper = styled("div")(({ theme }) => ({
|
||||
position: "relative",
|
||||
width: 165,
|
||||
minHeight: 36,
|
||||
|
||||
"> input": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 1,
|
||||
opacity: 0,
|
||||
},
|
||||
"> input:focus + .list": {
|
||||
borderColor: alpha(theme.palette.primary.main, 0.75),
|
||||
},
|
||||
".list": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: 36,
|
||||
boxSizing: "border-box",
|
||||
padding: "3px 4px",
|
||||
border: "1px solid",
|
||||
borderRadius: 4,
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.15),
|
||||
"&:last-child": {
|
||||
marginRight: 0,
|
||||
},
|
||||
},
|
||||
".item": {
|
||||
color: theme.palette.text.primary,
|
||||
border: "1px solid",
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.2),
|
||||
borderRadius: "2px",
|
||||
padding: "1px 1px",
|
||||
margin: "2px 0",
|
||||
marginRight: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
}
|
||||
|
||||
const HotkeyInput = (props: Props) => {
|
||||
const { value, onChange } = props;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<KeyWrapper>
|
||||
<input
|
||||
onKeyDown={(e) => {
|
||||
const evt = e.nativeEvent;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const key = parseHotkey(evt.key);
|
||||
if (key === "UNIDENTIFIED") return;
|
||||
|
||||
const newList = [...new Set([...value, key])];
|
||||
onChange(newList);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="list">
|
||||
{value.map((key) => (
|
||||
<div key={key} className="item">
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</KeyWrapper>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
title="Delete"
|
||||
color="inherit"
|
||||
onClick={() => onChange([])}
|
||||
>
|
||||
<DeleteRounded fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotkeyInput;
|
||||
@@ -0,0 +1,132 @@
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
|
||||
import { ModalHandler } from "@/hooks/use-modal-handler";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
import HotkeyInput from "./hotkey-input";
|
||||
|
||||
const ItemWrapper = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const HOTKEY_FUNC = [
|
||||
"clash_mode_rule",
|
||||
"clash_mode_global",
|
||||
"clash_mode_direct",
|
||||
"clash_mode_script",
|
||||
"toggle_system_proxy",
|
||||
"enable_system_proxy",
|
||||
"disable_system_proxy",
|
||||
"toggle_tun_mode",
|
||||
"enable_tun_mode",
|
||||
"disable_tun_mode",
|
||||
];
|
||||
|
||||
interface Props {
|
||||
handler: ModalHandler;
|
||||
}
|
||||
|
||||
const HotkeyViewer = ({ handler }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (handler) {
|
||||
handler.current = {
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
};
|
||||
}
|
||||
|
||||
const { data: vergeConfig, mutate: mutateVerge } = useSWR(
|
||||
"getVergeConfig",
|
||||
getVergeConfig
|
||||
);
|
||||
|
||||
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const map = {} as typeof hotkeyMap;
|
||||
|
||||
vergeConfig?.hotkeys?.forEach((text) => {
|
||||
const [func, key] = text.split(",").map((e) => e.trim());
|
||||
|
||||
if (!func || !key) return;
|
||||
|
||||
map[func] = key
|
||||
.split("+")
|
||||
.map((e) => e.trim())
|
||||
.map((k) => (k === "PLUS" ? "+" : k));
|
||||
});
|
||||
|
||||
setHotkeyMap(map);
|
||||
}, [vergeConfig?.hotkeys, open]);
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
const hotkeys = Object.entries(hotkeyMap)
|
||||
.map(([func, keys]) => {
|
||||
if (!func || !keys?.length) return "";
|
||||
|
||||
const key = keys
|
||||
.map((k) => k.trim())
|
||||
.filter(Boolean)
|
||||
.map((k) => (k === "+" ? "PLUS" : k))
|
||||
.join("+");
|
||||
|
||||
if (!key) return "";
|
||||
return `${func},${key}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
try {
|
||||
patchVergeConfig({ hotkeys });
|
||||
setOpen(false);
|
||||
mutateVerge();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<DialogTitle>{t("Hotkey Viewer")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 450, maxHeight: 330 }}>
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
<ItemWrapper key={func}>
|
||||
<Typography>{t(func)}</Typography>
|
||||
<HotkeyInput
|
||||
value={hotkeyMap[func] ?? []}
|
||||
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
|
||||
/>
|
||||
</ItemWrapper>
|
||||
))}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setOpen(false)}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onSave} variant="contained">
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotkeyViewer;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { InfoRounded } from "@mui/icons-material";
|
||||
import { ClickAwayListener, Tooltip } from "@mui/material";
|
||||
import { getSystemProxy } from "@/services/cmds";
|
||||
|
||||
const SysproxyTooltip = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [info, setInfo] = useState<any>({});
|
||||
|
||||
const onShow = async () => {
|
||||
const data = await getSystemProxy();
|
||||
setInfo(data ?? {});
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const timer = setTimeout(() => setOpen(false), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [open]);
|
||||
|
||||
// todo: add error info
|
||||
const showTitle = (
|
||||
<div>
|
||||
<div>Enable: {(!!info.enable).toString()}</div>
|
||||
<div>Server: {info.server}</div>
|
||||
<div>Bypass: {info.bypass}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={() => setOpen(false)}>
|
||||
<Tooltip
|
||||
PopperProps={{
|
||||
disablePortal: true,
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
open={open}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
placement="top"
|
||||
title={showTitle}
|
||||
arrow
|
||||
>
|
||||
<InfoRounded
|
||||
fontSize="small"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
onClick={onShow}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
|
||||
export default SysproxyTooltip;
|
||||
@@ -0,0 +1,204 @@
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
styled,
|
||||
Switch,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
getSystemProxy,
|
||||
getVergeConfig,
|
||||
patchVergeConfig,
|
||||
} from "@/services/cmds";
|
||||
import { ModalHandler } from "@/hooks/use-modal-handler";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
|
||||
interface Props {
|
||||
handler: ModalHandler;
|
||||
}
|
||||
|
||||
const FlexBox = styled("div")`
|
||||
display: flex;
|
||||
margin-top: 4px;
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
width: 80px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SysproxyViewer = ({ handler }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (handler) {
|
||||
handler.current = {
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
};
|
||||
}
|
||||
|
||||
const { data: vergeConfig, mutate: mutateVerge } = useSWR(
|
||||
"getVergeConfig",
|
||||
getVergeConfig
|
||||
);
|
||||
|
||||
const {
|
||||
enable_system_proxy: enabled,
|
||||
enable_proxy_guard,
|
||||
system_proxy_bypass,
|
||||
proxy_guard_duration,
|
||||
} = vergeConfig ?? {};
|
||||
|
||||
const { data: sysproxy } = useSWR(
|
||||
open ? "getSystemProxy" : null,
|
||||
getSystemProxy
|
||||
);
|
||||
|
||||
const [value, setValue] = useState({
|
||||
guard: enable_proxy_guard,
|
||||
bypass: system_proxy_bypass,
|
||||
duration: proxy_guard_duration ?? 10,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue({
|
||||
guard: enable_proxy_guard,
|
||||
bypass: system_proxy_bypass,
|
||||
duration: proxy_guard_duration ?? 10,
|
||||
});
|
||||
}, [vergeConfig]);
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
if (value.duration < 5) {
|
||||
Notice.error("Proxy guard duration at least 5 seconds");
|
||||
return;
|
||||
}
|
||||
|
||||
const patch: Partial<CmdType.VergeConfig> = {};
|
||||
|
||||
if (value.guard !== enable_proxy_guard) {
|
||||
patch.enable_proxy_guard = value.guard;
|
||||
}
|
||||
if (value.duration !== proxy_guard_duration) {
|
||||
patch.proxy_guard_duration = value.duration;
|
||||
}
|
||||
if (value.bypass !== system_proxy_bypass) {
|
||||
patch.system_proxy_bypass = value.bypass;
|
||||
}
|
||||
|
||||
try {
|
||||
await patchVergeConfig(patch);
|
||||
mutateVerge();
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<DialogTitle>{t("System Proxy Setting")}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: 450, maxHeight: 300 }}>
|
||||
<List>
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Proxy Guard")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
checked={value.guard}
|
||||
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Guard Duration")} />
|
||||
<TextField
|
||||
disabled={!enabled}
|
||||
size="small"
|
||||
value={value.duration}
|
||||
sx={{ width: 100 }}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">s</InputAdornment>,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setValue((v) => ({
|
||||
...v,
|
||||
duration: +e.target.value.replace(/\D/, ""),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
||||
<ListItemText
|
||||
primary={t("Proxy Bypass")}
|
||||
sx={{ padding: "3px 0" }}
|
||||
/>
|
||||
<TextField
|
||||
disabled={!enabled}
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
multiline
|
||||
rows={3}
|
||||
sx={{ width: 280 }}
|
||||
value={value.bypass}
|
||||
onChange={(e) =>
|
||||
setValue((v) => ({ ...v, bypass: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
<Typography variant="body1" sx={{ fontSize: "18px", mb: 1 }}>
|
||||
{t("Current System Proxy")}
|
||||
</Typography>
|
||||
|
||||
<FlexBox>
|
||||
<Typography className="label">Enable:</Typography>
|
||||
<Typography className="value">
|
||||
{(!!sysproxy?.enable).toString()}
|
||||
</Typography>
|
||||
</FlexBox>
|
||||
|
||||
<FlexBox>
|
||||
<Typography className="label">Server:</Typography>
|
||||
<Typography className="value">{sysproxy?.server || "-"}</Typography>
|
||||
</FlexBox>
|
||||
|
||||
<FlexBox>
|
||||
<Typography className="label">Bypass:</Typography>
|
||||
<Typography className="value">{sysproxy?.bypass || "-"}</Typography>
|
||||
</FlexBox>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setOpen(false)}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onSave} variant="contained">
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SysproxyViewer;
|
||||
@@ -1,5 +1,4 @@
|
||||
import useSWR from "swr";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TextField,
|
||||
@@ -9,17 +8,16 @@ import {
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import { atomClashPort } from "@/services/states";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { SettingList, SettingItem } from "./setting";
|
||||
import { getClashConfig, getVersion, updateConfigs } from "@/services/api";
|
||||
import useModalHandler from "@/hooks/use-modal-handler";
|
||||
import Notice from "../base/base-notice";
|
||||
import GuardState from "./mods/guard-state";
|
||||
import CoreSwitch from "./mods/core-switch";
|
||||
import WebUIViewer from "./mods/web-ui-viewer";
|
||||
import ClashFieldViewer from "./mods/clash-field-viewer";
|
||||
import ClashPortViewer from "./mods/clash-port-viewer";
|
||||
|
||||
interface Props {
|
||||
onError: (err: Error) => void;
|
||||
@@ -41,10 +39,9 @@ const SettingClash = ({ onError }: Props) => {
|
||||
"mixed-port": mixedPort,
|
||||
} = clashConfig ?? {};
|
||||
|
||||
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
||||
|
||||
const webUIHandler = useModalHandler();
|
||||
const fieldHandler = useModalHandler();
|
||||
const portHandler = useModalHandler();
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
|
||||
@@ -55,21 +52,6 @@ const SettingClash = ({ onError }: Props) => {
|
||||
await patchClashConfig(patch);
|
||||
};
|
||||
|
||||
const onUpdatePort = async (port: number) => {
|
||||
if (port < 1000) {
|
||||
throw new Error("The port should not < 1000");
|
||||
}
|
||||
if (port > 65536) {
|
||||
throw new Error("The port should not > 65536");
|
||||
}
|
||||
await patchClashConfig({ "mixed-port": port });
|
||||
setGlobalClashPort(port);
|
||||
Notice.success("Change Clash port successfully!", 1000);
|
||||
|
||||
// update the config
|
||||
mutateClash();
|
||||
};
|
||||
|
||||
// get clash core version
|
||||
const clashVer = versionData?.premium
|
||||
? `${versionData.version} Premium`
|
||||
@@ -79,6 +61,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
<SettingList title={t("Clash Setting")}>
|
||||
<WebUIViewer handler={webUIHandler} onError={onError} />
|
||||
<ClashFieldViewer handler={fieldHandler} />
|
||||
<ClashPortViewer handler={portHandler} />
|
||||
|
||||
<SettingItem label={t("Allow Lan")}>
|
||||
<GuardState
|
||||
@@ -108,16 +91,17 @@ const SettingClash = ({ onError }: Props) => {
|
||||
|
||||
<SettingItem label={t("Log Level")}>
|
||||
<GuardState
|
||||
value={logLevel ?? "info"}
|
||||
// clash premium 2022.08.26 值为warn
|
||||
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ "log-level": e })}
|
||||
onGuard={(e) => onUpdateData({ "log-level": e })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 120, "> div": { py: "7.5px" } }}>
|
||||
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
|
||||
<MenuItem value="debug">Debug</MenuItem>
|
||||
<MenuItem value="info">Info</MenuItem>
|
||||
<MenuItem value="warning">Warning</MenuItem>
|
||||
<MenuItem value="warning">Warn</MenuItem>
|
||||
<MenuItem value="error">Error</MenuItem>
|
||||
<MenuItem value="silent">Silent</MenuItem>
|
||||
</Select>
|
||||
@@ -125,20 +109,16 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Mixed Port")}>
|
||||
<GuardState
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
value={mixedPort ?? 0}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => +e.target.value?.replace(/\D+/, "")}
|
||||
onChange={(e) => onChangeData({ "mixed-port": e })}
|
||||
onGuard={onUpdatePort}
|
||||
waitTime={1000}
|
||||
>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
sx={{ width: 120, input: { py: "7.5px" } }}
|
||||
/>
|
||||
</GuardState>
|
||||
sx={{ width: 100, input: { py: "7.5px" } }}
|
||||
onClick={(e) => {
|
||||
portHandler.current.open();
|
||||
(e.target as any).blur();
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Web UI")}>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton, Switch, TextField } from "@mui/material";
|
||||
import { ArrowForward, PrivacyTipRounded } from "@mui/icons-material";
|
||||
import { IconButton, Switch } from "@mui/material";
|
||||
import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material";
|
||||
import {
|
||||
checkService,
|
||||
getVergeConfig,
|
||||
patchVergeConfig,
|
||||
} from "@/services/cmds";
|
||||
import { SettingList, SettingItem } from "./setting";
|
||||
import useModalHandler from "@/hooks/use-modal-handler";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import GuardState from "./mods/guard-state";
|
||||
import ServiceMode from "./mods/service-mode";
|
||||
import SysproxyTooltip from "./mods/sysproxy-tooltip";
|
||||
import SysproxyViewer from "./mods/sysproxy-viewer";
|
||||
|
||||
interface Props {
|
||||
onError?: (err: Error) => void;
|
||||
@@ -22,8 +23,11 @@ const isWIN = getSystem() === "windows";
|
||||
|
||||
const SettingSystem = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
|
||||
|
||||
const { data: vergeConfig, mutate: mutateVerge } = useSWR(
|
||||
"getVergeConfig",
|
||||
getVergeConfig
|
||||
);
|
||||
|
||||
// service mode
|
||||
const [serviceOpen, setServiceOpen] = useState(false);
|
||||
@@ -39,17 +43,19 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
enable_service_mode,
|
||||
enable_silent_start,
|
||||
enable_system_proxy,
|
||||
system_proxy_bypass,
|
||||
enable_proxy_guard,
|
||||
} = vergeConfig ?? {};
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<CmdType.VergeConfig>) => {
|
||||
mutate("getVergeConfig", { ...vergeConfig, ...patch }, false);
|
||||
mutateVerge({ ...vergeConfig, ...patch }, false);
|
||||
};
|
||||
|
||||
const sysproxyHandler = useModalHandler();
|
||||
|
||||
return (
|
||||
<SettingList title={t("System Setting")}>
|
||||
<SysproxyViewer handler={sysproxyHandler} />
|
||||
|
||||
<SettingItem label={t("Tun Mode")}>
|
||||
<GuardState
|
||||
value={enable_tun_mode ?? false}
|
||||
@@ -91,6 +97,7 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => setServiceOpen(true)}
|
||||
>
|
||||
<ArrowForward />
|
||||
@@ -108,6 +115,31 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingItem
|
||||
label={t("System Proxy")}
|
||||
extra={
|
||||
<Settings
|
||||
fontSize="small"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
onClick={() => sysproxyHandler.current.open()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
value={enable_system_proxy ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_system_proxy: e })}
|
||||
onGuard={async (e) => {
|
||||
await patchVergeConfig({ enable_system_proxy: e });
|
||||
mutateVerge(); // update bypass value
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Auto Launch")}>
|
||||
<GuardState
|
||||
value={enable_auto_launch ?? false}
|
||||
@@ -133,56 +165,6 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("System Proxy")} extra={<SysproxyTooltip />}>
|
||||
<GuardState
|
||||
value={enable_system_proxy ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_system_proxy: e })}
|
||||
onGuard={async (e) => {
|
||||
await patchVergeConfig({ enable_system_proxy: e });
|
||||
mutate("getVergeConfig"); // update bypass value
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
{enable_system_proxy && (
|
||||
<SettingItem label={t("Proxy Guard")}>
|
||||
<GuardState
|
||||
value={enable_proxy_guard ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_proxy_guard: e })}
|
||||
onGuard={(e) => patchVergeConfig({ enable_proxy_guard: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{enable_system_proxy && (
|
||||
<SettingItem label={t("Proxy Bypass")}>
|
||||
<GuardState
|
||||
value={system_proxy_bypass ?? ""}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ system_proxy_bypass: e })}
|
||||
onGuard={(e) => patchVergeConfig({ system_proxy_bypass: e })}
|
||||
waitTime={1000}
|
||||
>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
sx={{ width: 120, input: { py: "7.5px" } }}
|
||||
/>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { SettingList, SettingItem } from "./setting";
|
||||
import { version } from "@root/package.json";
|
||||
import useModalHandler from "@/hooks/use-modal-handler";
|
||||
import ThemeModeSwitch from "./mods/theme-mode-switch";
|
||||
import ConfigViewer from "./mods/config-viewer";
|
||||
import HotkeyViewer from "./mods/hotkey-viewer";
|
||||
import GuardState from "./mods/guard-state";
|
||||
import SettingTheme from "./setting-theme";
|
||||
|
||||
@@ -43,8 +45,12 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
mutateVerge({ ...vergeConfig, ...patch }, false);
|
||||
};
|
||||
|
||||
const hotkeyHandler = useModalHandler();
|
||||
|
||||
return (
|
||||
<SettingList title={t("Verge Setting")}>
|
||||
<HotkeyViewer handler={hotkeyHandler} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
value={language ?? "en"}
|
||||
@@ -108,6 +114,17 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Hotkey Setting")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ my: "2px" }}
|
||||
onClick={() => hotkeyHandler.current.open()}
|
||||
>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Runtime Config")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
||||
+17
-1
@@ -3,6 +3,7 @@
|
||||
"Label-Profiles": "Profiles",
|
||||
"Label-Connections": "Connections",
|
||||
"Label-Logs": "Logs",
|
||||
"Label-Rules": "Rules",
|
||||
"Label-Settings": "Settings",
|
||||
|
||||
"Connections": "Connections",
|
||||
@@ -65,11 +66,15 @@
|
||||
"Auto Launch": "Auto Launch",
|
||||
"Silent Start": "Silent Start",
|
||||
"System Proxy": "System Proxy",
|
||||
"System Proxy Setting": "System Proxy Setting",
|
||||
"Proxy Guard": "Proxy Guard",
|
||||
"Guard Duration": "Guard Duration",
|
||||
"Proxy Bypass": "Proxy Bypass",
|
||||
"Current System Proxy": "Current System Proxy",
|
||||
"Theme Mode": "Theme Mode",
|
||||
"Theme Blur": "Theme Blur",
|
||||
"Theme Setting": "Theme Setting",
|
||||
"Hotkey Setting": "Hotkey Setting",
|
||||
"Traffic Graph": "Traffic Graph",
|
||||
"Language": "Language",
|
||||
"Open App Dir": "Open App Dir",
|
||||
@@ -84,5 +89,16 @@
|
||||
|
||||
"Back": "Back",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel"
|
||||
"Cancel": "Cancel",
|
||||
|
||||
"clash_mode_rule": "Rule Mode",
|
||||
"clash_mode_global": "Global Mode",
|
||||
"clash_mode_direct": "Direct Mode",
|
||||
"clash_mode_script": "Script Mode",
|
||||
"toggle_system_proxy": "Toggle System Proxy",
|
||||
"enable_system_proxy": "Enable System Proxy",
|
||||
"disable_system_proxy": "Disable System Proxy",
|
||||
"toggle_tun_mode": "Toggle Tun Mode",
|
||||
"enable_tun_mode": "Enable Tun Mode",
|
||||
"disable_tun_mode": "Disable Tun Mode"
|
||||
}
|
||||
|
||||
+21
-1
@@ -3,6 +3,7 @@
|
||||
"Label-Profiles": "配 置",
|
||||
"Label-Connections": "连 接",
|
||||
"Label-Logs": "日 志",
|
||||
"Label-Rules": "规 则",
|
||||
"Label-Settings": "设 置",
|
||||
|
||||
"Connections": "连接",
|
||||
@@ -65,11 +66,15 @@
|
||||
"Auto Launch": "开机自启",
|
||||
"Silent Start": "静默启动",
|
||||
"System Proxy": "系统代理",
|
||||
"System Proxy Setting": "系统代理设置",
|
||||
"Proxy Guard": "系统代理守卫",
|
||||
"Guard Duration": "代理守卫间隔",
|
||||
"Proxy Bypass": "Proxy Bypass",
|
||||
"Current System Proxy": "当前系统代理",
|
||||
"Theme Mode": "主题模式",
|
||||
"Theme Blur": "背景模糊",
|
||||
"Theme Setting": "主题设置",
|
||||
"Hotkey Setting": "热键设置",
|
||||
"Traffic Graph": "流量图显",
|
||||
"Language": "语言设置",
|
||||
"Open App Dir": "应用目录",
|
||||
@@ -84,5 +89,20 @@
|
||||
|
||||
"Back": "返回",
|
||||
"Save": "保存",
|
||||
"Cancel": "取消"
|
||||
"Cancel": "取消",
|
||||
|
||||
"Default": "默认",
|
||||
"Download Speed": "下载速度",
|
||||
"Upload Speed": "上传速度",
|
||||
|
||||
"clash_mode_rule": "规则模式",
|
||||
"clash_mode_global": "全局模式",
|
||||
"clash_mode_direct": "直连模式",
|
||||
"clash_mode_script": "脚本模式",
|
||||
"toggle_system_proxy": "切换系统代理",
|
||||
"enable_system_proxy": "开启系统代理",
|
||||
"disable_system_proxy": "关闭系统代理",
|
||||
"toggle_tun_mode": "切换Tun模式",
|
||||
"enable_tun_mode": "开启Tun模式",
|
||||
"disable_tun_mode": "关闭Tun模式"
|
||||
}
|
||||
|
||||
+20
-1
@@ -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";
|
||||
@@ -43,7 +44,10 @@ const Layout = () => {
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") appWindow.close();
|
||||
if (e.key === "Escape") {
|
||||
if (OS === "macos") appWindow.hide();
|
||||
else appWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
listen("verge://refresh-clash-config", async () => {
|
||||
@@ -56,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 ?? ""));
|
||||
}, []);
|
||||
|
||||
@@ -3,6 +3,7 @@ import ProxiesPage from "./proxies";
|
||||
import ProfilesPage from "./profiles";
|
||||
import SettingsPage from "./settings";
|
||||
import ConnectionsPage from "./connections";
|
||||
import RulesPage from "./rules";
|
||||
|
||||
export const routers = [
|
||||
{
|
||||
@@ -20,6 +21,11 @@ export const routers = [
|
||||
link: "/connections",
|
||||
ele: ConnectionsPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Rules",
|
||||
link: "/rules",
|
||||
ele: RulesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Logs",
|
||||
link: "/logs",
|
||||
|
||||
+83
-27
@@ -1,26 +1,56 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Paper, TextField } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableChartRounded, TableRowsRounded } from "@mui/icons-material";
|
||||
import { closeAllConnections, getInformation } from "@/services/api";
|
||||
import { atomConnectionSetting } from "@/services/states";
|
||||
import BasePage from "@/components/base/base-page";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
import BaseEmpty from "@/components/base/base-empty";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
import ConnectionTable from "@/components/connection/connection-table";
|
||||
|
||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||
|
||||
type OrderFunc = (list: ApiType.ConnectionsItem[]) => ApiType.ConnectionsItem[];
|
||||
|
||||
const ConnectionsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
||||
const [connData, setConnData] = useState<ApiType.Connections>(initConn);
|
||||
|
||||
const [setting, setSetting] = useRecoilState(atomConnectionSetting);
|
||||
|
||||
const isTableLayout = setting.layout === "table";
|
||||
|
||||
const orderOpts: Record<string, OrderFunc> = {
|
||||
Default: (list) => list,
|
||||
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
"Download Speed": (list) =>
|
||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||
};
|
||||
|
||||
const filterConn = useMemo(() => {
|
||||
return connData.connections.filter((conn) =>
|
||||
const orderFunc = orderOpts[curOrderOpt];
|
||||
const connections = connData.connections.filter((conn) =>
|
||||
(conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText)
|
||||
);
|
||||
}, [connData, filterText]);
|
||||
|
||||
if (orderFunc) return orderFunc(connections);
|
||||
return connections;
|
||||
}, [connData, filterText, curOrderOpt]);
|
||||
|
||||
useEffect(() => {
|
||||
let ws: WebSocket | null = null;
|
||||
@@ -76,14 +106,29 @@ const ConnectionsPage = () => {
|
||||
title={t("Connections")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
variant="contained"
|
||||
onClick={onCloseAll}
|
||||
>
|
||||
{t("Close All")}
|
||||
</Button>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() =>
|
||||
setSetting((o) =>
|
||||
o.layout === "list"
|
||||
? { ...o, layout: "table" }
|
||||
: { ...o, layout: "list" }
|
||||
)
|
||||
}
|
||||
>
|
||||
{isTableLayout ? (
|
||||
<TableChartRounded fontSize="inherit" />
|
||||
) : (
|
||||
<TableRowsRounded fontSize="inherit" />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<Button size="small" variant="contained" onClick={onCloseAll}>
|
||||
{t("Close All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper sx={{ boxShadow: 2, height: "100%" }}>
|
||||
@@ -97,23 +142,32 @@ const ConnectionsPage = () => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/* <Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
value={logState}
|
||||
onChange={(e) => setLogState(e.target.value)}
|
||||
sx={{ width: 120, mr: 1, '[role="button"]': { py: 0.65 } }}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
</Select> */}
|
||||
{!isTableLayout && (
|
||||
<Select
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
value={curOrderOpt}
|
||||
onChange={(e) => setOrderOpt(e.target.value)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
width: i18n.language === "en" ? 190 : 120,
|
||||
'[role="button"]': { py: 0.65 },
|
||||
}}
|
||||
>
|
||||
{Object.keys(orderOpts).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
variant="outlined"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
@@ -123,13 +177,15 @@ const ConnectionsPage = () => {
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)">
|
||||
{filterConn.length > 0 ? (
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable connections={filterConn} />
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty text="No Connections" />
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
+38
-11
@@ -1,16 +1,29 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { Box, Button, MenuItem, Paper, Select, TextField } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { atomLogData } from "@/services/states";
|
||||
import {
|
||||
PlayCircleOutlineRounded,
|
||||
PauseCircleOutlineRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { atomEnableLog, atomLogData } from "@/services/states";
|
||||
import BasePage from "@/components/base/base-page";
|
||||
import LogItem from "@/components/log/log-item";
|
||||
import BaseEmpty from "@/components/base/base-empty";
|
||||
import LogItem from "@/components/log/log-item";
|
||||
|
||||
const LogPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [logData, setLogData] = useRecoilState(atomLogData);
|
||||
const [enableLog, setEnableLog] = useRecoilState(atomEnableLog);
|
||||
|
||||
const [logState, setLogState] = useState("all");
|
||||
const [filterText, setFilterText] = useState("");
|
||||
@@ -29,14 +42,27 @@ const LogPage = () => {
|
||||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
variant="contained"
|
||||
onClick={() => setLogData([])}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setEnableLog((e) => !e)}
|
||||
>
|
||||
{enableLog ? (
|
||||
<PauseCircleOutlineRounded />
|
||||
) : (
|
||||
<PlayCircleOutlineRounded />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setLogData([])}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper sx={{ boxSizing: "border-box", boxShadow: 2, height: "100%" }}>
|
||||
@@ -67,6 +93,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")}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import useSWR from "swr";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Box, Button, MenuItem, Paper, Select, TextField } from "@mui/material";
|
||||
import { getRules } from "@/services/api";
|
||||
import BasePage from "@/components/base/base-page";
|
||||
import BaseEmpty from "@/components/base/base-empty";
|
||||
import RuleItem from "@/components/rule/rule-item";
|
||||
|
||||
const RulesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data = [] } = useSWR("getRules", getRules);
|
||||
|
||||
const [filterText, setFilterText] = useState("");
|
||||
|
||||
const rules = useMemo(() => {
|
||||
return data.filter((each) => each.payload.includes(filterText));
|
||||
}, [data, filterText]);
|
||||
|
||||
return (
|
||||
<BasePage title={t("Rules")} contentStyle={{ height: "100%" }}>
|
||||
<Paper sx={{ boxSizing: "border-box", boxShadow: 2, height: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "12px",
|
||||
height: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
variant="outlined"
|
||||
spellCheck="false"
|
||||
placeholder={t("Filter conditions")}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height="calc(100% - 50px)">
|
||||
{rules.length > 0 ? (
|
||||
<Virtuoso
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty text="No Rules" />
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesPage;
|
||||
+49
-27
@@ -5,7 +5,7 @@ let axiosIns: AxiosInstance = null!;
|
||||
let server = "";
|
||||
let secret = "";
|
||||
|
||||
/// initialize some infomation
|
||||
/// initialize some information
|
||||
/// enable force update axiosIns
|
||||
export async function getAxios(force: boolean = false) {
|
||||
if (axiosIns && !force) return axiosIns;
|
||||
@@ -62,7 +62,8 @@ export async function updateConfigs(config: Partial<ApiType.ConfigData>) {
|
||||
/// Get current rules
|
||||
export async function getRules() {
|
||||
const instance = await getAxios();
|
||||
return instance.get("/rules") as Promise<ApiType.RuleItem[]>;
|
||||
const response = await instance.get<any, any>("/rules");
|
||||
return response?.rules as ApiType.RuleItem[];
|
||||
}
|
||||
|
||||
/// Get Proxy delay
|
||||
@@ -85,64 +86,85 @@ export async function updateProxy(group: string, proxy: string) {
|
||||
return instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy });
|
||||
}
|
||||
|
||||
/// Get the Proxy infomation
|
||||
export async function getProxies() {
|
||||
// get proxy
|
||||
async function getProxiesInner() {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/proxies");
|
||||
const records = (response?.proxies ?? {}) as Record<
|
||||
string,
|
||||
ApiType.ProxyItem
|
||||
>;
|
||||
return (response?.proxies || {}) as Record<string, ApiType.ProxyItem>;
|
||||
}
|
||||
|
||||
const global = records["GLOBAL"];
|
||||
const direct = records["DIRECT"];
|
||||
const reject = records["REJECT"];
|
||||
const order = global?.all;
|
||||
/// Get the Proxy information
|
||||
export async function getProxies() {
|
||||
const [proxyRecord, providerRecord] = await Promise.all([
|
||||
getProxiesInner(),
|
||||
getProviders(),
|
||||
]);
|
||||
|
||||
let groups: ApiType.ProxyGroupItem[] = [];
|
||||
// provider name map
|
||||
const providerMap = Object.fromEntries(
|
||||
Object.entries(providerRecord).flatMap(([provider, item]) =>
|
||||
item.proxies.map((p) => [p.name, { ...p, provider }])
|
||||
)
|
||||
);
|
||||
|
||||
// compatible with proxy-providers
|
||||
const generateItem = (name: string) => {
|
||||
if (records[name]) return records[name];
|
||||
if (proxyRecord[name]) return proxyRecord[name];
|
||||
if (providerMap[name]) return providerMap[name];
|
||||
return { name, type: "unknown", udp: false, history: [] };
|
||||
};
|
||||
|
||||
if (order) {
|
||||
groups = order
|
||||
.filter((name) => records[name]?.all)
|
||||
.map((name) => records[name])
|
||||
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
|
||||
|
||||
let groups: ApiType.ProxyGroupItem[] = [];
|
||||
|
||||
if (global?.all) {
|
||||
groups = global.all
|
||||
.filter((name) => proxyRecord[name]?.all)
|
||||
.map((name) => proxyRecord[name])
|
||||
.map((each) => ({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
}));
|
||||
} else {
|
||||
groups = Object.values(records)
|
||||
groups = Object.values(proxyRecord)
|
||||
.filter((each) => each.name !== "GLOBAL" && each.all)
|
||||
.map((each) => ({
|
||||
...each,
|
||||
all: each.all!.map((item) => generateItem(item)),
|
||||
}));
|
||||
groups.sort((a, b) => b.name.localeCompare(a.name));
|
||||
}))
|
||||
.sort((a, b) => b.name.localeCompare(a.name));
|
||||
}
|
||||
|
||||
const proxies = [direct, reject].concat(
|
||||
Object.values(records).filter(
|
||||
Object.values(proxyRecord).filter(
|
||||
(p) => !p.all?.length && p.name !== "DIRECT" && p.name !== "REJECT"
|
||||
)
|
||||
);
|
||||
|
||||
return { global, direct, groups, records, proxies };
|
||||
return { global, direct, groups, records: proxyRecord, proxies };
|
||||
}
|
||||
|
||||
// todo: get proxy providers
|
||||
// get proxy providers
|
||||
export async function getProviders() {
|
||||
const instance = await getAxios();
|
||||
const response = await instance.get<any, any>("/providers/proxies");
|
||||
return response.providers as any;
|
||||
|
||||
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";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// todo: proxy providers health check
|
||||
export async function getProviderHealthCheck(name: string) {
|
||||
// proxy providers health check
|
||||
export async function providerHealthCheck(name: string) {
|
||||
const instance = await getAxios();
|
||||
return instance.get(
|
||||
`/providers/proxies/${encodeURIComponent(name)}/healthcheck`
|
||||
|
||||
+28
-1
@@ -1,6 +1,29 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import Notice from "@/components/base/base-notice";
|
||||
|
||||
export async function getClashLogs() {
|
||||
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
|
||||
const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
|
||||
const logs = await invoke<string[]>("get_clash_logs");
|
||||
|
||||
return logs
|
||||
.map((log) => {
|
||||
const result = log.match(regex);
|
||||
if (result) {
|
||||
const [_, time, type, payload] = result;
|
||||
return { time, type, payload };
|
||||
}
|
||||
|
||||
const result2 = log.match(newRegex);
|
||||
if (result2) {
|
||||
const [_, time, type, payload] = result2;
|
||||
return { time, type, payload };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as ApiType.LogItem[];
|
||||
}
|
||||
|
||||
export async function getProfiles() {
|
||||
return invoke<CmdType.ProfilesConfig>("get_profiles");
|
||||
}
|
||||
@@ -98,7 +121,11 @@ export async function patchVergeConfig(payload: CmdType.VergeConfig) {
|
||||
}
|
||||
|
||||
export async function getSystemProxy() {
|
||||
return invoke<any>("get_sys_proxy");
|
||||
return invoke<{
|
||||
enable: boolean;
|
||||
server: string;
|
||||
bypass: string;
|
||||
}>("get_sys_proxy");
|
||||
}
|
||||
|
||||
export async function changeClashCore(clashCore: string) {
|
||||
|
||||
@@ -59,6 +59,9 @@ class DelayManager {
|
||||
let count = 0;
|
||||
let current = 0;
|
||||
|
||||
// 设置正在延迟测试中
|
||||
names.forEach((name) => this.setDelay(name, groupName, -2));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const help = async (): Promise<void> => {
|
||||
if (current >= skipNum) return;
|
||||
|
||||
@@ -15,6 +15,56 @@ export const atomLogData = atom<ApiType.LogItem[]>({
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const atomEnableLog = atom<boolean>({
|
||||
key: "atomEnableLog",
|
||||
effects: [
|
||||
({ setSelf, onSet }) => {
|
||||
const key = "enable-log";
|
||||
|
||||
try {
|
||||
setSelf(localStorage.getItem(key) !== "false");
|
||||
} catch {}
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
try {
|
||||
if (isReset) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
localStorage.setItem(key, newValue.toString());
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
interface IConnectionSetting {
|
||||
layout: "table" | "list";
|
||||
}
|
||||
|
||||
export const atomConnectionSetting = atom<IConnectionSetting>({
|
||||
key: "atomConnectionSetting",
|
||||
effects: [
|
||||
({ setSelf, onSet }) => {
|
||||
const key = "connections-setting";
|
||||
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
const data = value == null ? { layout: "list" } : JSON.parse(value);
|
||||
setSelf(data);
|
||||
} catch {
|
||||
setSelf({ layout: "list" });
|
||||
}
|
||||
|
||||
onSet((newValue) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(newValue));
|
||||
} catch {}
|
||||
});
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// save the state of each profile item loading
|
||||
export const atomLoadingCache = atom<Record<string, boolean>>({
|
||||
key: "atomLoadingCache",
|
||||
|
||||
Vendored
+15
-2
@@ -31,12 +31,21 @@ declare namespace ApiType {
|
||||
}[];
|
||||
all?: string[];
|
||||
now?: string;
|
||||
provider?: string; // 记录是否来自provider
|
||||
}
|
||||
|
||||
type ProxyGroupItem = Omit<ProxyItem, "all"> & {
|
||||
all: ProxyItem[];
|
||||
};
|
||||
|
||||
interface ProviderItem {
|
||||
name: string;
|
||||
type: string;
|
||||
proxies: ProxyItem[];
|
||||
updatedAt: string;
|
||||
vehicleType: string;
|
||||
}
|
||||
|
||||
interface TrafficItem {
|
||||
up: number;
|
||||
down: number;
|
||||
@@ -58,6 +67,8 @@ declare namespace ApiType {
|
||||
sourcePort: string;
|
||||
destinationPort: string;
|
||||
destinationIP?: string;
|
||||
process?: string;
|
||||
processPath?: string;
|
||||
};
|
||||
upload: number;
|
||||
download: number;
|
||||
@@ -65,8 +76,8 @@ declare namespace ApiType {
|
||||
chains: string[];
|
||||
rule: string;
|
||||
rulePayload: string;
|
||||
curUpload?: number; // calculate
|
||||
curDownload?: number; // calculate
|
||||
curUpload?: number; // upload speed, calculate at runtime
|
||||
curDownload?: number; // download speed, calculate at runtime
|
||||
}
|
||||
|
||||
interface Connections {
|
||||
@@ -135,8 +146,10 @@ declare namespace CmdType {
|
||||
enable_silent_start?: boolean;
|
||||
enable_system_proxy?: boolean;
|
||||
enable_proxy_guard?: boolean;
|
||||
proxy_guard_duration?: number;
|
||||
system_proxy_bypass?: string;
|
||||
web_ui_list?: string[];
|
||||
hotkeys?: string[];
|
||||
theme_setting?: {
|
||||
primary_color?: string;
|
||||
secondary_color?: string;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const parseHotkey = (key: string) => {
|
||||
let temp = key.toUpperCase();
|
||||
|
||||
if (temp.startsWith("ARROW")) {
|
||||
temp = temp.slice(5);
|
||||
} else if (temp.startsWith("DIGIT")) {
|
||||
temp = temp.slice(5);
|
||||
} else if (temp.startsWith("KEY")) {
|
||||
temp = temp.slice(3);
|
||||
} else if (temp.endsWith("LEFT")) {
|
||||
temp = temp.slice(0, -4);
|
||||
} else if (temp.endsWith("RIGHT")) {
|
||||
temp = temp.slice(0, -5);
|
||||
}
|
||||
|
||||
switch (temp) {
|
||||
case "CONTROL":
|
||||
return "CTRL";
|
||||
case "META":
|
||||
return "CMD";
|
||||
case " ":
|
||||
return "SPACE";
|
||||
|
||||
default:
|
||||
return temp;
|
||||
}
|
||||
};
|
||||
|
||||
export default parseHotkey;
|
||||
@@ -552,6 +552,17 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/x-data-grid@^5.17.4":
|
||||
version "5.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-5.17.4.tgz#93ccd06a0a15d02b8d59c2d3038e217ffc72350d"
|
||||
integrity sha512-cxZuu65Whh1DNU9M2X5ljDOx+GAEpGeJLPnugMjhgqTOszfJZX/4kI7NftrPy051Hy0um0sv0NVTDSFXG6yixA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.9"
|
||||
"@mui/utils" "^5.10.3"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
reselect "^4.1.6"
|
||||
|
||||
"@octokit/auth-token@^2.4.4":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
|
||||
@@ -732,70 +743,70 @@
|
||||
"@svgr/hast-util-to-babel-ast" "^6.3.1"
|
||||
svg-parser "^2.0.4"
|
||||
|
||||
"@tauri-apps/api@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.0.2.tgz#5228720e35d50fd08df87067dc29e7306c1f7a10"
|
||||
integrity sha512-yuNW0oeJ1/ZA7wNF1KgxhHrSu5viPVzY/UgUczzN5ptLM8dH15Juy5rEGkoHfeXGju90Y/l22hi3BtIrp/za+w==
|
||||
"@tauri-apps/api@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.1.0.tgz#2c5756f6aeecd36f7683183eaba021690711bedf"
|
||||
integrity sha512-n13pIqdPd3KtaMmmAcrU7BTfdMtIlGNnfZD0dNX8L4p8dgmuNyikm6JAA+yCpl9gqq6I8x5cV2Y0muqdgD0cWw==
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.0.5.tgz#6fd17a8d9e0e7982b5c9887639407fdec783a744"
|
||||
integrity sha512-oxpFb9ZeMiC3xPUJ9NsXWCnnwFSVkPbJUvDKpc9IaoDIUpsMTV72W4P0Nh0uQRbyhx4modPpstt7+ONypNVYNg==
|
||||
"@tauri-apps/cli-darwin-arm64@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.1.1.tgz#c6f4553cfb338f24131910a1ebbe9fe74d89b8ae"
|
||||
integrity sha512-qBG11ig525/qf0f5OQxn0ON3hT8YdpTfpa4Y4kVqBJhdW50R5fadPv6tv5Dpl2TS2X7nWh/zg5mEXYoCK3HZ9w==
|
||||
|
||||
"@tauri-apps/cli-darwin-x64@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.0.5.tgz#6aaaadd68739c4c4f86546f3d17b2fb60a5a0c04"
|
||||
integrity sha512-hRNYC6L9edz2dEqK33tssPylF2ti6x6udidBlGWc5GSoeEb/05qKMEA1MESQYKBG+4q+wjJvACA2vvz6AfgJ3Q==
|
||||
"@tauri-apps/cli-darwin-x64@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.1.1.tgz#11beb8b3dfc43725f0acd3736cd9d181f93526a2"
|
||||
integrity sha512-M3dMsp78OdxisbTwAWGvy3jIb3uqThtQcUYVvqOu9LeEOHyldOBFDSht+6PTBpaJLAHFMQK2rmNxiWgigklJaA==
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.0.5.tgz#d2d01c8b85ee3771656386bfda3852af8ed811da"
|
||||
integrity sha512-hc/Jp3TtFpxB8XVkLEwWy7MNcUBlS8rNCafQBUt4KSElXB+/oGo50jPO+wd5GSMSOR59UCzH08v11P0b+sAa/w==
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.1.1.tgz#f4bd3f5839cfcd69132d213efee09ce5da5c6d90"
|
||||
integrity sha512-LYlvdAd73cq+yTi6rw7j/DWIvDpeApwgQkIn+HYsNNeFhyFmABU7tmw+pekK3W3nHAkYAJ69Rl4ZdoxdNGKmHg==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.0.5.tgz#7ca848120c2e90b57188aaa9daee11cf29e2e2f4"
|
||||
integrity sha512-btFlkD2PG+yzJBZzWeJmyCy8ZV+iys2Jl66Fs4g9lSi3KrBDnyfQ26RpGZb2pRfkkcVP8/x1WSfByO+Rj+PTBA==
|
||||
"@tauri-apps/cli-linux-arm64-gnu@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.1.1.tgz#2631b2a68e7901ea7281af022d8826c0d82b9edd"
|
||||
integrity sha512-o/hbMQIKuFI7cTNpeQBHD/OCNJOBIci78faKms/t6AstLXx0QJuRHDk477Rg6VVy/I3BBKbyATALbmcTq+ti0A==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.0.5.tgz#5ba65790849e0737f560f3ccd12f7a2a88c25b23"
|
||||
integrity sha512-p5JFdWab2AWhfgAZW/mgOLu+YiIJXKV0NdATGmdiBgQCMmz1k/FM8iOFApCgGbo3/zkR58cJ7Z7hyWmQ07M6Pw==
|
||||
"@tauri-apps/cli-linux-arm64-musl@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.1.1.tgz#11a4bdc52696583152fc70ea373cf31717cb0f4c"
|
||||
integrity sha512-8Ci4qlDnXIp93XqUrtzFCBDatUzPHpZq7L3bociUbWpvy/bnlzxp1C/C+vwdc4uS1MiAp9v3BFgrU4i0f0Z3QQ==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.0.5.tgz#d06430c608316cb5a29ec805d4c5d078a293c3ae"
|
||||
integrity sha512-fOXR635AXxwSO7MCfBhMLnGpcg1H83XGw9ocuyg4jjvtE8QoYPwC4ksfb5lLhDVMui9iIKY93NAK3EkQiSGGmQ==
|
||||
"@tauri-apps/cli-linux-x64-gnu@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.1.1.tgz#11acbcc4743562cc8166c92ffc3bc12f68e0987f"
|
||||
integrity sha512-ES4Bkx2JAI8+dDNDJswhLS3yqt+yT/4C6UfGOPIHFxcXUh6fe36eUllrTt+HLRS9xTZbYnteJy7ebq2TqMkaxw==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.0.5.tgz#813c90531f4001453e73b3ad2d4c75929412a249"
|
||||
integrity sha512-8be4zJVkuMs427JqONhFx5Ia5zWsQ5tbZXd80C3dHNL+5/3VIOK6nGQ0iijyZSLXiE9JKEH2jp1EHB+1TVJRcw==
|
||||
"@tauri-apps/cli-linux-x64-musl@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.1.1.tgz#7dd81b8380d7e7183fb43a2ccbbc26931718204a"
|
||||
integrity sha512-qrN1WOMAaDl+LE8P8iO0+DYlrWNTc9jIu/CsnVY/LImTn79ZPxEkcVBo0UGeKRI7f10TfvkVmLCBLxTz8QhEyA==
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.0.5.tgz#a3eb6f56c3f4ba35f6311e1fb31bd30e6892b316"
|
||||
integrity sha512-WpnIfzS1e4InGhvd1IDSKC3w6kbI5c6oJgMmtkMTBlhjhiZXhZmQF4XA784A5Y13pzsbXnbNJKOp8DuPVkoTRQ==
|
||||
"@tauri-apps/cli-win32-ia32-msvc@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.1.1.tgz#1d106ae1c3766f95d7f49bac51eba0d2cd2daf33"
|
||||
integrity sha512-vw7VOmrQlywHhFV3pf54udf2FRNj9dg9WP1gL0My55FnB+w+PWS9Ipm871kX5qepmChdnZHKq9fsqE2uTjX//Q==
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.0.5.tgz#f1e0860166e8f86b550e35aaf22ef95d6da5a7c8"
|
||||
integrity sha512-8iEhVD3X4LZfrlxEPOV+mAj4QrJrEqKTICiJnwmgjvhYQOOsNHzg5kca7pcBbqcgorQOBydLpfGJtxWRusVPaw==
|
||||
"@tauri-apps/cli-win32-x64-msvc@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.1.1.tgz#ce931d299fcc8c60aceec3f9e18b0987f9951c25"
|
||||
integrity sha512-OukxlLLi3AoCN4ABnqCDTiiC7xJGWukAjrKCIx7wFISrLjNfsrnH7/UOzuopfGpZChSe2c+AamVmcpBfVsEmJA==
|
||||
|
||||
"@tauri-apps/cli@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.0.5.tgz#a15a61e8467be29277b72707c4189c58c33d94bf"
|
||||
integrity sha512-vbY+MwK+xN65x0R/o16UQPxBtJl8pmzVzC0TZKokZfmeOkomoqOEOinSwznAMeyR1ZMJW+fXVgJCPvGsRQ0LGg==
|
||||
"@tauri-apps/cli@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.1.1.tgz#480d5e1ab758aa1d7efcbf97d801bf9812b15cd4"
|
||||
integrity sha512-80kjMEMPBwLYCp0tTKSquy90PHHGGBvZsneNr3B/mWxNsvjzA1C0vOyGJGFrJuT2OmkvrdvuJZ5mch5hL8O1Xg==
|
||||
optionalDependencies:
|
||||
"@tauri-apps/cli-darwin-arm64" "1.0.5"
|
||||
"@tauri-apps/cli-darwin-x64" "1.0.5"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "1.0.5"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "1.0.5"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "1.0.5"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "1.0.5"
|
||||
"@tauri-apps/cli-linux-x64-musl" "1.0.5"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.0.5"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.0.5"
|
||||
"@tauri-apps/cli-darwin-arm64" "1.1.1"
|
||||
"@tauri-apps/cli-darwin-x64" "1.1.1"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "1.1.1"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "1.1.1"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "1.1.1"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "1.1.1"
|
||||
"@tauri-apps/cli-linux-x64-musl" "1.1.1"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.1.1"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.1.1"
|
||||
|
||||
"@types/fs-extra@^9.0.13":
|
||||
version "9.0.13"
|
||||
@@ -928,7 +939,7 @@ ahooks@^3.7.0:
|
||||
dependencies:
|
||||
"@types/js-cookie" "^2.x.x"
|
||||
ahooks-v3-count "^1.0.0"
|
||||
dayjs "^1.11.5"
|
||||
dayjs "^1.9.1"
|
||||
intersection-observer "^0.12.0"
|
||||
js-cookie "^2.x.x"
|
||||
lodash "^4.17.21"
|
||||
@@ -1172,7 +1183,7 @@ data-uri-to-buffer@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
|
||||
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
||||
|
||||
dayjs@^1.11.5:
|
||||
dayjs@^1.11.5, dayjs@^1.9.1:
|
||||
version "1.11.5"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
|
||||
integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
|
||||
@@ -2016,6 +2027,11 @@ regenerator-runtime@^0.13.4:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||
|
||||
reselect@^4.1.6:
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656"
|
||||
integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
|
||||
Reference in New Issue
Block a user