diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2c1e426100d6bad819d5ec1f508839a3499a76d3 --- /dev/null +++ b/src/components/base/base-dialog.tsx @@ -0,0 +1,66 @@ +import { forwardRef, ReactNode, useImperativeHandle, useState } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + type SxProps, + type Theme, +} from "@mui/material"; + +interface Props { + title: ReactNode; + open: boolean; + okBtn?: ReactNode; + cancelBtn?: ReactNode; + disableOk?: boolean; + disableCancel?: boolean; + disableFooter?: boolean; + contentSx?: SxProps<Theme>; + onOk?: () => void; + onCancel?: () => void; + onClose?: () => void; +} + +export interface DialogRef { + open: () => void; + close: () => void; +} + +export const BaseDialog: React.FC<Props> = (props) => { + const { + open, + title, + children, + okBtn, + cancelBtn, + contentSx, + disableCancel, + disableOk, + disableFooter, + } = props; + + return ( + <Dialog open={open} onClose={props.onClose}> + <DialogTitle>{title}</DialogTitle> + + <DialogContent sx={contentSx}>{children}</DialogContent> + + {!disableFooter && ( + <DialogActions> + {!disableCancel && ( + <Button variant="outlined" onClick={props.onCancel}> + {cancelBtn} + </Button> + )} + {!disableOk && ( + <Button variant="contained" onClick={props.onOk}> + {okBtn} + </Button> + )} + </DialogActions> + )} + </Dialog> + ); +}; diff --git a/src/components/base/index.ts b/src/components/base/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef1eb46972037760941429302e92e805658a9a34 --- /dev/null +++ b/src/components/base/index.ts @@ -0,0 +1,2 @@ +export { BaseDialog, type DialogRef } from "./base-dialog"; +export { Notice } from "./base-notice"; diff --git a/src/components/setting/mods/clash-field-viewer.tsx b/src/components/setting/mods/clash-field-viewer.tsx index dbfa51dd3f6c3a94e25dad654bd1925f12416e8e..bb1579f3c058553b2a8e35e68166be5e611d32da 100644 --- a/src/components/setting/mods/clash-field-viewer.tsx +++ b/src/components/setting/mods/clash-field-viewer.tsx @@ -1,36 +1,18 @@ import useSWR from "swr"; -import { useEffect, useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Stack, - Tooltip, - Typography, -} from "@mui/material"; +import { Checkbox, Divider, Stack, Tooltip, Typography } from "@mui/material"; import { InfoRounded } from "@mui/icons-material"; -import { - getProfiles, - getRuntimeExists, - patchProfilesConfig, -} from "@/services/cmds"; -import { ModalHandler } from "@/hooks/use-modal-handler"; +import { getRuntimeExists } from "@/services/cmds"; import { HANDLE_FIELDS, DEFAULT_FIELDS, OTHERS_FIELDS, } from "@/utils/clash-fields"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { useProfiles } from "@/hooks/use-profiles"; import Notice from "@/components/base/base-notice"; -interface Props { - handler: ModalHandler; -} - const fieldSorter = (a: string, b: string) => { if (a.includes("-") === a.includes("-")) { if (a.length === b.length) return a.localeCompare(b); @@ -43,13 +25,10 @@ const fieldSorter = (a: string, b: string) => { const otherFields = [...OTHERS_FIELDS].sort(fieldSorter); const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter); -const ClashFieldViewer = ({ handler }: Props) => { +export const ClashFieldViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); - const { data: profiles = {}, mutate: mutateProfile } = useSWR( - "getProfiles", - getProfiles - ); + const { profiles = {}, patchProfiles } = useProfiles(); const { data: existsKeys = [], mutate: mutateExists } = useSWR( "getRuntimeExists", getRuntimeExists @@ -58,20 +37,14 @@ const ClashFieldViewer = ({ handler }: Props) => { const [open, setOpen] = useState(false); const [selected, setSelected] = useState<string[]>([]); - if (handler) { - handler.current = { - open: () => setOpen(true), - close: () => setOpen(false), - }; - } - - useEffect(() => { - if (open) { - mutateProfile(); + useImperativeHandle(ref, () => ({ + open: () => { mutateExists(); setSelected(profiles.valid || []); - } - }, [open, profiles.valid]); + setOpen(true); + }, + close: () => setOpen(false), + })); const handleChange = (item: string) => { if (!item) return; @@ -91,8 +64,7 @@ const ClashFieldViewer = ({ handler }: Props) => { if (curSet.size === oldSet.size && curSet.size === joinSet.size) return; try { - await patchProfilesConfig({ valid: [...curSet] }); - mutateProfile(); + await patchProfiles({ valid: [...curSet] }); // Notice.success("Refresh clash config", 1000); } catch (err: any) { Notice.error(err?.message || err.toString()); @@ -100,62 +72,56 @@ const ClashFieldViewer = ({ handler }: Props) => { }; return ( - <Dialog open={open} onClose={() => setOpen(false)}> - <DialogTitle>{t("Clash Field")}</DialogTitle> - - <DialogContent - sx={{ - pb: 0, - width: 320, - height: 300, - overflowY: "auto", - userSelect: "text", - }} - > - {otherFields.map((item) => { - const inSelect = selected.includes(item); - const inConfig = existsKeys.includes(item); - - return ( - <Stack key={item} mb={0.5} direction="row" alignItems="center"> - <Checkbox - checked={inSelect} - size="small" - sx={{ p: 0.5 }} - onChange={() => handleChange(item)} - /> - <Typography width="100%">{item}</Typography> - - {!inSelect && inConfig && <WarnIcon />} - </Stack> - ); - })} - - <Divider sx={{ my: 1 }}> - <Typography color="text.secondary" fontSize={14}> - Clash Verge Control Fields - </Typography> - </Divider> - - {handleFields.map((item) => ( + <BaseDialog + open={open} + title={t("Clash Field")} + contentSx={{ + pb: 0, + width: 320, + height: 300, + overflowY: "auto", + userSelect: "text", + }} + okBtn={t("Save")} + cancelBtn={t("Back")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={handleSave} + > + {otherFields.map((item) => { + const inSelect = selected.includes(item); + const inConfig = existsKeys.includes(item); + + return ( <Stack key={item} mb={0.5} direction="row" alignItems="center"> - <Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} /> - <Typography>{item}</Typography> + <Checkbox + checked={inSelect} + size="small" + sx={{ p: 0.5 }} + onChange={() => handleChange(item)} + /> + <Typography width="100%">{item}</Typography> + + {!inSelect && inConfig && <WarnIcon />} </Stack> - ))} - </DialogContent> - - <DialogActions> - <Button variant="outlined" onClick={() => setOpen(false)}> - {t("Back")} - </Button> - <Button variant="contained" onClick={handleSave}> - {t("Save")} - </Button> - </DialogActions> - </Dialog> + ); + })} + + <Divider sx={{ my: 1 }}> + <Typography color="text.secondary" fontSize={14}> + Clash Verge Control Fields + </Typography> + </Divider> + + {handleFields.map((item) => ( + <Stack key={item} mb={0.5} direction="row" alignItems="center"> + <Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} /> + <Typography>{item}</Typography> + </Stack> + ))} + </BaseDialog> ); -}; +}); function WarnIcon() { return ( @@ -164,5 +130,3 @@ function WarnIcon() { </Tooltip> ); } - -export default ClashFieldViewer; diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index 9c55367a21d4f747bbd24638f5fa265a2aa90723..36604cb3f345575f3452de13f6d7cd0236743e2f 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -1,30 +1,16 @@ import useSWR from "swr"; -import { useEffect, useState } from "react"; +import { forwardRef, useImperativeHandle, 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 { 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 { BaseDialog, DialogRef } from "@/components/base"; import Notice from "@/components/base/base-notice"; -interface Props { - handler: ModalHandler; -} - -const ClashPortViewer = ({ handler }: Props) => { +export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const { data: config, mutate: mutateClash } = useSWR( @@ -37,18 +23,15 @@ const ClashPortViewer = ({ handler }: Props) => { 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"]]); + useImperativeHandle(ref, () => ({ + open: () => { + if (config?.["mixed-port"]) { + setPort(config["mixed-port"]); + } + setOpen(true); + }, + close: () => setOpen(false), + })); const onSave = useLockFn(async () => { if (port < 1000) { @@ -72,36 +55,30 @@ const ClashPortViewer = ({ handler }: Props) => { }); 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> + <BaseDialog + open={open} + title={t("Clash Port")} + contentSx={{ width: 300 }} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + <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> + </BaseDialog> ); -}; - -export default ClashPortViewer; +}); diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx index 3de9af72daa67a2827369ae300878ef049748dd8..5b8810e40a7719f008b63e3590ac1a28d83db3d6 100644 --- a/src/components/setting/mods/config-viewer.tsx +++ b/src/components/setting/mods/config-viewer.tsx @@ -1,78 +1,76 @@ -import { useEffect, useRef } from "react"; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { useRecoilValue } from "recoil"; -import { - Button, - Chip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, -} from "@mui/material"; +import { Chip } from "@mui/material"; import { atomThemeMode } from "@/services/states"; import { getRuntimeYaml } from "@/services/cmds"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { editor } from "monaco-editor/esm/vs/editor/editor.api"; import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js"; import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js"; -import { editor } from "monaco-editor/esm/vs/editor/editor.api"; - -interface Props { - open: boolean; - onClose: () => void; -} - -const ConfigViewer = (props: Props) => { - const { open, onClose } = props; +export const ConfigViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); + const [open, setOpen] = useState(false); const editorRef = useRef<any>(); const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null); const themeMode = useRecoilValue(atomThemeMode); useEffect(() => { - if (!open) return; - - getRuntimeYaml().then((data) => { - const dom = editorRef.current; - - if (!dom) return; - if (instanceRef.current) instanceRef.current.dispose(); - - instanceRef.current = editor.create(editorRef.current, { - value: data ?? "# Error\n", - language: "yaml", - theme: themeMode === "light" ? "vs" : "vs-dark", - minimap: { enabled: false }, - readOnly: true, - }); - }); - return () => { if (instanceRef.current) { instanceRef.current.dispose(); instanceRef.current = null; } }; - }, [open]); + }, []); - return ( - <Dialog open={open} onClose={onClose}> - <DialogTitle> - {t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" /> - </DialogTitle> + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + + getRuntimeYaml().then((data) => { + const dom = editorRef.current; - <DialogContent sx={{ width: 520, pb: 1 }}> - <div style={{ width: "100%", height: "420px" }} ref={editorRef} /> - </DialogContent> + if (!dom) return; + if (instanceRef.current) instanceRef.current.dispose(); - <DialogActions> - <Button variant="outlined" onClick={onClose}> - {t("Back")} - </Button> - </DialogActions> - </Dialog> + instanceRef.current = editor.create(editorRef.current, { + value: data ?? "# Error\n", + language: "yaml", + theme: themeMode === "light" ? "vs" : "vs-dark", + minimap: { enabled: false }, + readOnly: true, + }); + }); + }, + close: () => setOpen(false), + })); + + return ( + <BaseDialog + open={open} + title={ + <> + {t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" /> + </> + } + contentSx={{ width: 520, pb: 1 }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + <div style={{ width: "100%", height: "420px" }} ref={editorRef} /> + </BaseDialog> ); -}; -export default ConfigViewer; +}); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index ec4c58acdd55a7483490b9b8b7902446dae7f4d2..3a49c8dbd52e5b07902230032c785fcd2e44e6cc 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -1,28 +1,14 @@ import useSWR from "swr"; -import { useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, - ListItem, - ListItemText, - TextField, -} from "@mui/material"; +import { List, ListItem, ListItemText, TextField } from "@mui/material"; import { getClashInfo, patchClashConfig } from "@/services/cmds"; -import { ModalHandler } from "@/hooks/use-modal-handler"; import { getAxios } from "@/services/api"; +import { BaseDialog, DialogRef } from "@/components/base"; import Notice from "@/components/base/base-notice"; -interface Props { - handler: ModalHandler; -} - -const ControllerViewer = ({ handler }: Props) => { +export const ControllerViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -30,16 +16,14 @@ const ControllerViewer = ({ handler }: Props) => { const [controller, setController] = useState(clashInfo?.server || ""); const [secret, setSecret] = useState(clashInfo?.secret || ""); - if (handler) { - handler.current = { - open: () => { - setOpen(true); - setController(clashInfo?.server || ""); - setSecret(clashInfo?.secret || ""); - }, - close: () => setOpen(false), - }; - } + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setController(clashInfo?.server || ""); + setSecret(clashInfo?.secret || ""); + }, + close: () => setOpen(false), + })); const onSave = useLockFn(async () => { try { @@ -55,47 +39,41 @@ const ControllerViewer = ({ handler }: Props) => { }); return ( - <Dialog open={open} onClose={() => setOpen(false)}> - <DialogTitle>{t("Clash Port")}</DialogTitle> - - <DialogContent sx={{ width: 400 }}> - <List> - <ListItem sx={{ padding: "5px 2px" }}> - <ListItemText primary="External Controller" /> - <TextField - size="small" - autoComplete="off" - sx={{ width: 175 }} - value={controller} - placeholder="Required" - onChange={(e) => setController(e.target.value)} - /> - </ListItem> + <BaseDialog + open={open} + title={t("Clash Port")} + contentSx={{ width: 400 }} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + <List> + <ListItem sx={{ padding: "5px 2px" }}> + <ListItemText primary="External Controller" /> + <TextField + size="small" + autoComplete="off" + sx={{ width: 175 }} + value={controller} + placeholder="Required" + onChange={(e) => setController(e.target.value)} + /> + </ListItem> - <ListItem sx={{ padding: "5px 2px" }}> - <ListItemText primary="Core Secret" /> - <TextField - size="small" - autoComplete="off" - sx={{ width: 175 }} - value={secret} - placeholder="Recommanded" - onChange={(e) => setSecret(e.target.value)} - /> - </ListItem> - </List> - </DialogContent> - - <DialogActions> - <Button variant="outlined" onClick={() => setOpen(false)}> - {t("Cancel")} - </Button> - <Button onClick={onSave} variant="contained"> - {t("Save")} - </Button> - </DialogActions> - </Dialog> + <ListItem sx={{ padding: "5px 2px" }}> + <ListItemText primary="Core Secret" /> + <TextField + size="small" + autoComplete="off" + sx={{ width: 175 }} + value={secret} + placeholder="Recommended" + onChange={(e) => setSecret(e.target.value)} + /> + </ListItem> + </List> + </BaseDialog> ); -}; - -export default ControllerViewer; +}); diff --git a/src/components/setting/mods/core-switch.tsx b/src/components/setting/mods/core-switch.tsx index c0a4417173eb12847e4cf4f3b8ee640d700d79ac..f57825924c146923aa38370d57ad3dfde013c0bf 100644 --- a/src/components/setting/mods/core-switch.tsx +++ b/src/components/setting/mods/core-switch.tsx @@ -12,7 +12,7 @@ const VALID_CORE = [ { name: "Clash Meta", core: "clash-meta" }, ]; -const CoreSwitch = () => { +export const CoreSwitch = () => { const { verge, mutateVerge } = useVerge(); const [anchorEl, setAnchorEl] = useState<any>(null); @@ -75,5 +75,3 @@ const CoreSwitch = () => { </> ); }; - -export default CoreSwitch; diff --git a/src/components/setting/mods/guard-state.tsx b/src/components/setting/mods/guard-state.tsx index 9b51b2eae0d6ce889f9c11f374c70fb2b384cd03..5ab8e99186c7035e2c93ef2e24725da1d399a365 100644 --- a/src/components/setting/mods/guard-state.tsx +++ b/src/components/setting/mods/guard-state.tsx @@ -13,7 +13,7 @@ interface Props<Value> { children: ReactNode; } -function GuardState<T>(props: Props<T>) { +export function GuardState<T>(props: Props<T>) { const { value, children, @@ -83,5 +83,3 @@ function GuardState<T>(props: Props<T>) { }; return cloneElement(children, childProps); } - -export default GuardState; diff --git a/src/components/setting/mods/hotkey-input.tsx b/src/components/setting/mods/hotkey-input.tsx index 63b53eb103aaaeaca57d5b2a928e7d5d29d70280..ddcb870dc6312d3942efbed37edf098408f8d6df 100644 --- a/src/components/setting/mods/hotkey-input.tsx +++ b/src/components/setting/mods/hotkey-input.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { alpha, Box, IconButton, styled } from "@mui/material"; import { DeleteRounded } from "@mui/icons-material"; import parseHotkey from "@/utils/parse-hotkey"; @@ -52,7 +51,7 @@ interface Props { onChange: (value: string[]) => void; } -const HotkeyInput = (props: Props) => { +export const HotkeyInput = (props: Props) => { const { value, onChange } = props; return ( @@ -92,5 +91,3 @@ const HotkeyInput = (props: Props) => { </Box> ); }; - -export default HotkeyInput; diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index ace2ad0e760275af1f3b697f1763fb72651ff013..9f41fde82bd4a26d8b0e287b89a0520920732c6e 100644 --- a/src/components/setting/mods/hotkey-viewer.tsx +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -1,19 +1,11 @@ -import { useEffect, useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - styled, - Typography, -} from "@mui/material"; +import { styled, Typography } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { ModalHandler } from "@/hooks/use-modal-handler"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { HotkeyInput } from "./hotkey-input"; import Notice from "@/components/base/base-notice"; -import HotkeyInput from "./hotkey-input"; const ItemWrapper = styled("div")` display: flex; @@ -35,42 +27,35 @@ const HOTKEY_FUNC = [ "disable_tun_mode", ]; -interface Props { - handler: ModalHandler; -} - -const HotkeyViewer = ({ handler }: Props) => { +export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - if (handler) { - handler.current = { - open: () => setOpen(true), - close: () => setOpen(false), - }; - } - const { verge, patchVerge } = useVerge(); const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({}); - useEffect(() => { - if (!open) return; - const map = {} as typeof hotkeyMap; + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); - verge?.hotkeys?.forEach((text) => { - const [func, key] = text.split(",").map((e) => e.trim()); + const map = {} as typeof hotkeyMap; - if (!func || !key) return; + verge?.hotkeys?.forEach((text) => { + const [func, key] = text.split(",").map((e) => e.trim()); - map[func] = key - .split("+") - .map((e) => e.trim()) - .map((k) => (k === "PLUS" ? "+" : k)); - }); + if (!func || !key) return; - setHotkeyMap(map); - }, [verge?.hotkeys, open]); + map[func] = key + .split("+") + .map((e) => e.trim()) + .map((k) => (k === "PLUS" ? "+" : k)); + }); + + setHotkeyMap(map); + }, + close: () => setOpen(false), + })); const onSave = useLockFn(async () => { const hotkeys = Object.entries(hotkeyMap) @@ -97,31 +82,25 @@ const HotkeyViewer = ({ handler }: Props) => { }); 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> + <BaseDialog + open={open} + title={t("Hotkey Viewer")} + contentSx={{ width: 450, maxHeight: 330 }} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + {HOTKEY_FUNC.map((func) => ( + <ItemWrapper key={func}> + <Typography>{t(func)}</Typography> + <HotkeyInput + value={hotkeyMap[func] ?? []} + onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))} + /> + </ItemWrapper> + ))} + </BaseDialog> ); -}; - -export default HotkeyViewer; +}); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index 098dd3fe8ec03ebad842474640c735766c17258e..89d2f991ab6d7b165990c4ec4528a66e6a20145b 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -1,27 +1,12 @@ -import { useEffect, useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, - ListItem, - ListItemText, - Switch, - TextField, -} from "@mui/material"; -import { ModalHandler } from "@/hooks/use-modal-handler"; +import { List, ListItem, ListItemText, Switch, TextField } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; +import { BaseDialog, DialogRef } from "@/components/base"; import Notice from "@/components/base/base-notice"; -interface Props { - handler: ModalHandler; -} - -const MiscViewer = ({ handler }: Props) => { +export const MiscViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const { verge, patchVerge } = useVerge(); @@ -31,21 +16,16 @@ const MiscViewer = ({ handler }: Props) => { defaultLatencyTest: "", }); - if (handler) { - handler.current = { - open: () => setOpen(true), - close: () => setOpen(false), - }; - } - - useEffect(() => { - if (open) { + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); setValues({ autoCloseConnection: verge?.auto_close_connection || false, defaultLatencyTest: verge?.default_latency_test || "", }); - } - }, [open, verge]); + }, + close: () => setOpen(false), + })); const onSave = useLockFn(async () => { try { @@ -60,51 +40,45 @@ const MiscViewer = ({ handler }: Props) => { }); return ( - <Dialog open={open} onClose={() => setOpen(false)}> - <DialogTitle>{t("Miscellaneous")}</DialogTitle> + <BaseDialog + open={open} + title={t("Miscellaneous")} + contentSx={{ width: 420 }} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + <List> + <ListItem sx={{ padding: "5px 2px" }}> + <ListItemText primary="Auto Close Connections" /> + <Switch + edge="end" + checked={values.autoCloseConnection} + onChange={(_, c) => + setValues((v) => ({ ...v, autoCloseConnection: c })) + } + /> + </ListItem> - <DialogContent sx={{ width: 420 }}> - <List> - <ListItem sx={{ padding: "5px 2px" }}> - <ListItemText primary="Auto Close Connections" /> - <Switch - edge="end" - checked={values.autoCloseConnection} - onChange={(_, c) => - setValues((v) => ({ ...v, autoCloseConnection: c })) - } - /> - </ListItem> - - <ListItem sx={{ padding: "5px 2px" }}> - <ListItemText primary="Default Latency Test" /> - <TextField - size="small" - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - spellCheck="false" - sx={{ width: 200 }} - value={values.defaultLatencyTest} - placeholder="http://www.gstatic.com/generate_204" - onChange={(e) => - setValues((v) => ({ ...v, defaultLatencyTest: e.target.value })) - } - /> - </ListItem> - </List> - </DialogContent> - - <DialogActions> - <Button variant="outlined" onClick={() => setOpen(false)}> - {t("Cancel")} - </Button> - <Button onClick={onSave} variant="contained"> - {t("Save")} - </Button> - </DialogActions> - </Dialog> + <ListItem sx={{ padding: "5px 2px" }}> + <ListItemText primary="Default Latency Test" /> + <TextField + size="small" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + sx={{ width: 200 }} + value={values.defaultLatencyTest} + placeholder="http://www.gstatic.com/generate_204" + onChange={(e) => + setValues((v) => ({ ...v, defaultLatencyTest: e.target.value })) + } + /> + </ListItem> + </List> + </BaseDialog> ); -}; - -export default MiscViewer; +}); diff --git a/src/components/setting/mods/service-mode.tsx b/src/components/setting/mods/service-mode.tsx deleted file mode 100644 index e6d50e53da0c58a99d94301a12a38e41ac349e7b..0000000000000000000000000000000000000000 --- a/src/components/setting/mods/service-mode.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import useSWR, { useSWRConfig } from "swr"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; -import { - Button, - Dialog, - DialogContent, - DialogTitle, - Stack, - Typography, -} from "@mui/material"; -import { - checkService, - installService, - uninstallService, - patchVergeConfig, -} from "@/services/cmds"; -import Notice from "@/components/base/base-notice"; -import noop from "@/utils/noop"; - -interface Props { - open: boolean; - enable: boolean; - onClose: () => void; - onError?: (err: Error) => void; -} - -const ServiceMode = (props: Props) => { - const { open, enable, onClose, onError = noop } = props; - - const { t } = useTranslation(); - const { mutate } = useSWRConfig(); - const { data: status } = useSWR("checkService", checkService, { - revalidateIfStale: true, - shouldRetryOnError: false, - }); - - const state = status != null ? status : "pending"; - - const onInstall = useLockFn(async () => { - try { - await installService(); - mutate("checkService"); - onClose(); - Notice.success("Service installed successfully"); - } catch (err: any) { - mutate("checkService"); - onError(err); - } - }); - - const onUninstall = useLockFn(async () => { - try { - if (state === "active" && enable) { - await patchVergeConfig({ enable_service_mode: false }); - } - - await uninstallService(); - mutate("checkService"); - onClose(); - Notice.success("Service uninstalled successfully"); - } catch (err: any) { - mutate("checkService"); - onError(err); - } - }); - - // fix unhandle error of the service mode - const onDisable = useLockFn(async () => { - await patchVergeConfig({ enable_service_mode: false }); - mutate("checkService"); - onClose(); - }); - - return ( - <Dialog open={open} onClose={onClose}> - <DialogTitle>{t("Service Mode")}</DialogTitle> - - <DialogContent sx={{ width: 360, userSelect: "text" }}> - <Typography>Current State: {state}</Typography> - - {(state === "unknown" || state === "uninstall") && ( - <Typography> - Infomation: Please make sure the Clash Verge Service is installed - and enabled - </Typography> - )} - - <Stack - direction="row" - spacing={1} - sx={{ mt: 4, justifyContent: "flex-end" }} - > - {state === "uninstall" && enable && ( - <Button variant="contained" onClick={onDisable}> - Disable Service Mode - </Button> - )} - - {state === "uninstall" && ( - <Button variant="contained" onClick={onInstall}> - Install - </Button> - )} - - {(state === "active" || state === "installed") && ( - <Button variant="outlined" onClick={onUninstall}> - Uninstall - </Button> - )} - </Stack> - </DialogContent> - </Dialog> - ); -}; - -export default ServiceMode; diff --git a/src/components/setting/mods/service-viewer.tsx b/src/components/setting/mods/service-viewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..52c0ebb1cde2f2e0bdbc01abc5f6e1057355beea --- /dev/null +++ b/src/components/setting/mods/service-viewer.tsx @@ -0,0 +1,109 @@ +import useSWR from "swr"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Button, Stack, Typography } from "@mui/material"; +import { + checkService, + installService, + uninstallService, + patchVergeConfig, +} from "@/services/cmds"; +import { forwardRef, useState } from "react"; +import { BaseDialog, DialogRef } from "@/components/base"; +import Notice from "@/components/base/base-notice"; + +interface Props { + enable: boolean; +} + +export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => { + const { enable } = props; + + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { data: status, mutate: mutateCheck } = useSWR( + "checkService", + checkService, + { revalidateIfStale: false, shouldRetryOnError: false } + ); + + const state = status != null ? status : "pending"; + + const onInstall = useLockFn(async () => { + try { + await installService(); + mutateCheck(); + setOpen(false); + Notice.success("Service installed successfully"); + } catch (err: any) { + mutateCheck(); + Notice.error(err.message || err.toString()); + } + }); + + const onUninstall = useLockFn(async () => { + try { + if (state === "active" && enable) { + await patchVergeConfig({ enable_service_mode: false }); + } + + await uninstallService(); + mutateCheck(); + setOpen(false); + Notice.success("Service uninstalled successfully"); + } catch (err: any) { + mutateCheck(); + Notice.error(err.message || err.toString()); + } + }); + + // fix unhandled error of the service mode + const onDisable = useLockFn(async () => { + await patchVergeConfig({ enable_service_mode: false }); + mutateCheck(); + setOpen(false); + }); + + return ( + <BaseDialog + open={open} + title={t("Service Mode")} + contentSx={{ width: 360, userSelect: "text" }} + onClose={() => setOpen(false)} + > + <Typography>Current State: {state}</Typography> + + {(state === "unknown" || state === "uninstall") && ( + <Typography> + Information: Please make sure the Clash Verge Service is installed and + enabled + </Typography> + )} + + <Stack + direction="row" + spacing={1} + sx={{ mt: 4, justifyContent: "flex-end" }} + > + {state === "uninstall" && enable && ( + <Button variant="contained" onClick={onDisable}> + Disable Service Mode + </Button> + )} + + {state === "uninstall" && ( + <Button variant="contained" onClick={onInstall}> + Install + </Button> + )} + + {(state === "active" || state === "installed") && ( + <Button variant="outlined" onClick={onUninstall}> + Uninstall + </Button> + )} + </Stack> + </BaseDialog> + ); +}); diff --git a/src/components/setting/setting.tsx b/src/components/setting/mods/setting-comp.tsx similarity index 100% rename from src/components/setting/setting.tsx rename to src/components/setting/mods/setting-comp.tsx diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index 42b8735778e933433e496d06796e7eb3b38915b5..edb5a8c27866f3cb9833ca4822b351461efb7d98 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -1,14 +1,8 @@ -import useSWR from "swr"; -import { useEffect, useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; import { Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, InputAdornment, List, ListItem, @@ -20,37 +14,19 @@ import { } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; import { getSystemProxy } from "@/services/cmds"; -import { ModalHandler } from "@/hooks/use-modal-handler"; +import { BaseDialog, DialogRef } from "@/components/base"; 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) => { +export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - if (handler) { - handler.current = { - open: () => setOpen(true), - close: () => setOpen(false), - }; - } - const { verge, patchVerge } = useVerge(); + type SysProxy = Awaited<ReturnType<typeof getSystemProxy>>; + const [sysproxy, setSysproxy] = useState<SysProxy>(); + const { enable_system_proxy: enabled, enable_proxy_guard, @@ -58,28 +34,28 @@ const SysproxyViewer = ({ handler }: Props) => { proxy_guard_duration, } = verge ?? {}; - 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, - }); - }, [verge]); + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setValue({ + guard: enable_proxy_guard, + bypass: system_proxy_bypass, + duration: proxy_guard_duration ?? 10, + }); + getSystemProxy().then((p) => setSysproxy(p)); + }, + close: () => setOpen(false), + })); const onSave = useLockFn(async () => { - if (value.duration < 5) { - Notice.error("Proxy guard duration at least 5 seconds"); + if (value.duration < 1) { + Notice.error("Proxy guard duration at least 1 seconds"); return; } @@ -104,94 +80,95 @@ const SysproxyViewer = ({ handler }: Props) => { }); 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")} + <BaseDialog + open={open} + title={t("System Proxy Setting")} + contentSx={{ width: 450, maxHeight: 300 }} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + <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> - <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> + </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> + </BaseDialog> ); -}; +}); -export default SysproxyViewer; +const FlexBox = styled("div")` + display: flex; + margin-top: 4px; + + .label { + flex: none; + width: 80px; + } +`; diff --git a/src/components/setting/mods/theme-mode-switch.tsx b/src/components/setting/mods/theme-mode-switch.tsx index 595dd6a3a61190d127c473e9455d5718696e0b67..29ae9ef0c633aaef427f9db566d8a5f0a8cba5a4 100644 --- a/src/components/setting/mods/theme-mode-switch.tsx +++ b/src/components/setting/mods/theme-mode-switch.tsx @@ -8,7 +8,7 @@ interface Props { onChange?: (value: ThemeValue) => void; } -const ThemeModeSwitch = (props: Props) => { +export const ThemeModeSwitch = (props: Props) => { const { value, onChange } = props; const { t } = useTranslation(); @@ -29,5 +29,3 @@ const ThemeModeSwitch = (props: Props) => { </ButtonGroup> ); }; - -export default ThemeModeSwitch; diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..014decd85646a6adaecdabd7facdf8d74abc84f6 --- /dev/null +++ b/src/components/setting/mods/theme-viewer.tsx @@ -0,0 +1,137 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { + List, + ListItem, + ListItemText, + styled, + TextField, + useTheme, +} from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; +import { BaseDialog, DialogRef } from "@/components/base"; +import Notice from "../../base/base-notice"; + +export const ThemeViewer = forwardRef<DialogRef>((props, ref) => { + const { t } = useTranslation(); + + const [open, setOpen] = useState(false); + const { verge, patchVerge } = useVerge(); + const { theme_setting } = verge ?? {}; + const [theme, setTheme] = useState(theme_setting || {}); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setTheme({ ...theme_setting } || {}); + }, + close: () => setOpen(false), + })); + + const textProps = { + size: "small", + autoComplete: "off", + sx: { width: 135 }, + } as const; + + const handleChange = (field: keyof typeof theme) => (e: any) => { + setTheme((t) => ({ ...t, [field]: e.target.value })); + }; + + const onSave = useLockFn(async () => { + try { + await patchVerge({ theme_setting: theme }); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + // default theme + const { palette } = useTheme(); + + const dt = palette.mode === "light" ? defaultTheme : defaultDarkTheme; + + type ThemeKey = keyof typeof theme & keyof typeof defaultTheme; + + const renderItem = (label: string, key: ThemeKey) => { + return ( + <Item> + <ListItemText primary={label} /> + <Round sx={{ background: theme[key] || dt[key] }} /> + <TextField + {...textProps} + value={theme[key] ?? ""} + placeholder={dt[key]} + onChange={handleChange(key)} + onKeyDown={(e) => e.key === "Enter" && onSave()} + /> + </Item> + ); + }; + + return ( + <BaseDialog + open={open} + title={t("Theme Setting")} + okBtn={t("Save")} + cancelBtn={t("Cancel")} + contentSx={{ width: 400, maxHeight: 300, overflow: "auto", pb: 0 }} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + <List sx={{ pt: 0 }}> + {renderItem("Primary Color", "primary_color")} + + {renderItem("Secondary Color", "secondary_color")} + + {renderItem("Primary Text", "primary_text")} + + {renderItem("Secondary Text", "secondary_text")} + + {renderItem("Info Color", "info_color")} + + {renderItem("Error Color", "error_color")} + + {renderItem("Warning Color", "warning_color")} + + {renderItem("Success Color", "success_color")} + + <Item> + <ListItemText primary="Font Family" /> + <TextField + {...textProps} + value={theme.font_family ?? ""} + onChange={handleChange("font_family")} + onKeyDown={(e) => e.key === "Enter" && onSave()} + /> + </Item> + + <Item> + <ListItemText primary="CSS Injection" /> + <TextField + {...textProps} + value={theme.css_injection ?? ""} + onChange={handleChange("css_injection")} + onKeyDown={(e) => e.key === "Enter" && onSave()} + /> + </Item> + </List> + </BaseDialog> + ); +}); + +const Item = styled(ListItem)(() => ({ + padding: "5px 2px", +})); + +const Round = styled("div")(() => ({ + width: "24px", + height: "24px", + borderRadius: "18px", + display: "inline-block", + marginRight: "8px", +})); diff --git a/src/components/setting/mods/web-ui-item.tsx b/src/components/setting/mods/web-ui-item.tsx index afc775d48074f69ad2f556a0cf3d6893c6820248..5d3d84d591edaef936973bcd76d67156852ed2ea 100644 --- a/src/components/setting/mods/web-ui-item.tsx +++ b/src/components/setting/mods/web-ui-item.tsx @@ -23,7 +23,7 @@ interface Props { onCancel?: () => void; } -const WebUIItem = (props: Props) => { +export const WebUIItem = (props: Props) => { const { value, onlyEdit = false, @@ -128,5 +128,3 @@ const WebUIItem = (props: Props) => { </> ); }; - -export default WebUIItem; diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index e0aafb08001e886f76303c8752ebfbc7a58a461e..cc0d679e2a8610a5348c78f1d2ab734f33a9dbc6 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -1,42 +1,31 @@ import useSWR from "swr"; -import { useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Typography, -} from "@mui/material"; +import { Button, Box, Typography } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; import { getClashInfo, openWebUrl } from "@/services/cmds"; -import { ModalHandler } from "@/hooks/use-modal-handler"; +import { WebUIItem } from "./web-ui-item"; +import { BaseDialog, DialogRef } from "@/components/base"; import BaseEmpty from "@/components/base/base-empty"; -import WebUIItem from "./web-ui-item"; +import Notice from "@/components/base/base-notice"; -interface Props { - handler: ModalHandler; - onError: (err: Error) => void; -} - -const WebUIViewer = ({ handler, onError }: Props) => { +export const WebUIViewer = forwardRef<DialogRef>((props, ref) => { const { t } = useTranslation(); const { verge, patchVerge, mutateVerge } = useVerge(); - const webUIList = verge?.web_ui_list || []; - const [open, setOpen] = useState(false); const [editing, setEditing] = useState(false); - if (handler) { - handler.current = { - open: () => setOpen(true), - close: () => setOpen(false), - }; - } + const { data: clashInfo } = useSWR("getClashInfo", getClashInfo); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const webUIList = verge?.web_ui_list || []; const handleAdd = useLockFn(async (value: string) => { const newList = [value, ...webUIList]; @@ -58,8 +47,6 @@ const WebUIViewer = ({ handler, onError }: Props) => { await patchVerge({ web_ui_list: newList }); }); - const { data: clashInfo } = useSWR("getClashInfo", getClashInfo); - const handleOpenUrl = useLockFn(async (value?: string) => { if (!value) return; try { @@ -83,74 +70,70 @@ const WebUIViewer = ({ handler, onError }: Props) => { await openWebUrl(url); } catch (e: any) { - onError(e); + Notice.error(e.message || e.toString()); } }); return ( - <Dialog open={open} onClose={() => setOpen(false)}> - <DialogTitle display="flex" justifyContent="space-between"> - {t("Web UI")} - <Button - variant="contained" - size="small" - disabled={editing} - onClick={() => setEditing(true)} - > - {t("New")} - </Button> - </DialogTitle> - - <DialogContent - sx={{ - width: 450, - height: 300, - pb: 1, - overflowY: "auto", - userSelect: "text", - }} - > - {editing && ( - <WebUIItem - value="" - onlyEdit - onChange={(v) => { - setEditing(false); - handleAdd(v || ""); - }} - onCancel={() => setEditing(false)} - /> - )} - - {!editing && webUIList.length === 0 && ( - <BaseEmpty - text="Empty List" - extra={ - <Typography mt={2} sx={{ fontSize: "12px" }}> - Replace host, port, secret with "%host" "%port" "%secret" - </Typography> - } - /> - )} - - {webUIList.map((item, index) => ( - <WebUIItem - key={index} - value={item} - onChange={(v) => handleChange(index, v)} - onDelete={() => handleDelete(index)} - onOpenUrl={handleOpenUrl} - /> - ))} - </DialogContent> - - <DialogActions> - <Button variant="outlined" onClick={() => setOpen(false)}> - {t("Back")} - </Button> - </DialogActions> - </Dialog> + <BaseDialog + open={open} + title={ + <Box display="flex" justifyContent="space-between"> + {t("Web UI")} + <Button + variant="contained" + size="small" + disabled={editing} + onClick={() => setEditing(true)} + > + {t("New")} + </Button> + </Box> + } + contentSx={{ + width: 450, + height: 300, + pb: 1, + overflowY: "auto", + userSelect: "text", + }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + {editing && ( + <WebUIItem + value="" + onlyEdit + onChange={(v) => { + setEditing(false); + handleAdd(v || ""); + }} + onCancel={() => setEditing(false)} + /> + )} + + {!editing && webUIList.length === 0 && ( + <BaseEmpty + text="Empty List" + extra={ + <Typography mt={2} sx={{ fontSize: "12px" }}> + Replace host, port, secret with "%host" "%port" "%secret" + </Typography> + } + /> + )} + + {webUIList.map((item, index) => ( + <WebUIItem + key={index} + value={item} + onChange={(v) => handleChange(index, v)} + onDelete={() => handleDelete(index)} + onOpenUrl={handleOpenUrl} + /> + ))} + </BaseDialog> ); -}; - -export default WebUIViewer; +}); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 06934f1e12b977faf80a6401db757af01f95be15..caaf5de807de7e5f754f7c0eefe4872ba3cbfcd5 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { TextField, @@ -10,15 +11,15 @@ import { } from "@mui/material"; 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 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"; -import ControllerViewer from "./mods/controller-viewer"; +import { DialogRef } from "@/components/base"; +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"; +import { ControllerViewer } from "./mods/controller-viewer"; +import { SettingList, SettingItem } from "./mods/setting-comp"; interface Props { onError: (err: Error) => void; @@ -40,10 +41,10 @@ const SettingClash = ({ onError }: Props) => { "mixed-port": mixedPort, } = clashConfig ?? {}; - const webUIHandler = useModalHandler(); - const fieldHandler = useModalHandler(); - const portHandler = useModalHandler(); - const controllerHandler = useModalHandler(); + const webRef = useRef<DialogRef>(null); + const fieldRef = useRef<DialogRef>(null); + const portRef = useRef<DialogRef>(null); + const ctrlRef = useRef<DialogRef>(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial<IConfigData>) => { @@ -61,10 +62,10 @@ const SettingClash = ({ onError }: Props) => { return ( <SettingList title={t("Clash Setting")}> - <WebUIViewer handler={webUIHandler} onError={onError} /> - <ClashFieldViewer handler={fieldHandler} /> - <ClashPortViewer handler={portHandler} /> - <ControllerViewer handler={controllerHandler} /> + <WebUIViewer ref={webRef} /> + <ClashFieldViewer ref={fieldRef} /> + <ClashPortViewer ref={portRef} /> + <ControllerViewer ref={ctrlRef} /> <SettingItem label={t("Allow Lan")}> <GuardState @@ -118,7 +119,7 @@ const SettingClash = ({ onError }: Props) => { value={mixedPort ?? 0} sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }} onClick={(e) => { - portHandler.current.open(); + portRef.current?.open(); (e.target as any).blur(); }} /> @@ -129,7 +130,7 @@ const SettingClash = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => controllerHandler.current.open()} + onClick={() => ctrlRef.current?.open()} > <ArrowForward /> </IconButton> @@ -140,7 +141,7 @@ const SettingClash = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => webUIHandler.current.open()} + onClick={() => webRef.current?.open()} > <ArrowForward /> </IconButton> @@ -151,7 +152,7 @@ const SettingClash = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => fieldHandler.current.open()} + onClick={() => fieldRef.current?.open()} > <ArrowForward /> </IconButton> diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index ba5db0b08e142e7831f7c141ad07fcd5fc69ac94..a23dc4f636d6622453601189a11aa0d44a869416 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -1,16 +1,16 @@ import useSWR from "swr"; -import { useState } from "react"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { IconButton, Switch } from "@mui/material"; import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material"; import { checkService } from "@/services/cmds"; import { useVerge } from "@/hooks/use-verge"; -import { SettingList, SettingItem } from "./setting"; -import useModalHandler from "@/hooks/use-modal-handler"; +import { DialogRef } from "@/components/base"; +import { SettingList, SettingItem } from "./mods/setting-comp"; +import { GuardState } from "./mods/guard-state"; +import { ServiceViewer } from "./mods/service-viewer"; +import { SysproxyViewer } from "./mods/sysproxy-viewer"; import getSystem from "@/utils/get-system"; -import GuardState from "./mods/guard-state"; -import ServiceMode from "./mods/service-mode"; -import SysproxyViewer from "./mods/sysproxy-viewer"; interface Props { onError?: (err: Error) => void; @@ -24,13 +24,15 @@ const SettingSystem = ({ onError }: Props) => { const { verge, mutateVerge, patchVerge } = useVerge(); // service mode - const [serviceOpen, setServiceOpen] = useState(false); const { data: serviceStatus } = useSWR( isWIN ? "checkService" : null, checkService, { revalidateIfStale: false, shouldRetryOnError: false } ); + const serviceRef = useRef<DialogRef>(null); + const sysproxyRef = useRef<DialogRef>(null); + const { enable_tun_mode, enable_auto_launch, @@ -44,11 +46,12 @@ const SettingSystem = ({ onError }: Props) => { mutateVerge({ ...verge, ...patch }, false); }; - const sysproxyHandler = useModalHandler(); - return ( <SettingList title={t("System Setting")}> - <SysproxyViewer handler={sysproxyHandler} /> + <SysproxyViewer ref={sysproxyRef} /> + {isWIN && ( + <ServiceViewer ref={serviceRef} enable={!!enable_service_mode} /> + )} <SettingItem label={t("Tun Mode")}> <GuardState @@ -71,7 +74,7 @@ const SettingSystem = ({ onError }: Props) => { <PrivacyTipRounded fontSize="small" style={{ cursor: "pointer", opacity: 0.75 }} - onClick={() => setServiceOpen(true)} + onClick={() => sysproxyRef.current?.open()} /> ) } @@ -92,7 +95,7 @@ const SettingSystem = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => setServiceOpen(true)} + onClick={() => sysproxyRef.current?.open()} > <ArrowForward /> </IconButton> @@ -100,22 +103,13 @@ const SettingSystem = ({ onError }: Props) => { </SettingItem> )} - {isWIN && ( - <ServiceMode - open={serviceOpen} - enable={!!enable_service_mode} - onError={onError} - onClose={() => setServiceOpen(false)} - /> - )} - <SettingItem label={t("System Proxy")} extra={ <Settings fontSize="small" style={{ cursor: "pointer", opacity: 0.75 }} - onClick={() => sysproxyHandler.current.open()} + onClick={() => sysproxyRef.current?.open()} /> } > diff --git a/src/components/setting/setting-theme.tsx b/src/components/setting/setting-theme.tsx deleted file mode 100644 index 041eec8e15377d502eee112b2f413b3e388206d8..0000000000000000000000000000000000000000 --- a/src/components/setting/setting-theme.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useEffect, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, - ListItem, - ListItemText, - styled, - TextField, - useTheme, -} from "@mui/material"; -import { useVerge } from "@/hooks/use-verge"; -import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; - -interface Props { - open: boolean; - onClose: () => void; - onError?: (err: Error) => void; -} - -const Item = styled(ListItem)(() => ({ - padding: "5px 2px", -})); - -const Round = styled("div")(() => ({ - width: "24px", - height: "24px", - borderRadius: "18px", - display: "inline-block", - marginRight: "8px", -})); - -const SettingTheme = (props: Props) => { - const { open, onClose, onError } = props; - - const { t } = useTranslation(); - - const { verge, patchVerge } = useVerge(); - - const { theme_setting } = verge ?? {}; - const [theme, setTheme] = useState(theme_setting || {}); - - useEffect(() => { - setTheme({ ...theme_setting } || {}); - }, [theme_setting]); - - const textProps = { - size: "small", - autoComplete: "off", - sx: { width: 135 }, - } as const; - - const handleChange = (field: keyof typeof theme) => (e: any) => { - setTheme((t) => ({ ...t, [field]: e.target.value })); - }; - - const onSave = useLockFn(async () => { - try { - await patchVerge({ theme_setting: theme }); - onClose(); - } catch (err: any) { - onError?.(err); - } - }); - - // default theme - const { palette } = useTheme(); - - const dt = palette.mode === "light" ? defaultTheme : defaultDarkTheme; - - type ThemeKey = keyof typeof theme & keyof typeof defaultTheme; - - const renderItem = (label: string, key: ThemeKey) => { - return ( - <Item> - <ListItemText primary={label} /> - <Round sx={{ background: theme[key] || dt[key] }} /> - <TextField - {...textProps} - value={theme[key] ?? ""} - placeholder={dt[key]} - onChange={handleChange(key)} - onKeyDown={(e) => e.key === "Enter" && onSave()} - /> - </Item> - ); - }; - - return ( - <Dialog open={open} onClose={onClose}> - <DialogTitle>{t("Theme Setting")}</DialogTitle> - - <DialogContent - sx={{ width: 400, maxHeight: 300, overflow: "auto", pb: 0 }} - > - <List sx={{ pt: 0 }}> - {renderItem("Primary Color", "primary_color")} - - {renderItem("Secondary Color", "secondary_color")} - - {renderItem("Primary Text", "primary_text")} - - {renderItem("Secondary Text", "secondary_text")} - - {renderItem("Info Color", "info_color")} - - {renderItem("Error Color", "error_color")} - - {renderItem("Warning Color", "warning_color")} - - {renderItem("Success Color", "success_color")} - - <Item> - <ListItemText primary="Font Family" /> - <TextField - {...textProps} - value={theme.font_family ?? ""} - onChange={handleChange("font_family")} - onKeyDown={(e) => e.key === "Enter" && onSave()} - /> - </Item> - - <Item> - <ListItemText primary="CSS Injection" /> - <TextField - {...textProps} - value={theme.css_injection ?? ""} - onChange={handleChange("css_injection")} - onKeyDown={(e) => e.key === "Enter" && onSave()} - /> - </Item> - </List> - </DialogContent> - - <DialogActions> - <Button variant="outlined" onClick={onClose}> - {t("Cancel")} - </Button> - <Button onClick={onSave} variant="contained"> - {t("Save")} - </Button> - </DialogActions> - </Dialog> - ); -}; - -export default SettingTheme; diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx index 2616defe9d4f472beabadd3c631f0952c525ce97..12cf47b5060c68ddd69bd0cd4c2daa5149453148 100644 --- a/src/components/setting/setting-verge.tsx +++ b/src/components/setting/setting-verge.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { IconButton, @@ -10,15 +10,15 @@ import { import { openAppDir, openLogsDir, patchVergeConfig } from "@/services/cmds"; import { ArrowForward } from "@mui/icons-material"; import { useVerge } from "@/hooks/use-verge"; -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 MiscViewer from "./mods/misc-viewer"; -import SettingTheme from "./setting-theme"; +import { DialogRef } from "@/components/base"; +import { SettingList, SettingItem } from "./mods/setting-comp"; +import { ThemeModeSwitch } from "./mods/theme-mode-switch"; +import { ConfigViewer } from "./mods/config-viewer"; +import { HotkeyViewer } from "./mods/hotkey-viewer"; +import { MiscViewer } from "./mods/misc-viewer"; +import { ThemeViewer } from "./mods/theme-viewer"; +import { GuardState } from "./mods/guard-state"; interface Props { onError?: (err: Error) => void; @@ -31,21 +31,22 @@ const SettingVerge = ({ onError }: Props) => { const { theme_mode, theme_blur, traffic_graph, language } = verge ?? {}; - const [themeOpen, setThemeOpen] = useState(false); - const [configOpen, setConfigOpen] = useState(false); + const configRef = useRef<DialogRef>(null); + const hotkeyRef = useRef<DialogRef>(null); + const miscRef = useRef<DialogRef>(null); + const themeRef = useRef<DialogRef>(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial<IVergeConfig>) => { mutateVerge({ ...verge, ...patch }, false); }; - const miscHandler = useModalHandler(); - const hotkeyHandler = useModalHandler(); - return ( <SettingList title={t("Verge Setting")}> - <HotkeyViewer handler={hotkeyHandler} /> - <MiscViewer handler={miscHandler} /> + <ThemeViewer ref={themeRef} /> + <ConfigViewer ref={configRef} /> + <HotkeyViewer ref={hotkeyRef} /> + <MiscViewer ref={miscRef} /> <SettingItem label={t("Language")}> <GuardState @@ -104,7 +105,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => miscHandler.current.open()} + onClick={() => miscRef.current?.open()} > <ArrowForward /> </IconButton> @@ -115,7 +116,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => setThemeOpen(true)} + onClick={() => themeRef.current?.open()} > <ArrowForward /> </IconButton> @@ -126,7 +127,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => hotkeyHandler.current.open()} + onClick={() => hotkeyRef.current?.open()} > <ArrowForward /> </IconButton> @@ -137,7 +138,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => setConfigOpen(true)} + onClick={() => configRef.current?.open()} > <ArrowForward /> </IconButton> @@ -168,9 +169,6 @@ const SettingVerge = ({ onError }: Props) => { <SettingItem label={t("Verge Version")}> <Typography sx={{ py: "7px" }}>v{version}</Typography> </SettingItem> - - <SettingTheme open={themeOpen} onClose={() => setThemeOpen(false)} /> - <ConfigViewer open={configOpen} onClose={() => setConfigOpen(false)} /> </SettingList> ); };