Compare commits

...
65 Commits
Author SHA1 Message Date
GyDi 30ef3057ac v1.1.1 2022-10-11 23:03:12 +08:00
GyDi 48f3a934c9 chore: update log 2022-10-11 23:02:49 +08:00
GyDi 38d1fde84f fix: api error handle 2022-10-11 22:51:18 +08:00
GyDi dd78670a4b fix: clash meta not load geoip, close #212 2022-10-11 22:24:54 +08:00
GyDi b8a8190a43 fix: sort proxy during loading, close #221 2022-10-11 22:06:55 +08:00
GyDi 2d0989342f fix: not create windows when enable slient start 2022-10-11 21:50:00 +08:00
GyDi 15ff9b06a1 fix: root background color 2022-10-11 20:49:03 +08:00
GyDi 41b19f69de fix: create window correctly 2022-10-11 00:57:34 +08:00
GyDi f2bd6f1fce fix: set_activation_policy 2022-10-10 22:15:21 +08:00
GyDi ec94218a4b chore: rm aarch64 ci 2022-09-28 18:50:33 +08:00
GyDi c2e5c7cf38 chore: test ci 2022-09-28 17:26:32 +08:00
GyDi bac5734527 chore: test ci 2022-09-28 17:12:27 +08:00
GyDi bef4033d94 chore: fix ci 2022-09-28 16:04:05 +08:00
GyDi 64216cba67 chore: fix test ci 2022-09-28 15:19:50 +08:00
Particle_G a9d6167a9f chore: add support for windows arm64, close #216 (#209) 2022-09-28 15:13:24 +08:00
GyDi 6c6b40548f fix: disable spell check 2022-09-28 14:15:22 +08:00
苏业钦 a916d88e85 chore: add support linux arm64 (#215) 2022-09-28 10:40:41 +08:00
GyDi ab2f0548a3 chore: clash meta linux use compatible version 2022-09-27 00:16:22 +08:00
GyDi e30ba07285 feat: optimize config feedback 2022-09-26 20:46:29 +08:00
GyDi 1b336d973d fix: adjust init launch on dev 2022-09-26 01:35:19 +08:00
GyDi 7b1866737f v1.1.0 2022-09-26 01:27:56 +08:00
GyDi eedc4ab648 chore: update log 2022-09-26 01:27:32 +08:00
GyDi 5e429c7a94 fix: ignore disable auto launch error 2022-09-26 01:09:05 +08:00
GyDi 57d23eb043 fix(macos): set auto launch path to application 2022-09-25 23:36:55 +08:00
GyDi 5ebd9be89a fix: i18n 2022-09-25 01:41:46 +08:00
GyDi 5a743779e2 feat: show connections with table layout 2022-09-25 01:35:21 +08:00
GyDi 0495062110 fix: style 2022-09-24 20:55:40 +08:00
GyDi d522191f69 fix: save enable log on localstorage 2022-09-24 19:03:14 +08:00
Priestch bbbdc8b7a6 fix: typo in api.ts (#207) 2022-09-24 18:48:11 +08:00
GyDi c00ed8aa5a chore: use ubuntu latest 2022-09-24 16:08:39 +08:00
GyDi 0beaa94068 feat: show loading on proxy group delay check 2022-09-24 15:31:09 +08:00
GyDi 96e76665d6 fix: refresh clash ui await patch 2022-09-24 14:01:28 +08:00
Shun Li 6423a29600 feat: add chains[0] and process to connections display (#205)
* add chains[0] display

* add metadata.process to connections
2022-09-24 13:06:32 +08:00
GyDi 8bddf30dcf refactor(hotkey): use tauri global shortcut 2022-09-23 15:31:01 +08:00
GyDi a6b2db182d feat: adjust connection page ui 2022-09-23 00:08:52 +08:00
GyDi b9f3f9d859 Merge pull request #197 from FoundTheWOUT/main
Support ordering connection
2022-09-22 22:55:46 +08:00
GyDi 6331447dcd feat: yaml merge key 2022-09-21 22:15:24 +08:00
GyDi 4213ee660f feat: toggle log ws 2022-09-20 22:15:28 +08:00
GyDi 3bd9287c5d chore: update deps 2022-09-20 21:26:38 +08:00
GyDi b23d3f7c8b feat: add rule page 2022-09-18 23:19:02 +08:00
GyDi f8d9e5e027 feat: hotkey viewer 2022-09-18 15:52:53 +08:00
GyDi 8fa7fb3b1f feat: refresh ui when hotkey clicked 2022-09-18 15:50:03 +08:00
FoundTheWOUT 63d92a0872 Support ordering connection 2022-09-15 15:52:12 +08:00
GyDi 509d83365e feat: support hotkey (wip) 2022-09-14 01:19:02 +08:00
GyDi d6ab73c905 fix: remove dead code 2022-09-12 13:42:21 +08:00
GyDi 5a93ba05d5 fix: style 2022-09-12 13:42:13 +08:00
GyDi 2d2fdf0b1e fix: handle is none 2022-09-12 00:45:19 +08:00
GyDi cf96622261 fix: unused 2022-09-12 00:02:25 +08:00
GyDi 47c8ccb0e5 refactor: optimize 2022-09-11 20:58:55 +08:00
GyDi 02fdb8778b fix: style 2022-09-11 18:38:51 +08:00
GyDi 6bed7f0e66 fix: windows logo size 2022-09-11 17:50:58 +08:00
GyDi 7597d335b9 feat: hide window on macos 2022-09-09 16:42:35 +08:00
GyDi f32c5ba244 fix: do not kill sidecar during updating 2022-09-09 16:33:04 +08:00
GyDi ab53ab21e2 fix: delay update config 2022-09-09 16:30:27 +08:00
GyDi acfe5dbb49 fix: reduce logo size 2022-09-09 16:19:13 +08:00
GyDi f9b91fa189 feat: system proxy setting 2022-09-07 01:51:43 +08:00
GyDi 2462e68ba1 fix: window center 2022-09-06 22:18:06 +08:00
GyDi 019b2a1681 chore: update resource 2022-09-06 15:32:22 +08:00
GyDi e94a07b677 fix: log level warn value 2022-09-06 14:42:58 +08:00
GyDi c058c29755 feat: change default singleton port and support to change the port 2022-09-06 00:45:01 +08:00
GyDi 9e7c7ac163 feat: log info 2022-09-05 20:30:39 +08:00
GyDi fcee41f00d feat: kill clash by pid 2022-09-05 16:30:29 +08:00
GyDi ff2c1bf8ed feat: change clash port in dialog 2022-09-05 02:12:25 +08:00
GyDi a2cf26e7ed feat: add proxy item check loading 2022-09-04 23:53:48 +08:00
GyDi 71e6900375 feat: compatible with proxy providers health check 2022-09-04 22:55:54 +08:00
71 changed files with 3521 additions and 1938 deletions
+1 -1
View File
@@ -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') &&
+32
View File
@@ -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
View File
@@ -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
View File
@@ -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");
+748 -488
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -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
View File
@@ -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(())
}
}
+2 -2
View File
@@ -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;
+107
View File
@@ -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(())
}
}
+153
View File
@@ -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(&current);
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
View File
@@ -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
-37
View File
@@ -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
View File
@@ -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
View File
@@ -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"),
}
}
+9 -18
View File
@@ -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
+32
View File
@@ -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()
}
+93
View 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
View File
@@ -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(())
}
+24
View File
@@ -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<()> {
+4 -2
View File
@@ -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";
+4 -6
View File
@@ -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]
-1
View File
@@ -4,6 +4,5 @@ pub mod help;
pub mod init;
pub mod resolve;
pub mod server;
pub mod sysopt;
pub mod tmpl;
mod winhelp;
+20 -49
View File
@@ -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
+13 -12
View File
@@ -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;
});
}
-366
View File
@@ -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}"))
}
}
+1 -2
View File
@@ -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
View File
@@ -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';"
}
}
}
}
+6
View File
@@ -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 -7
View File
@@ -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;
}
+50
View File
@@ -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;
+18 -9
View File
@@ -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;
-3
View File
@@ -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);
+14 -8
View File
@@ -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]);
}
+1 -1
View File
@@ -33,7 +33,7 @@ const EnhancedMode = (props: Props) => {
try {
await enhanceProfiles();
mutateLogs();
Notice.success("Refresh clash config", 1000);
// Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
+4 -2
View File
@@ -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 }}
+17 -4
View File
@@ -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")
);
});
+17 -5
View File
@@ -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")
);
});
+74 -43
View File
@@ -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>
+2 -2
View File
@@ -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;
});
+72
View File
@@ -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 -3
View File
@@ -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;
+16 -36
View File
@@ -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")}>
+41 -59
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 ?? ""));
}, []);
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+2 -1
View File
@@ -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")}
+66
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+3
View File
@@ -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;
+50
View File
@@ -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",
+15 -2
View File
@@ -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;
+29
View File
@@ -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;
+71 -55
View File
@@ -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"