diff --git a/src/components/guard-state.tsx b/src/components/guard-state.tsx new file mode 100644 index 0000000..f17fd51 --- /dev/null +++ b/src/components/guard-state.tsx @@ -0,0 +1,58 @@ +import { cloneElement, isValidElement, ReactNode, useRef } from "react"; +import noop from "../utils/noop"; + +interface Props { + value?: Value; + valueProps?: string; + onChangeProps?: string; + onChange?: (value: Value) => void; + onFormat?: (...args: any[]) => Value; + onGuard?: (value: Value) => Promise; + onCatch?: (error: Error) => void; + children: ReactNode; +} + +function GuardState(props: Props) { + const { + value, + children, + valueProps = "value", + onChangeProps = "onChange", + onGuard = noop, + onCatch = noop, + onChange = noop, + onFormat = (v: T) => v, + } = props; + + const lockRef = useRef(false); + + if (isValidElement(children)) { + const childProps = { ...children.props }; + + 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); + } catch (err: any) { + // 状态回退 + onChange(oldValue!); + onCatch(err); + } + lockRef.current = false; + }; + return cloneElement(children, childProps); + } + + return children as any; +} + +export default GuardState; diff --git a/src/components/setting-clash.tsx b/src/components/setting-clash.tsx new file mode 100644 index 0000000..9fa9b32 --- /dev/null +++ b/src/components/setting-clash.tsx @@ -0,0 +1,101 @@ +import useSWR, { useSWRConfig } from "swr"; +import { + List, + ListItemText, + ListSubheader, + TextField, + Switch, + Select, + MenuItem, +} from "@mui/material"; +import { ConfigType, getClashConfig, updateConfigs } from "../services/common"; +import { patchClashConfig } from "../services/command"; +import GuardState from "./guard-state"; +import SettingItem from "./setting-item"; + +interface Props { + onError?: (err: Error) => void; +} + +const SettingClash = ({ onError }: Props) => { + const { mutate } = useSWRConfig(); + const { data: clashConfig } = useSWR("getClashConfig", getClashConfig); + + const { + ipv6 = false, + "allow-lan": allowLan = false, + "log-level": logLevel = "silent", + "mixed-port": mixedPort = 7890, + } = clashConfig ?? {}; + + const onSwitchFormat = (_e: any, value: boolean) => value; + + const onChangeData = (patch: Partial) => { + mutate("getClashConfig", { ...clashConfig, ...patch }, false); + }; + + const onUpdateData = async (patch: Partial) => { + await updateConfigs(patch); + await patchClashConfig(patch); + }; + + return ( + + Clash设置 + + + + onChangeData({ "allow-lan": e })} + onGuard={(e) => onUpdateData({ "allow-lan": e })} + > + + + + + + + onChangeData({ ipv6: e })} + onGuard={(e) => onUpdateData({ ipv6: e })} + > + + + + + + + e.target.value} + onChange={(e) => onChangeData({ "log-level": e })} + onGuard={(e) => onUpdateData({ "log-level": e })} + > + + + + + + + + + + ); +}; + +export default SettingClash; diff --git a/src/components/setting-item.tsx b/src/components/setting-item.tsx new file mode 100644 index 0000000..52a0584 --- /dev/null +++ b/src/components/setting-item.tsx @@ -0,0 +1,8 @@ +import { ListItem, styled } from "@mui/material"; + +const SettingItem = styled(ListItem)(() => ({ + paddingTop: 5, + paddingBottom: 5, +})); + +export default SettingItem; diff --git a/src/components/setting-verge.tsx b/src/components/setting-verge.tsx new file mode 100644 index 0000000..fa4794b --- /dev/null +++ b/src/components/setting-verge.tsx @@ -0,0 +1,89 @@ +import useSWR, { useSWRConfig } from "swr"; +import { List, ListItemText, ListSubheader, Switch } from "@mui/material"; +import { + getVergeConfig, + patchVergeConfig, + setSysProxy, + VergeConfig, +} from "../services/command"; +import GuardState from "./guard-state"; +import SettingItem from "./setting-item"; +import PaletteSwitch from "./palette-switch"; + +interface Props { + onError?: (err: Error) => void; +} + +const SettingVerge = ({ onError }: Props) => { + const { mutate } = useSWRConfig(); + const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); + + const { + theme_mode: mode = "light", + enable_self_startup: startup = false, + enable_system_proxy: proxy = false, + } = vergeConfig ?? {}; + + const onSwitchFormat = (_e: any, value: boolean) => value; + + const onChangeData = (patch: Partial) => { + mutate("getVergeConfig", { ...vergeConfig, ...patch }, false); + }; + + return ( + + 通用设置 + + + + onChangeData({ theme_mode: e ? "dark" : "light" })} + onGuard={async (c) => { + await patchVergeConfig({ theme_mode: c ? "dark" : "light" }); + }} + > + + + + + + + onChangeData({ enable_self_startup: e })} + onGuard={async (e) => { + await patchVergeConfig({ enable_self_startup: e }); + }} + > + + + + + + + onChangeData({ enable_system_proxy: e })} + onGuard={async (e) => { + await setSysProxy(e); + await patchVergeConfig({ enable_system_proxy: e }); + }} + > + + + + + ); +}; + +export default SettingVerge; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 143871e..61beaae 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -1,9 +1,10 @@ -import { useMemo } from "react"; -import { SWRConfig } from "swr"; +import { useEffect, useMemo } from "react"; +import useSWR, { SWRConfig } from "swr"; import { Route, Routes } from "react-router-dom"; -import { useRecoilValue } from "recoil"; +import { useRecoilState } from "recoil"; import { createTheme, List, Paper, ThemeProvider } from "@mui/material"; import { atomPaletteMode } from "../states/setting"; +import { getVergeConfig } from "../services/command"; import LogoSvg from "../assets/image/logo.svg"; import LogPage from "../pages/log"; import HomePage from "../pages/home"; @@ -14,34 +15,39 @@ import ConnectionsPage from "../pages/connections"; import ListItemLink from "../components/list-item-link"; import Traffic from "../components/traffic"; -const Layout = () => { - const paletteMode = useRecoilValue(atomPaletteMode); +const routers = [ + { + label: "代理", + link: "/proxy", + }, + { + label: "规则", + link: "/rules", + }, + { + label: "连接", + link: "/connections", + }, + { + label: "日志", + link: "/log", + }, + { + label: "设置", + link: "/setting", + }, +]; - const routers = [ - { - label: "代理", - link: "/proxy", - }, - { - label: "规则", - link: "/rules", - }, - { - label: "连接", - link: "/connections", - }, - { - label: "日志", - link: "/log", - }, - { - label: "设置", - link: "/setting", - }, - ]; +const Layout = () => { + const [mode, setMode] = useRecoilState(atomPaletteMode); + const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); + + useEffect(() => { + setMode(vergeConfig?.theme_mode ?? "light"); + }, [vergeConfig?.theme_mode]); const theme = useMemo(() => { - if (paletteMode === "light") { + if (mode === "light") { document.documentElement.style.background = "#f5f5f5"; document.documentElement.style.setProperty( "--selection-color", @@ -66,7 +72,7 @@ const Layout = () => { }, }, palette: { - mode: paletteMode, + mode, primary: { main: "#5b5c9d", }, @@ -76,7 +82,7 @@ const Layout = () => { }, }, }); - }, [paletteMode]); + }, [mode]); return ( diff --git a/src/pages/setting.tsx b/src/pages/setting.tsx index 0770a2d..c15bc08 100644 --- a/src/pages/setting.tsx +++ b/src/pages/setting.tsx @@ -1,101 +1,17 @@ -import { useState } from "react"; -import { useRecoilState } from "recoil"; -import { - Box, - List, - ListItem, - ListItemText, - ListSubheader, - Typography, - TextField, - styled, - Switch, - Select, - MenuItem, -} from "@mui/material"; -import { atomPaletteMode } from "../states/setting"; -import PaletteSwitch from "../components/palette-switch"; -import { setSysProxy } from "../services/command"; - -const MiniListItem = styled(ListItem)(({ theme }) => ({ - paddingTop: 5, - paddingBottom: 5, -})); +import { Box, Typography } from "@mui/material"; +import SettingVerge from "../components/setting-verge"; +import SettingClash from "../components/setting-clash"; const SettingPage = () => { - const [mode, setMode] = useRecoilState(atomPaletteMode); - const [proxy, setProxy] = useState(false); - - const onSysproxy = (enable: boolean) => { - const value = proxy; - setProxy(enable); - setSysProxy(enable) - .then(() => { - console.log("success"); - }) - .catch((err) => { - setProxy(value); // recover - console.log(err); - }); - }; - return ( - + Setting - - 通用设置 + - - - setMode(c ? "dark" : "light")} - /> - - - - - - - - - - onSysproxy(c)} - /> - - - - - - - - - - - - - - - - - - - - - - + ); }; diff --git a/src/services/command.ts b/src/services/command.ts index 84045b2..5fcb97f 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; +import { ConfigType } from "./common"; export async function restartSidecar() { return invoke("restart_sidecar"); @@ -14,6 +15,10 @@ export async function getClashInfo() { return invoke("get_clash_info"); } +export async function patchClashConfig(payload: Partial) { + return invoke("patch_clash_config", { payload }); +} + export async function importProfile(url: string) { return invoke("import_profile", { url }); } @@ -56,3 +61,17 @@ export async function putProfiles(current: number) { export async function setSysProxy(enable: boolean) { return invoke("set_sys_proxy", { enable }); } + +export interface VergeConfig { + theme_mode?: "light" | "dark"; + enable_self_startup?: boolean; + enable_system_proxy?: boolean; +} + +export async function getVergeConfig() { + return invoke("get_verge_config"); +} + +export async function patchVergeConfig(payload: VergeConfig) { + return invoke("patch_verge_config", { payload }); +} diff --git a/src/services/common.ts b/src/services/common.ts index 6ab0d3f..604a85e 100644 --- a/src/services/common.ts +++ b/src/services/common.ts @@ -12,14 +12,18 @@ export async function getVersion() { export interface ConfigType { port: number; mode: string; + ipv6: boolean; "socket-port": number; "allow-lan": boolean; "log-level": string; "mixed-port": number; + "redir-port": number; + "socks-port": number; + "tproxy-port": number; } /// Get current base configs -export async function getConfigs() { +export async function getClashConfig() { return (await getAxios()).get("/configs") as Promise; } diff --git a/src/utils/noop.ts b/src/utils/noop.ts new file mode 100644 index 0000000..ca6a744 --- /dev/null +++ b/src/utils/noop.ts @@ -0,0 +1 @@ +export default function noop() {}