Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b52a081e7b | ||
|
|
f981a44861 | ||
|
|
d7c5ce0750 | ||
|
|
9ccc66ca1e | ||
|
|
8606af3616 | ||
|
|
f6e821ba6b | ||
|
|
e8dbcf819b | ||
|
|
bbe2ef4e8e | ||
|
|
dd15455031 | ||
|
|
12ac7bb338 |
2
.github/workflows/ci.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: "Clash Verge v__VERSION__"
|
releaseName: "Clash Verge v__VERSION__"
|
||||||
releaseBody: "Clash Verge now supports Windows and macos(intel)."
|
releaseBody: "Clash Verge now supports Windows and macos."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
||||||
|
|||||||
10
README.md
@@ -11,19 +11,17 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Now it's no different from the others, even fewer. (WIP)
|
Now it's no different from the others, maybe fewer. (WIP)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You should install Rust and Nodejs. Then install tauri cli and packages.
|
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri
|
|
||||||
|
|
||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/en/docs/api/config#tauri.bundle.externalBin).
|
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn run check
|
yarn run check
|
||||||
@@ -56,7 +54,7 @@ This is a learning project for Rust practice.
|
|||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
PR welcome!
|
Issue and PR welcome!
|
||||||
|
|
||||||
## Acknowledgement
|
## Acknowledgement
|
||||||
|
|
||||||
|
|||||||
BIN
docs/demo1.png
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
docs/demo2.png
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 30 KiB |
BIN
docs/demo3.png
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
BIN
docs/demo4.png
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 24 KiB |
BIN
docs/demo5.png
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 27 KiB |
BIN
docs/demo6.png
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.17",
|
"version": "0.0.18",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
utils::{dirs::app_home_dir, fetch::fetch_profile, sysopt::SysProxyConfig},
|
utils::{dirs::app_home_dir, fetch::fetch_profile, sysopt::SysProxyConfig},
|
||||||
};
|
};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::process::Command;
|
use std::{path::PathBuf, process::Command};
|
||||||
use tauri::{api, State};
|
use tauri::{api, State};
|
||||||
|
|
||||||
/// get all profiles from `profiles.yaml`
|
/// get all profiles from `profiles.yaml`
|
||||||
@@ -175,10 +175,7 @@ pub fn view_profile(index: usize, profiles_state: State<'_, ProfilesState>) -> R
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
match open_command().arg(path).spawn() {
|
open_path_cmd(path, "failed to open file by `open`")
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err("failed to open file by `open`".into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// restart the sidecar
|
/// restart the sidecar
|
||||||
@@ -262,10 +259,26 @@ pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfi
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_verge_config(
|
pub fn patch_verge_config(
|
||||||
payload: VergeConfig,
|
payload: VergeConfig,
|
||||||
|
clash_state: State<'_, ClashState>,
|
||||||
verge_state: State<'_, VergeState>,
|
verge_state: State<'_, VergeState>,
|
||||||
|
profiles_state: State<'_, ProfilesState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let tun_mode = payload.enable_tun_mode.clone();
|
||||||
|
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
let mut verge = verge_state.0.lock().unwrap();
|
||||||
verge.patch_config(payload)
|
verge.patch_config(payload)?;
|
||||||
|
|
||||||
|
// change tun mode
|
||||||
|
if tun_mode.is_some() {
|
||||||
|
let mut clash = clash_state.0.lock().unwrap();
|
||||||
|
let profiles = profiles_state.0.lock().unwrap();
|
||||||
|
|
||||||
|
clash.tun_mode(tun_mode.unwrap())?;
|
||||||
|
clash.update_config();
|
||||||
|
profiles.activate(&clash)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// kill all sidecars when update app
|
/// kill all sidecars when update app
|
||||||
@@ -278,30 +291,35 @@ pub fn kill_sidecars() {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_app_dir() -> Result<(), String> {
|
pub fn open_app_dir() -> Result<(), String> {
|
||||||
let app_dir = app_home_dir();
|
let app_dir = app_home_dir();
|
||||||
|
open_path_cmd(app_dir, "failed to open app dir")
|
||||||
match open_command().arg(app_dir).spawn() {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err("failed to open logs dir".into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// open logs dir
|
/// open logs dir
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_logs_dir() -> Result<(), String> {
|
pub fn open_logs_dir() -> Result<(), String> {
|
||||||
let log_dir = app_home_dir().join("logs");
|
let log_dir = app_home_dir().join("logs");
|
||||||
|
open_path_cmd(log_dir, "failed to open logs dir")
|
||||||
match open_command().arg(log_dir).spawn() {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err("failed to open logs dir".into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get open/explorer command
|
/// get open/explorer command
|
||||||
fn open_command() -> Command {
|
fn open_path_cmd(dir: PathBuf, err_str: &str) -> Result<(), String> {
|
||||||
let open = if cfg!(target_os = "windows") {
|
#[cfg(target_os = "windows")]
|
||||||
"explorer"
|
{
|
||||||
} else {
|
use std::os::windows::process::CommandExt;
|
||||||
"open"
|
|
||||||
};
|
match Command::new("explorer")
|
||||||
Command::new(open)
|
.creation_flags(0x08000000)
|
||||||
|
.arg(dir)
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(err_str.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
match Command::new("open").arg(dir).spawn() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(err_str.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,48 @@ impl Clash {
|
|||||||
}
|
}
|
||||||
self.save_config()
|
self.save_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// enable tun mode
|
||||||
|
/// only revise the config and restart the
|
||||||
|
pub fn tun_mode(&mut self, enable: bool) -> Result<(), String> {
|
||||||
|
let tun_key = Value::String("tun".into());
|
||||||
|
let tun_val = self.config.get(&tun_key);
|
||||||
|
|
||||||
|
let mut new_val = Mapping::new();
|
||||||
|
|
||||||
|
if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() {
|
||||||
|
new_val = tun_val.as_ref().unwrap().as_mapping().unwrap().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! revise {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
if $map.contains_key(&ret_key) {
|
||||||
|
$map[&ret_key] = $val;
|
||||||
|
} else {
|
||||||
|
$map.insert(ret_key, $val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! append {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
if !$map.contains_key(&ret_key) {
|
||||||
|
$map.insert(ret_key, $val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
revise!(new_val, "enable", Value::from(enable));
|
||||||
|
append!(new_val, "stack", Value::from("gvisor"));
|
||||||
|
append!(new_val, "auto-route", Value::from(true));
|
||||||
|
append!(new_val, "auto-detect-interface", Value::from(true));
|
||||||
|
|
||||||
|
revise!(self.config, "tun", Value::from(new_val));
|
||||||
|
|
||||||
|
self.save_config()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Clash {
|
impl Default for Clash {
|
||||||
|
|||||||
@@ -329,7 +329,9 @@ pub async fn activate_profile(
|
|||||||
|
|
||||||
// begin to generate the new profile config
|
// begin to generate the new profile config
|
||||||
let def_config = config::read_yaml::<Mapping>(file_path.clone());
|
let def_config = config::read_yaml::<Mapping>(file_path.clone());
|
||||||
let mut new_config = Mapping::new();
|
|
||||||
|
// use the clash config except 5 keys below
|
||||||
|
let mut new_config = clash_config.clone();
|
||||||
|
|
||||||
// Only the following fields are allowed:
|
// Only the following fields are allowed:
|
||||||
// proxies/proxy-providers/proxy-groups/rule-providers/rules
|
// proxies/proxy-providers/proxy-groups/rule-providers/rules
|
||||||
@@ -348,24 +350,6 @@ pub async fn activate_profile(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// add some of the clash `config.yaml` config to it
|
|
||||||
let valid_keys = vec![
|
|
||||||
"mixed-port",
|
|
||||||
"log-level",
|
|
||||||
"allow-lan",
|
|
||||||
"external-controller",
|
|
||||||
"secret",
|
|
||||||
"mode",
|
|
||||||
"ipv6",
|
|
||||||
];
|
|
||||||
valid_keys.iter().for_each(|key| {
|
|
||||||
let key = Value::String(key.to_string());
|
|
||||||
if clash_config.contains_key(&key) {
|
|
||||||
let value = clash_config[&key].clone();
|
|
||||||
new_config.insert(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
config::save_yaml(
|
config::save_yaml(
|
||||||
temp_path.clone(),
|
temp_path.clone(),
|
||||||
&new_config,
|
&new_config,
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ pub struct VergeConfig {
|
|||||||
/// enable traffic graph default is true
|
/// enable traffic graph default is true
|
||||||
pub traffic_graph: Option<bool>,
|
pub traffic_graph: Option<bool>,
|
||||||
|
|
||||||
|
/// clash tun mode
|
||||||
|
pub enable_tun_mode: Option<bool>,
|
||||||
|
|
||||||
/// can the app auto startup
|
/// can the app auto startup
|
||||||
pub enable_auto_launch: Option<bool>,
|
pub enable_auto_launch: Option<bool>,
|
||||||
|
|
||||||
@@ -258,6 +261,11 @@ impl Verge {
|
|||||||
Verge::guard_proxy(self.guard_state.clone());
|
Verge::guard_proxy(self.guard_state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle the tun mode
|
||||||
|
if patch.enable_tun_mode.is_some() {
|
||||||
|
self.config.enable_tun_mode = patch.enable_tun_mode;
|
||||||
|
}
|
||||||
|
|
||||||
self.config.save_file()
|
self.config.save_file()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,27 @@ pub fn resolve_setup(app: &App) {
|
|||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
let mut profiles = profiles_state.0.lock().unwrap();
|
||||||
|
|
||||||
if let Err(err) = clash.run_sidecar() {
|
if let Err(err) = clash.run_sidecar() {
|
||||||
log::error!("{}", err);
|
log::error!("{err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
*profiles = Profiles::read_file();
|
*profiles = Profiles::read_file();
|
||||||
if let Err(err) = profiles.activate(&clash) {
|
if let Err(err) = profiles.activate(&clash) {
|
||||||
log::error!("{}", err);
|
log::error!("{err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
verge.init_sysproxy(clash.info.port.clone());
|
verge.init_sysproxy(clash.info.port.clone());
|
||||||
|
// enable tun mode
|
||||||
|
if verge.config.enable_tun_mode.clone().unwrap_or(false)
|
||||||
|
&& verge.cur_sysproxy.is_some()
|
||||||
|
&& verge.cur_sysproxy.as_ref().unwrap().enable
|
||||||
|
{
|
||||||
|
log::info!("enable tun mode");
|
||||||
|
clash.tun_mode(true).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
verge.init_launch();
|
verge.init_launch();
|
||||||
if let Err(err) = verge.sync_launch() {
|
if let Err(err) = verge.sync_launch() {
|
||||||
log::error!("{}", err);
|
log::error!("{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "clash-verge",
|
"productName": "clash-verge",
|
||||||
"version": "0.0.17"
|
"version": "0.0.18"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
import { ApiType } from "../../services/types";
|
import { ApiType } from "../../services/types";
|
||||||
import { getInfomation } from "../../services/api";
|
import { getInfomation } from "../../services/api";
|
||||||
import { getVergeConfig } from "../../services/cmds";
|
import { getVergeConfig } from "../../services/cmds";
|
||||||
import { atomClashPort } from "../../states/setting";
|
import { atomClashPort } from "../../services/states";
|
||||||
import parseTraffic from "../../utils/parse-traffic";
|
import useLogSetup from "./use-log-setup";
|
||||||
import useTrafficGraph from "./use-traffic-graph";
|
import useTrafficGraph from "./use-traffic-graph";
|
||||||
|
import parseTraffic from "../../utils/parse-traffic";
|
||||||
|
|
||||||
const LayoutTraffic = () => {
|
const LayoutTraffic = () => {
|
||||||
const portValue = useRecoilValue(atomClashPort);
|
const portValue = useRecoilValue(atomClashPort);
|
||||||
@@ -21,6 +22,9 @@ const LayoutTraffic = () => {
|
|||||||
const { data } = useSWR("getVergeConfig", getVergeConfig);
|
const { data } = useSWR("getVergeConfig", getVergeConfig);
|
||||||
const trafficGraph = data?.traffic_graph ?? true;
|
const trafficGraph = data?.traffic_graph ?? true;
|
||||||
|
|
||||||
|
// setup log ws during layout
|
||||||
|
useLogSetup();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: () => void = null!;
|
let unlisten: () => void = null!;
|
||||||
|
|
||||||
|
|||||||
49
src/components/layout/use-log-setup.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { ApiType } from "../../services/types";
|
||||||
|
import { getInfomation } from "../../services/api";
|
||||||
|
import { atomLogData } from "../../services/states";
|
||||||
|
|
||||||
|
const MAX_LOG_NUM = 1000;
|
||||||
|
|
||||||
|
// setup the log websocket
|
||||||
|
export default function useLogSetup() {
|
||||||
|
const setLogData = useSetRecoilState(atomLogData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let ws: WebSocket = null!;
|
||||||
|
let unlisten: () => void = null!;
|
||||||
|
|
||||||
|
const handler = (event: MessageEvent<any>) => {
|
||||||
|
const data = JSON.parse(event.data) as ApiType.LogItem;
|
||||||
|
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||||
|
setLogData((l) => {
|
||||||
|
if (l.length >= MAX_LOG_NUM) l.shift();
|
||||||
|
return [...l, { ...data, time }];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { server = "", secret = "" } = await getInfomation();
|
||||||
|
|
||||||
|
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||||
|
ws.addEventListener("message", handler);
|
||||||
|
|
||||||
|
// reconnect the websocket
|
||||||
|
unlisten = await listen("restart_clash", async () => {
|
||||||
|
const { server = "", secret = "" } = await getInfomation();
|
||||||
|
|
||||||
|
ws?.close();
|
||||||
|
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||||
|
ws.addEventListener("message", handler);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ws?.close();
|
||||||
|
unlisten?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ interface Props<Value> {
|
|||||||
value?: Value;
|
value?: Value;
|
||||||
valueProps?: string;
|
valueProps?: string;
|
||||||
onChangeProps?: string;
|
onChangeProps?: string;
|
||||||
|
waitTime?: number;
|
||||||
onChange?: (value: Value) => void;
|
onChange?: (value: Value) => void;
|
||||||
onFormat?: (...args: any[]) => Value;
|
onFormat?: (...args: any[]) => Value;
|
||||||
onGuard?: (value: Value, oldValue: Value) => Promise<void>;
|
onGuard?: (value: Value, oldValue: Value) => Promise<void>;
|
||||||
@@ -18,6 +19,7 @@ function GuardState<T>(props: Props<T>) {
|
|||||||
children,
|
children,
|
||||||
valueProps = "value",
|
valueProps = "value",
|
||||||
onChangeProps = "onChange",
|
onChangeProps = "onChange",
|
||||||
|
waitTime = 0, // debounce wait time default 0
|
||||||
onGuard = noop,
|
onGuard = noop,
|
||||||
onCatch = noop,
|
onCatch = noop,
|
||||||
onChange = noop,
|
onChange = noop,
|
||||||
@@ -25,34 +27,61 @@ function GuardState<T>(props: Props<T>) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const lockRef = useRef(false);
|
const lockRef = useRef(false);
|
||||||
|
const saveRef = useRef(value);
|
||||||
|
const lastRef = useRef(0);
|
||||||
|
const timeRef = useRef<any>();
|
||||||
|
|
||||||
if (isValidElement(children)) {
|
if (!isValidElement(children)) {
|
||||||
const childProps = { ...children.props };
|
return children as any;
|
||||||
|
|
||||||
childProps[valueProps] = value;
|
|
||||||
childProps[onChangeProps] = async (...args: any[]) => {
|
|
||||||
// 多次操作无效
|
|
||||||
if (lockRef.current) return;
|
|
||||||
|
|
||||||
lockRef.current = true;
|
|
||||||
const oldValue = value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newValue = (onFormat as any)(...args);
|
|
||||||
// 先在ui上响应操作
|
|
||||||
onChange(newValue);
|
|
||||||
await onGuard(newValue, oldValue!);
|
|
||||||
} catch (err: any) {
|
|
||||||
// 状态回退
|
|
||||||
onChange(oldValue!);
|
|
||||||
onCatch(err);
|
|
||||||
}
|
|
||||||
lockRef.current = false;
|
|
||||||
};
|
|
||||||
return cloneElement(children, childProps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return children as any;
|
const childProps = { ...children.props };
|
||||||
|
|
||||||
|
childProps[valueProps] = value;
|
||||||
|
childProps[onChangeProps] = async (...args: any[]) => {
|
||||||
|
// 多次操作无效
|
||||||
|
if (lockRef.current) return;
|
||||||
|
|
||||||
|
lockRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newValue = (onFormat as any)(...args);
|
||||||
|
// 先在ui上响应操作
|
||||||
|
onChange(newValue);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// save the old value
|
||||||
|
if (waitTime <= 0 || now - lastRef.current >= waitTime) {
|
||||||
|
saveRef.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRef.current = now;
|
||||||
|
|
||||||
|
if (waitTime <= 0) {
|
||||||
|
await onGuard(newValue, value!);
|
||||||
|
} else {
|
||||||
|
// debounce guard
|
||||||
|
clearTimeout(timeRef.current);
|
||||||
|
|
||||||
|
timeRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await onGuard(newValue, saveRef.current!);
|
||||||
|
} catch (err: any) {
|
||||||
|
// 状态回退
|
||||||
|
onChange(saveRef.current!);
|
||||||
|
onCatch(err);
|
||||||
|
}
|
||||||
|
}, waitTime);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// 状态回退
|
||||||
|
onChange(saveRef.current!);
|
||||||
|
onCatch(err);
|
||||||
|
}
|
||||||
|
lockRef.current = false;
|
||||||
|
};
|
||||||
|
return cloneElement(children, childProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GuardState;
|
export default GuardState;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useDebounceFn } from "ahooks";
|
|
||||||
import { useSetRecoilState } from "recoil";
|
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
import {
|
import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -11,7 +9,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { ApiType } from "../../services/types";
|
import { ApiType } from "../../services/types";
|
||||||
import { atomClashPort } from "../../states/setting";
|
import { atomClashPort } from "../../services/states";
|
||||||
import { patchClashConfig } from "../../services/cmds";
|
import { patchClashConfig } from "../../services/cmds";
|
||||||
import { SettingList, SettingItem } from "./setting";
|
import { SettingList, SettingItem } from "./setting";
|
||||||
import { getClashConfig, getVersion, updateConfigs } from "../../services/api";
|
import { getClashConfig, getVersion, updateConfigs } from "../../services/api";
|
||||||
@@ -25,18 +23,16 @@ interface Props {
|
|||||||
const SettingClash = ({ onError }: Props) => {
|
const SettingClash = ({ onError }: Props) => {
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
||||||
|
const { data: versionData } = useSWR("getVersion", getVersion);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ipv6 = false,
|
ipv6 = false,
|
||||||
"allow-lan": allowLan = false,
|
"allow-lan": allowLan = false,
|
||||||
"log-level": logLevel = "silent",
|
"log-level": logLevel = "silent",
|
||||||
"mixed-port": thePort = 0,
|
"mixed-port": mixedPort = 0,
|
||||||
} = clashConfig ?? {};
|
} = clashConfig ?? {};
|
||||||
|
|
||||||
const setPort = useSetRecoilState(atomClashPort);
|
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
||||||
const [mixedPort, setMixedPort] = useState(thePort);
|
|
||||||
|
|
||||||
useEffect(() => setMixedPort(thePort), [thePort]);
|
|
||||||
|
|
||||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||||
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
|
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
|
||||||
@@ -47,35 +43,25 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
await patchClashConfig(patch);
|
await patchClashConfig(patch);
|
||||||
};
|
};
|
||||||
|
|
||||||
// restart clash when port is changed
|
const onUpdatePort = async (port: number) => {
|
||||||
const { run: onUpdatePort } = useDebounceFn(
|
if (port < 1000) {
|
||||||
async (port: number) => {
|
throw new Error("The port should not < 1000");
|
||||||
try {
|
}
|
||||||
if (port < 1000) {
|
if (port > 65536) {
|
||||||
throw new Error("The port should not < 1000");
|
throw new Error("The port should not > 65536");
|
||||||
}
|
}
|
||||||
if (port > 65536) {
|
await patchClashConfig({ "mixed-port": port });
|
||||||
throw new Error("The port should not > 65536");
|
setGlobalClashPort(port);
|
||||||
}
|
Notice.success("Change Clash port successfully!");
|
||||||
await patchClashConfig({ "mixed-port": port });
|
|
||||||
onChangeData({ "mixed-port": port });
|
// update the config
|
||||||
setPort(port);
|
mutate("getClashConfig");
|
||||||
Notice.success("Change Clash port successfully!");
|
};
|
||||||
} catch (err: any) {
|
|
||||||
setMixedPort(thePort); // back to old port value
|
|
||||||
Notice.error(err.message ?? err.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ wait: 1000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// get clash core version
|
// get clash core version
|
||||||
const [clashVer, setClashVer] = useState("");
|
const clashVer = versionData?.premium
|
||||||
useEffect(() => {
|
? `${versionData.version} Premium`
|
||||||
getVersion().then(({ premium, version }) => {
|
: versionData?.version || "-";
|
||||||
setClashVer(premium ? `${version} Premium` : version);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title="Clash Setting">
|
<SettingList title="Clash Setting">
|
||||||
@@ -130,9 +116,11 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
<ListItemText primary="Mixed Port" />
|
<ListItemText primary="Mixed Port" />
|
||||||
<GuardState
|
<GuardState
|
||||||
value={mixedPort!}
|
value={mixedPort!}
|
||||||
|
onCatch={onError}
|
||||||
onFormat={(e: any) => +e.target.value?.replace(/\D+/, "")}
|
onFormat={(e: any) => +e.target.value?.replace(/\D+/, "")}
|
||||||
onChange={setMixedPort}
|
onChange={(e) => onChangeData({ "mixed-port": e })}
|
||||||
onGuard={onUpdatePort}
|
onGuard={onUpdatePort}
|
||||||
|
waitTime={800}
|
||||||
>
|
>
|
||||||
<TextField autoComplete="off" size="small" sx={{ width: 120 }} />
|
<TextField autoComplete="off" size="small" sx={{ width: 120 }} />
|
||||||
</GuardState>
|
</GuardState>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
|
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
enable_tun_mode = false,
|
||||||
enable_auto_launch = false,
|
enable_auto_launch = false,
|
||||||
enable_system_proxy = false,
|
enable_system_proxy = false,
|
||||||
system_proxy_bypass = "",
|
system_proxy_bypass = "",
|
||||||
@@ -28,6 +29,20 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title="System Setting">
|
<SettingList title="System Setting">
|
||||||
|
<SettingItem>
|
||||||
|
<ListItemText primary="Tun Mode" />
|
||||||
|
<GuardState
|
||||||
|
value={enable_tun_mode}
|
||||||
|
valueProps="checked"
|
||||||
|
onCatch={onError}
|
||||||
|
onFormat={onSwitchFormat}
|
||||||
|
onChange={(e) => onChangeData({ enable_tun_mode: e })}
|
||||||
|
onGuard={(e) => patchVergeConfig({ enable_tun_mode: e })}
|
||||||
|
>
|
||||||
|
<Switch edge="end" />
|
||||||
|
</GuardState>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem>
|
<SettingItem>
|
||||||
<ListItemText primary="Auto Launch" />
|
<ListItemText primary="Auto Launch" />
|
||||||
<GuardState
|
<GuardState
|
||||||
@@ -91,6 +106,7 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
onFormat={(e: any) => e.target.value}
|
onFormat={(e: any) => e.target.value}
|
||||||
onChange={(e) => onChangeData({ system_proxy_bypass: e })}
|
onChange={(e) => onChangeData({ system_proxy_bypass: e })}
|
||||||
onGuard={(e) => patchVergeConfig({ system_proxy_bypass: e })}
|
onGuard={(e) => patchVergeConfig({ system_proxy_bypass: e })}
|
||||||
|
waitTime={1000}
|
||||||
>
|
>
|
||||||
<TextField autoComplete="off" size="small" sx={{ width: 120 }} />
|
<TextField autoComplete="off" size="small" sx={{ width: 120 }} />
|
||||||
</GuardState>
|
</GuardState>
|
||||||
|
|||||||
@@ -1,38 +1,12 @@
|
|||||||
import dayjs from "dayjs";
|
import { useRecoilState } from "recoil";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button, Paper } from "@mui/material";
|
import { Button, Paper } from "@mui/material";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { ApiType } from "../services/types";
|
import { atomLogData } from "../services/states";
|
||||||
import { getInfomation } from "../services/api";
|
|
||||||
import BasePage from "../components/base/base-page";
|
import BasePage from "../components/base/base-page";
|
||||||
import LogItem from "../components/log/log-item";
|
import LogItem from "../components/log/log-item";
|
||||||
|
|
||||||
let logCache: ApiType.LogItem[] = [];
|
|
||||||
|
|
||||||
const LogPage = () => {
|
const LogPage = () => {
|
||||||
const [logData, setLogData] = useState(logCache);
|
const [logData, setLogData] = useRecoilState(atomLogData);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let ws: WebSocket | null = null;
|
|
||||||
|
|
||||||
getInfomation().then((result) => {
|
|
||||||
const { server = "", secret = "" } = result;
|
|
||||||
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
|
||||||
|
|
||||||
ws.addEventListener("message", (event) => {
|
|
||||||
const data = JSON.parse(event.data) as ApiType.LogItem;
|
|
||||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
|
||||||
setLogData((l) => (logCache = [...l, { ...data, time }]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => ws?.close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onClear = () => {
|
|
||||||
setLogData([]);
|
|
||||||
logCache = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
@@ -43,7 +17,7 @@ const LogPage = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={onClear}
|
onClick={() => setLogData([])}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
12
src/services/states.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
import { ApiType } from "./types";
|
||||||
|
|
||||||
|
export const atomClashPort = atom<number>({
|
||||||
|
key: "atomClashPort",
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const atomLogData = atom<ApiType.LogItem[]>({
|
||||||
|
key: "atomLogData",
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
@@ -113,6 +113,7 @@ export namespace CmdType {
|
|||||||
theme_mode?: "light" | "dark";
|
theme_mode?: "light" | "dark";
|
||||||
theme_blur?: boolean;
|
theme_blur?: boolean;
|
||||||
traffic_graph?: boolean;
|
traffic_graph?: boolean;
|
||||||
|
enable_tun_mode?: boolean;
|
||||||
enable_auto_launch?: boolean;
|
enable_auto_launch?: boolean;
|
||||||
enable_system_proxy?: boolean;
|
enable_system_proxy?: boolean;
|
||||||
enable_proxy_guard?: boolean;
|
enable_proxy_guard?: boolean;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { atom } from "recoil";
|
|
||||||
|
|
||||||
export const atomClashPort = atom<number>({
|
|
||||||
key: "atomClashPort",
|
|
||||||
default: 0,
|
|
||||||
});
|
|
||||||