diff --git a/src/components/guard-state.tsx b/src/components/guard-state.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f17fd51634af8d3b8f03718e7b8bd5551f54e2ca --- /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?: Value; + valueProps?: string; + onChangeProps?: string; + onChange?: (value: Value) => void; + onFormat?: (...args: any[]) => Value; + onGuard?: (value: Value) => Promise<void>; + onCatch?: (error: Error) => void; + children: ReactNode; +} + +function GuardState<T>(props: Props<T>) { + 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 0000000000000000000000000000000000000000..9fa9b324cc11263be0adfc584370b7e50b2350f8 --- /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<ConfigType>) => { + mutate("getClashConfig", { ...clashConfig, ...patch }, false); + }; + + const onUpdateData = async (patch: Partial<ConfigType>) => { + await updateConfigs(patch); + await patchClashConfig(patch); + }; + + return ( + <List sx={{ borderRadius: 1, boxShadow: 2, mt: 3 }}> + <ListSubheader>Clash设置</ListSubheader> + + <SettingItem> + <ListItemText primary="局域网连接" /> + <GuardState + value={allowLan} + valueProps="checked" + onCatch={onError} + onFormat={onSwitchFormat} + onChange={(e) => onChangeData({ "allow-lan": e })} + onGuard={(e) => onUpdateData({ "allow-lan": e })} + > + <Switch edge="end" /> + </GuardState> + </SettingItem> + + <SettingItem> + <ListItemText primary="IPv6" /> + <GuardState + value={ipv6} + valueProps="checked" + onCatch={onError} + onFormat={onSwitchFormat} + onChange={(e) => onChangeData({ ipv6: e })} + onGuard={(e) => onUpdateData({ ipv6: e })} + > + <Switch edge="end" /> + </GuardState> + </SettingItem> + + <SettingItem> + <ListItemText primary="日志ç‰çº§" /> + <GuardState + value={logLevel} + 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 }}> + <MenuItem value="info">Info</MenuItem> + <MenuItem value="warning">Warning</MenuItem> + <MenuItem value="error">Error</MenuItem> + <MenuItem value="debug">Debug</MenuItem> + <MenuItem value="silent">Silent</MenuItem> + </Select> + </GuardState> + </SettingItem> + + <SettingItem> + <ListItemText primary="æ··åˆä»£ç†ç«¯å£" /> + <TextField size="small" defaultValue={mixedPort!} sx={{ width: 120 }} /> + </SettingItem> + </List> + ); +}; + +export default SettingClash; diff --git a/src/components/setting-item.tsx b/src/components/setting-item.tsx new file mode 100644 index 0000000000000000000000000000000000000000..52a05843579988faf19758f733413fd0da3448d4 --- /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 0000000000000000000000000000000000000000..fa4794b0f1f1befe75fd626e07dec2d8428b1b0c --- /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<VergeConfig>) => { + mutate("getVergeConfig", { ...vergeConfig, ...patch }, false); + }; + + return ( + <List sx={{ borderRadius: 1, boxShadow: 2 }}> + <ListSubheader>通用设置</ListSubheader> + + <SettingItem> + <ListItemText primary="外观主题" /> + <GuardState + value={mode === "dark"} + valueProps="checked" + onCatch={onError} + onFormat={onSwitchFormat} + onChange={(e) => onChangeData({ theme_mode: e ? "dark" : "light" })} + onGuard={async (c) => { + await patchVergeConfig({ theme_mode: c ? "dark" : "light" }); + }} + > + <PaletteSwitch edge="end" /> + </GuardState> + </SettingItem> + + <SettingItem> + <ListItemText primary="开机自å¯" /> + <GuardState + value={startup} + valueProps="checked" + onCatch={onError} + onFormat={onSwitchFormat} + onChange={(e) => onChangeData({ enable_self_startup: e })} + onGuard={async (e) => { + await patchVergeConfig({ enable_self_startup: e }); + }} + > + <Switch edge="end" /> + </GuardState> + </SettingItem> + + <SettingItem> + <ListItemText primary="设置系统代ç†" /> + <GuardState + value={proxy} + valueProps="checked" + onCatch={onError} + onFormat={onSwitchFormat} + onChange={(e) => onChangeData({ enable_system_proxy: e })} + onGuard={async (e) => { + await setSysProxy(e); + await patchVergeConfig({ enable_system_proxy: e }); + }} + > + <Switch edge="end" /> + </GuardState> + </SettingItem> + </List> + ); +}; + +export default SettingVerge; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 143871e1c7ced12e835c6146eab0b32118d9cc2f..61beaaec8ed8294f935dc0b7832743a0205517a8 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 routers = [ + { + label: "代ç†", + link: "/proxy", + }, + { + label: "规则", + link: "/rules", + }, + { + label: "连接", + link: "/connections", + }, + { + label: "日志", + link: "/log", + }, + { + label: "设置", + link: "/setting", + }, +]; + const Layout = () => { - const paletteMode = useRecoilValue(atomPaletteMode); + const [mode, setMode] = useRecoilState(atomPaletteMode); + const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); - const routers = [ - { - label: "代ç†", - link: "/proxy", - }, - { - label: "规则", - link: "/rules", - }, - { - label: "连接", - link: "/connections", - }, - { - label: "日志", - link: "/log", - }, - { - label: "设置", - link: "/setting", - }, - ]; + 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 ( <SWRConfig value={{}}> diff --git a/src/pages/setting.tsx b/src/pages/setting.tsx index 0770a2de4f5ef2c05e4c54b517daa816b6287b5b..c15bc085da5cd3f2b4543ed58da02fd0ce8374ef 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 ( - <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}> + <Box sx={{ width: 0.9, maxWidth: 850, mx: "auto", mb: 2 }}> <Typography variant="h4" component="h1" sx={{ py: 2 }}> Setting </Typography> - <List sx={{ borderRadius: 1, boxShadow: 2 }}> - <ListSubheader>通用设置</ListSubheader> - - <MiniListItem> - <ListItemText primary="外观主题" /> - <PaletteSwitch - edge="end" - checked={mode !== "light"} - onChange={(_e, c) => setMode(c ? "dark" : "light")} - /> - </MiniListItem> - - <MiniListItem> - <ListItemText primary="开机自å¯" /> - <Switch edge="end" /> - </MiniListItem> - - <MiniListItem> - <ListItemText primary="设置系统代ç†" /> - <Switch - edge="end" - checked={proxy} - onChange={(_e, c) => onSysproxy(c)} - /> - </MiniListItem> - - <MiniListItem> - <ListItemText primary="局域网连接" /> - <Switch edge="end" /> - </MiniListItem> - - <MiniListItem> - <ListItemText primary="IPv6" /> - <Switch edge="end" /> - </MiniListItem> - - <MiniListItem> - <ListItemText primary="日志ç‰çº§" /> - <Select size="small" sx={{ width: 120 }}> - <MenuItem value="debug">Debug</MenuItem> - <MenuItem value="info">Info</MenuItem> - <MenuItem value="warning">Warning</MenuItem> - <MenuItem value="error">Error</MenuItem> - </Select> - </MiniListItem> + <SettingVerge /> - <MiniListItem> - <ListItemText primary="æ··åˆä»£ç†ç«¯å£" /> - <TextField size="small" defaultValue={7890} sx={{ width: 120 }} /> - </MiniListItem> - </List> + <SettingClash /> </Box> ); }; diff --git a/src/services/command.ts b/src/services/command.ts index 84045b2612f431d8c62258537966d003f3709b8f..5fcb97fa5cb1db0bd9c5d098f57214bc51f8a71e 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<void>("restart_sidecar"); @@ -14,6 +15,10 @@ export async function getClashInfo() { return invoke<ClashInfo | null>("get_clash_info"); } +export async function patchClashConfig(payload: Partial<ConfigType>) { + return invoke<void>("patch_clash_config", { payload }); +} + export async function importProfile(url: string) { return invoke<void>("import_profile", { url }); } @@ -56,3 +61,17 @@ export async function putProfiles(current: number) { export async function setSysProxy(enable: boolean) { return invoke<void>("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<VergeConfig>("get_verge_config"); +} + +export async function patchVergeConfig(payload: VergeConfig) { + return invoke<void>("patch_verge_config", { payload }); +} diff --git a/src/services/common.ts b/src/services/common.ts index 6ab0d3fc7d772bc9f7a85a2cfabf5ac8923bf1c5..604a85eeb5ab49ecb58925c8ebf64159e52565c2 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<ConfigType>; } diff --git a/src/utils/noop.ts b/src/utils/noop.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca6a744710d3ca4435f577fa0dfff4550d0f893f --- /dev/null +++ b/src/utils/noop.ts @@ -0,0 +1 @@ +export default function noop() {}