From 892b919cf3ce0dd3b376218acbfcfa2e201b296b Mon Sep 17 00:00:00 2001
From: GyDi <zzzgydi@gmail.com>
Date: Sun, 20 Nov 2022 21:48:39 +0800
Subject: [PATCH] refactor: adjust setting dialog component

---
 src/components/base/base-dialog.tsx           |  66 +++++
 src/components/base/index.ts                  |   2 +
 .../setting/mods/clash-field-viewer.tsx       | 158 +++++-------
 .../setting/mods/clash-port-viewer.tsx        | 101 +++-----
 src/components/setting/mods/config-viewer.tsx | 102 ++++----
 .../setting/mods/controller-viewer.tsx        | 118 ++++-----
 src/components/setting/mods/core-switch.tsx   |   4 +-
 src/components/setting/mods/guard-state.tsx   |   4 +-
 src/components/setting/mods/hotkey-input.tsx  |   5 +-
 src/components/setting/mods/hotkey-viewer.tsx | 107 ++++----
 src/components/setting/mods/misc-viewer.tsx   | 126 ++++-----
 src/components/setting/mods/service-mode.tsx  | 117 ---------
 .../setting/mods/service-viewer.tsx           | 109 ++++++++
 .../{setting.tsx => mods/setting-comp.tsx}    |   0
 .../setting/mods/sysproxy-viewer.tsx          | 241 ++++++++----------
 .../setting/mods/theme-mode-switch.tsx        |   4 +-
 src/components/setting/mods/theme-viewer.tsx  | 137 ++++++++++
 src/components/setting/mods/web-ui-item.tsx   |   4 +-
 src/components/setting/mods/web-ui-viewer.tsx | 169 ++++++------
 src/components/setting/setting-clash.tsx      |  41 +--
 src/components/setting/setting-system.tsx     |  38 ++-
 src/components/setting/setting-theme.tsx      | 152 -----------
 src/components/setting/setting-verge.tsx      |  44 ++--
 23 files changed, 853 insertions(+), 996 deletions(-)
 create mode 100644 src/components/base/base-dialog.tsx
 create mode 100644 src/components/base/index.ts
 delete mode 100644 src/components/setting/mods/service-mode.tsx
 create mode 100644 src/components/setting/mods/service-viewer.tsx
 rename src/components/setting/{setting.tsx => mods/setting-comp.tsx} (100%)
 create mode 100644 src/components/setting/mods/theme-viewer.tsx
 delete mode 100644 src/components/setting/setting-theme.tsx

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<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 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<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 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<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 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<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 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<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 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<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 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<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 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) => {
     </Box>
   );
 };
-
-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<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 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<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 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 (
-    <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 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<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 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<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 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) => {
     </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 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<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 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<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 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<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 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<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 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 (
-      <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 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<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>
   );
 };
-- 
GitLab