10 Commits

Author SHA1 Message Date
GyDi
b52a081e7b v0.0.18 2022-02-25 02:13:41 +08:00
GyDi
f981a44861 chore: ci 2022-02-25 02:12:55 +08:00
GyDi
d7c5ce0750 feat: clash tun mode supports 2022-02-25 02:09:39 +08:00
GyDi
9ccc66ca1e feat: use enhanced guard-state 2022-02-25 01:22:33 +08:00
GyDi
8606af3616 feat: guard state supports debounce guard 2022-02-25 01:21:13 +08:00
GyDi
f6e821ba6b feat: adjust clash version display 2022-02-24 23:04:18 +08:00
GyDi
e8dbcf819b feat: hide command window 2022-02-24 22:46:12 +08:00
GyDi
bbe2ef4e8e fix: use full clash config 2022-02-23 23:23:07 +08:00
GyDi
dd15455031 feat: enhance log data 2022-02-23 02:00:45 +08:00
GyDi
12ac7bb338 chore: update readme 2022-02-22 21:54:33 +08:00
24 changed files with 280 additions and 154 deletions

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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",

View File

@@ -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()),
}
} }

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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()
} }
} }

View 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}");
} }
} }

View File

@@ -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",

View File

@@ -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!;

View 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?.();
};
}, []);
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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: [],
});

View File

@@ -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;

View File

@@ -1,6 +0,0 @@
import { atom } from "recoil";
export const atomClashPort = atom<number>({
key: "atomClashPort",
default: 0,
});