diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx new file mode 100644 index 0000000..2c1e426 --- /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; + onOk?: () => void; + onCancel?: () => void; + onClose?: () => void; +} + +export interface DialogRef { + open: () => void; + close: () => void; +} + +export const BaseDialog: React.FC = (props) => { + const { + open, + title, + children, + okBtn, + cancelBtn, + contentSx, + disableCancel, + disableOk, + disableFooter, + } = props; + + return ( + + {title} + + {children} + + {!disableFooter && ( + + {!disableCancel && ( + + )} + {!disableOk && ( + + )} + + )} + + ); +}; diff --git a/src/components/base/index.ts b/src/components/base/index.ts new file mode 100644 index 0000000..ef1eb46 --- /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 dbfa51d..bb1579f 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((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([]); - 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 ( - setOpen(false)}> - {t("Clash Field")} + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={handleSave} + > + {otherFields.map((item) => { + const inSelect = selected.includes(item); + const inConfig = existsKeys.includes(item); - - {otherFields.map((item) => { - const inSelect = selected.includes(item); - const inConfig = existsKeys.includes(item); - - return ( - - handleChange(item)} - /> - {item} - - {!inSelect && inConfig && } - - ); - })} - - - - Clash Verge Control Fields - - - - {handleFields.map((item) => ( + return ( - - {item} - - ))} - + handleChange(item)} + /> + {item} - - - - - + {!inSelect && inConfig && } + + ); + })} + + + + Clash Verge Control Fields + + + + {handleFields.map((item) => ( + + + {item} + + ))} + ); -}; +}); function WarnIcon() { return ( @@ -164,5 +130,3 @@ function WarnIcon() { ); } - -export default ClashFieldViewer; diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index 9c55367..36604cb 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((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 ( - setOpen(false)}> - {t("Clash Port")} - - - - - - - setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - /> - - - - - - - - - + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + + setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) + } + /> + + + ); -}; - -export default ClashPortViewer; +}); diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx index 3de9af7..5b8810e 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((props, ref) => { const { t } = useTranslation(); + const [open, setOpen] = useState(false); const editorRef = useRef(); const instanceRef = useRef(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]); + }, []); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + + 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, + }); + }); + }, + close: () => setOpen(false), + })); return ( - - - {t("Runtime Config")} - - - -
- - - - - -
+ + {t("Runtime Config")} + + } + contentSx={{ width: 520, pb: 1 }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > +
+ ); -}; -export default ConfigViewer; +}); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index ec4c58a..3a49c8d 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((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 ( - setOpen(false)}> - {t("Clash Port")} + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + setController(e.target.value)} + /> + - - - - - setController(e.target.value)} - /> - - - - - setSecret(e.target.value)} - /> - - - - - - - - - + + + setSecret(e.target.value)} + /> + + + ); -}; - -export default ControllerViewer; +}); diff --git a/src/components/setting/mods/core-switch.tsx b/src/components/setting/mods/core-switch.tsx index c0a4417..f578259 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(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 9b51b2e..5ab8e99 100644 --- a/src/components/setting/mods/guard-state.tsx +++ b/src/components/setting/mods/guard-state.tsx @@ -13,7 +13,7 @@ interface Props { children: ReactNode; } -function GuardState(props: Props) { +export function GuardState(props: Props) { const { value, children, @@ -83,5 +83,3 @@ function GuardState(props: Props) { }; 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 63b53eb..ddcb870 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) => { ); }; - -export default HotkeyInput; diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index ace2ad0..9f41fde 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((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>({}); - 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 ( - setOpen(false)}> - {t("Hotkey Viewer")} - - - {HOTKEY_FUNC.map((func) => ( - - {t(func)} - setHotkeyMap((m) => ({ ...m, [func]: v }))} - /> - - ))} - - - - - - - + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + {HOTKEY_FUNC.map((func) => ( + + {t(func)} + setHotkeyMap((m) => ({ ...m, [func]: v }))} + /> + + ))} + ); -}; - -export default HotkeyViewer; +}); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index 098dd3f..89d2f99 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((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 ( - setOpen(false)}> - {t("Miscellaneous")} + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + + setValues((v) => ({ ...v, autoCloseConnection: c })) + } + /> + - - - - - - setValues((v) => ({ ...v, autoCloseConnection: c })) - } - /> - - - - - - setValues((v) => ({ ...v, defaultLatencyTest: e.target.value })) - } - /> - - - - - - - - - + + + + setValues((v) => ({ ...v, defaultLatencyTest: e.target.value })) + } + /> + + + ); -}; - -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 e6d50e5..0000000 --- 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 ( - - {t("Service Mode")} - - - Current State: {state} - - {(state === "unknown" || state === "uninstall") && ( - - Infomation: Please make sure the Clash Verge Service is installed - and enabled - - )} - - - {state === "uninstall" && enable && ( - - )} - - {state === "uninstall" && ( - - )} - - {(state === "active" || state === "installed") && ( - - )} - - - - ); -}; - -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 0000000..52c0ebb --- /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((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 ( + setOpen(false)} + > + Current State: {state} + + {(state === "unknown" || state === "uninstall") && ( + + Information: Please make sure the Clash Verge Service is installed and + enabled + + )} + + + {state === "uninstall" && enable && ( + + )} + + {state === "uninstall" && ( + + )} + + {(state === "active" || state === "installed") && ( + + )} + + + ); +}); 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 42b8735..edb5a8c 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((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>; + const [sysproxy, setSysproxy] = useState(); + 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 ( - setOpen(false)}> - {t("System Proxy Setting")} + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + setValue((v) => ({ ...v, guard: e }))} + /> + - - - - - setValue((v) => ({ ...v, guard: e }))} - /> - + + + s, + }} + onChange={(e) => { + setValue((v) => ({ + ...v, + duration: +e.target.value.replace(/\D/, ""), + })); + }} + /> + - - - s, - }} - onChange={(e) => { - setValue((v) => ({ - ...v, - duration: +e.target.value.replace(/\D/, ""), - })); - }} - /> - + + + + setValue((v) => ({ ...v, bypass: e.target.value })) + } + /> + + - - - - setValue((v) => ({ ...v, bypass: e.target.value })) - } - /> - - + + + {t("Current System Proxy")} + - - - {t("Current System Proxy")} + + Enable: + + {(!!sysproxy?.enable).toString()} + - - Enable: - - {(!!sysproxy?.enable).toString()} - - + + Server: + {sysproxy?.server || "-"} + - - Server: - {sysproxy?.server || "-"} - - - - Bypass: - {sysproxy?.bypass || "-"} - - - - - - - - - + + Bypass: + {sysproxy?.bypass || "-"} + + + ); -}; +}); -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 595dd6a..29ae9ef 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) => { ); }; - -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 0000000..014decd --- /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((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 ( + + + + e.key === "Enter" && onSave()} + /> + + ); + }; + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + {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")} + + + + e.key === "Enter" && onSave()} + /> + + + + + e.key === "Enter" && onSave()} + /> + + + + ); +}); + +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 afc775d..5d3d84d 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 e0aafb0..cc0d679 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((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 ( - setOpen(false)}> - - {t("Web UI")} - - + + {t("Web UI")} + + + } + contentSx={{ + width: 450, + height: 300, + pb: 1, + overflowY: "auto", + userSelect: "text", + }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + {editing && ( + { + setEditing(false); + handleAdd(v || ""); + }} + onCancel={() => setEditing(false)} + /> + )} - - {editing && ( - { - setEditing(false); - handleAdd(v || ""); - }} - onCancel={() => setEditing(false)} - /> - )} + {!editing && webUIList.length === 0 && ( + + Replace host, port, secret with "%host" "%port" "%secret" + + } + /> + )} - {!editing && webUIList.length === 0 && ( - - Replace host, port, secret with "%host" "%port" "%secret" - - } - /> - )} - - {webUIList.map((item, index) => ( - handleChange(index, v)} - onDelete={() => handleDelete(index)} - onOpenUrl={handleOpenUrl} - /> - ))} - - - - - - + {webUIList.map((item, index) => ( + handleChange(index, v)} + onDelete={() => handleDelete(index)} + onOpenUrl={handleOpenUrl} + /> + ))} + ); -}; - -export default WebUIViewer; +}); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 06934f1..caaf5de 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(null); + const fieldRef = useRef(null); + const portRef = useRef(null); + const ctrlRef = useRef(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { @@ -61,10 +62,10 @@ const SettingClash = ({ onError }: Props) => { return ( - - - - + + + + { 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()} > @@ -140,7 +141,7 @@ const SettingClash = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => webUIHandler.current.open()} + onClick={() => webRef.current?.open()} > @@ -151,7 +152,7 @@ const SettingClash = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => fieldHandler.current.open()} + onClick={() => fieldRef.current?.open()} > diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index ba5db0b..a23dc4f 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(null); + const sysproxyRef = useRef(null); + const { enable_tun_mode, enable_auto_launch, @@ -44,11 +46,12 @@ const SettingSystem = ({ onError }: Props) => { mutateVerge({ ...verge, ...patch }, false); }; - const sysproxyHandler = useModalHandler(); - return ( - + + {isWIN && ( + + )} { 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()} > @@ -100,22 +103,13 @@ const SettingSystem = ({ onError }: Props) => { )} - {isWIN && ( - setServiceOpen(false)} - /> - )} - 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 041eec8..0000000 --- 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 ( - - - - e.key === "Enter" && onSave()} - /> - - ); - }; - - return ( - - {t("Theme Setting")} - - - - {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")} - - - - e.key === "Enter" && onSave()} - /> - - - - - e.key === "Enter" && onSave()} - /> - - - - - - - - - - ); -}; - -export default SettingTheme; diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx index 2616def..12cf47b 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(null); + const hotkeyRef = useRef(null); + const miscRef = useRef(null); + const themeRef = useRef(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); }; - const miscHandler = useModalHandler(); - const hotkeyHandler = useModalHandler(); - return ( - - + + + + { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => miscHandler.current.open()} + onClick={() => miscRef.current?.open()} > @@ -115,7 +116,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => setThemeOpen(true)} + onClick={() => themeRef.current?.open()} > @@ -126,7 +127,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => hotkeyHandler.current.open()} + onClick={() => hotkeyRef.current?.open()} > @@ -137,7 +138,7 @@ const SettingVerge = ({ onError }: Props) => { color="inherit" size="small" sx={{ my: "2px" }} - onClick={() => setConfigOpen(true)} + onClick={() => configRef.current?.open()} > @@ -168,9 +169,6 @@ const SettingVerge = ({ onError }: Props) => { v{version} - - setThemeOpen(false)} /> - setConfigOpen(false)} /> ); };