From ab3404419696e29d7ea2f29a06d68cc504da5c02 Mon Sep 17 00:00:00 2001
From: GyDi <zzzgydi@gmail.com>
Date: Mon, 28 Nov 2022 22:29:58 +0800
Subject: [PATCH] feat: profile page ui

---
 src/components/profile/enhanced.tsx       |  93 --------
 src/components/profile/info-viewer.tsx    | 211 -----------------
 src/components/profile/profile-item.tsx   |  15 +-
 src/components/profile/profile-more.tsx   |  12 +-
 src/components/profile/profile-new.tsx    | 212 -----------------
 src/components/profile/profile-viewer.tsx | 274 ++++++++++++++++++++++
 src/hooks/use-profiles.ts                 |  10 +-
 src/locales/en.json                       |   2 +-
 src/locales/zh.json                       |   4 +-
 src/pages/profiles.tsx                    | 112 +++++++--
 src/services/types.d.ts                   |   4 +-
 11 files changed, 382 insertions(+), 567 deletions(-)
 delete mode 100644 src/components/profile/enhanced.tsx
 delete mode 100644 src/components/profile/info-viewer.tsx
 delete mode 100644 src/components/profile/profile-new.tsx
 create mode 100644 src/components/profile/profile-viewer.tsx

diff --git a/src/components/profile/enhanced.tsx b/src/components/profile/enhanced.tsx
deleted file mode 100644
index 6c20eb8..0000000
--- a/src/components/profile/enhanced.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import useSWR from "swr";
-import { useLockFn } from "ahooks";
-import { Grid } from "@mui/material";
-import {
-  getProfiles,
-  deleteProfile,
-  patchProfilesConfig,
-  getRuntimeLogs,
-} from "@/services/cmds";
-import { Notice } from "@/components/base";
-import { ProfileMore } from "./profile-more";
-
-interface Props {
-  items: IProfileItem[];
-  chain: string[];
-}
-
-export const EnhancedMode = (props: Props) => {
-  const { items, chain } = props;
-
-  const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
-  const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
-    "getRuntimeLogs",
-    getRuntimeLogs
-  );
-
-  const onEnhanceEnable = useLockFn(async (uid: string) => {
-    if (chain.includes(uid)) return;
-
-    const newChain = [...chain, uid];
-    await patchProfilesConfig({ chain: newChain });
-    mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
-    mutateLogs();
-  });
-
-  const onEnhanceDisable = useLockFn(async (uid: string) => {
-    if (!chain.includes(uid)) return;
-
-    const newChain = chain.filter((i) => i !== uid);
-    await patchProfilesConfig({ chain: newChain });
-    mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
-    mutateLogs();
-  });
-
-  const onEnhanceDelete = useLockFn(async (uid: string) => {
-    try {
-      await onEnhanceDisable(uid);
-      await deleteProfile(uid);
-      mutateProfiles();
-      mutateLogs();
-    } catch (err: any) {
-      Notice.error(err?.message || err.toString());
-    }
-  });
-
-  const onMoveTop = useLockFn(async (uid: string) => {
-    if (!chain.includes(uid)) return;
-
-    const newChain = [uid].concat(chain.filter((i) => i !== uid));
-    await patchProfilesConfig({ chain: newChain });
-    mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
-    mutateLogs();
-  });
-
-  const onMoveEnd = useLockFn(async (uid: string) => {
-    if (!chain.includes(uid)) return;
-
-    const newChain = chain.filter((i) => i !== uid).concat([uid]);
-    await patchProfilesConfig({ chain: newChain });
-    mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
-    mutateLogs();
-  });
-
-  return (
-    <Grid container spacing={{ xs: 2, lg: 3 }}>
-      {items.map((item) => (
-        <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
-          <ProfileMore
-            selected={!!chain.includes(item.uid)}
-            itemData={item}
-            enableNum={chain.length}
-            logInfo={chainLogs[item.uid]}
-            onEnable={() => onEnhanceEnable(item.uid)}
-            onDisable={() => onEnhanceDisable(item.uid)}
-            onDelete={() => onEnhanceDelete(item.uid)}
-            onMoveTop={() => onMoveTop(item.uid)}
-            onMoveEnd={() => onMoveEnd(item.uid)}
-          />
-        </Grid>
-      ))}
-    </Grid>
-  );
-};
diff --git a/src/components/profile/info-viewer.tsx b/src/components/profile/info-viewer.tsx
deleted file mode 100644
index ffeb3af..0000000
--- a/src/components/profile/info-viewer.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import { mutate } from "swr";
-import { useEffect, useState } from "react";
-import { useLockFn, useSetState } from "ahooks";
-import { useTranslation } from "react-i18next";
-import {
-  Button,
-  Collapse,
-  Dialog,
-  DialogActions,
-  DialogContent,
-  DialogTitle,
-  FormControlLabel,
-  IconButton,
-  Switch,
-  TextField,
-} from "@mui/material";
-import { Settings } from "@mui/icons-material";
-import { patchProfile } from "@/services/cmds";
-import { version } from "@root/package.json";
-import { Notice } from "@/components/base";
-
-interface Props {
-  open: boolean;
-  itemData: IProfileItem;
-  onClose: () => void;
-}
-
-// edit the profile item
-// remote / local file / merge / script
-export const InfoViewer = (props: Props) => {
-  const { open, itemData, onClose } = props;
-
-  const { t } = useTranslation();
-  const [form, setForm] = useSetState({ ...itemData });
-  const [option, setOption] = useSetState(itemData.option ?? {});
-  const [showOpt, setShowOpt] = useState(!!itemData.option);
-
-  useEffect(() => {
-    if (itemData) {
-      const { option } = itemData;
-      setForm({ ...itemData });
-      setOption(option ?? {});
-      setShowOpt(
-        itemData.type === "remote" &&
-          (!!option?.user_agent ||
-            !!option?.update_interval ||
-            !!option?.self_proxy ||
-            !!option?.with_proxy)
-      );
-    }
-  }, [itemData]);
-
-  const onUpdate = useLockFn(async () => {
-    try {
-      const { uid } = itemData;
-      const { name, desc, url } = form;
-      const option_ =
-        itemData.type === "remote" || itemData.type === "local"
-          ? option
-          : undefined;
-
-      if (itemData.type === "remote" && !url) {
-        throw new Error("Remote URL should not be null");
-      }
-
-      await patchProfile(uid, { uid, name, desc, url, option: option_ });
-      mutate("getProfiles");
-      onClose();
-    } catch (err: any) {
-      Notice.error(err?.message || err.toString());
-    }
-  });
-
-  const textFieldProps = {
-    fullWidth: true,
-    size: "small",
-    margin: "normal",
-    variant: "outlined",
-  } as const;
-
-  const type =
-    form.type ||
-    (form.url ? "remote" : form.file?.endsWith(".js") ? "script" : "local");
-
-  return (
-    <Dialog open={open} onClose={onClose}>
-      <DialogTitle sx={{ pb: 0.5 }}>{t("Edit Info")}</DialogTitle>
-
-      <DialogContent sx={{ width: 336, pb: 1, userSelect: "text" }}>
-        <TextField
-          {...textFieldProps}
-          disabled
-          label={t("Type")}
-          value={type}
-          sx={{ input: { textTransform: "capitalize" } }}
-        />
-
-        <TextField
-          {...textFieldProps}
-          autoFocus
-          label={t("Name")}
-          value={form.name}
-          onChange={(e) => setForm({ name: e.target.value })}
-          onKeyDown={(e) => e.key === "Enter" && onUpdate()}
-        />
-
-        <TextField
-          {...textFieldProps}
-          label={t("Descriptions")}
-          value={form.desc}
-          onChange={(e) => setForm({ desc: e.target.value })}
-          onKeyDown={(e) => e.key === "Enter" && onUpdate()}
-        />
-
-        {type === "remote" && (
-          <TextField
-            {...textFieldProps}
-            label={t("Subscription URL")}
-            value={form.url}
-            onChange={(e) => setForm({ url: e.target.value })}
-            onKeyDown={(e) => e.key === "Enter" && onUpdate()}
-          />
-        )}
-
-        {(type === "remote" || type === "local") && (
-          <TextField
-            {...textFieldProps}
-            label={t("Update Interval(mins)")}
-            value={option.update_interval}
-            onChange={(e) => {
-              const str = e.target.value?.replace(/\D/, "");
-              setOption({ update_interval: !!str ? +str : undefined });
-            }}
-            onKeyDown={(e) => e.key === "Enter" && onUpdate()}
-          />
-        )}
-
-        <Collapse
-          in={type === "remote" && showOpt}
-          timeout="auto"
-          unmountOnExit
-        >
-          <TextField
-            {...textFieldProps}
-            label="User Agent"
-            value={option.user_agent}
-            placeholder={`clash-verge/v${version}`}
-            onChange={(e) => setOption({ user_agent: e.target.value })}
-            onKeyDown={(e) => e.key === "Enter" && onUpdate()}
-          />
-
-          <FormControlLabel
-            label={t("Use System Proxy")}
-            labelPlacement="start"
-            sx={{ ml: 0, my: 1 }}
-            control={
-              <Switch
-                color="primary"
-                checked={option.with_proxy ?? false}
-                onChange={(_e, c) =>
-                  setOption((o) => ({
-                    self_proxy: c ? false : o.self_proxy ?? false,
-                    with_proxy: c,
-                  }))
-                }
-              />
-            }
-          />
-
-          <FormControlLabel
-            label={t("Use Clash Proxy")}
-            labelPlacement="start"
-            sx={{ ml: 0, my: 1 }}
-            control={
-              <Switch
-                color="primary"
-                checked={option.self_proxy ?? false}
-                onChange={(_e, c) =>
-                  setOption((o) => ({
-                    with_proxy: c ? false : o.with_proxy ?? false,
-                    self_proxy: c,
-                  }))
-                }
-              />
-            }
-          />
-        </Collapse>
-      </DialogContent>
-
-      <DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
-        {form.type === "remote" && (
-          <IconButton
-            size="small"
-            color="inherit"
-            sx={{ position: "absolute", left: 18 }}
-            onClick={() => setShowOpt((o) => !o)}
-          >
-            <Settings />
-          </IconButton>
-        )}
-
-        <Button onClick={onClose} variant="outlined">
-          {t("Cancel")}
-        </Button>
-        <Button onClick={onUpdate} variant="contained">
-          {t("Save")}
-        </Button>
-      </DialogActions>
-    </Dialog>
-  );
-};
diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx
index 2840017..b370307 100644
--- a/src/components/profile/profile-item.tsx
+++ b/src/components/profile/profile-item.tsx
@@ -17,7 +17,6 @@ import { RefreshRounded } from "@mui/icons-material";
 import { atomLoadingCache } from "@/services/states";
 import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
 import { Notice } from "@/components/base";
-import { InfoViewer } from "./info-viewer";
 import { EditorViewer } from "./editor-viewer";
 import { ProfileBox } from "./profile-box";
 import parseTraffic from "@/utils/parse-traffic";
@@ -31,10 +30,11 @@ interface Props {
   selected: boolean;
   itemData: IProfileItem;
   onSelect: (force: boolean) => void;
+  onEdit: () => void;
 }
 
 export const ProfileItem = (props: Props) => {
-  const { selected, itemData, onSelect } = props;
+  const { selected, itemData, onSelect, onEdit } = props;
 
   const { t } = useTranslation();
   const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -55,7 +55,7 @@ export const ProfileItem = (props: Props) => {
 
   const loading = loadingCache[itemData.uid] ?? false;
 
-  // interval update from now field
+  // interval update fromNow field
   const [, setRefresh] = useState({});
   useEffect(() => {
     if (!hasUrl) return;
@@ -83,12 +83,11 @@ export const ProfileItem = (props: Props) => {
     };
   }, [hasUrl, updated]);
 
-  const [editOpen, setEditOpen] = useState(false);
   const [fileOpen, setFileOpen] = useState(false);
 
   const onEditInfo = () => {
     setAnchorEl(null);
-    setEditOpen(true);
+    onEdit();
   };
 
   const onEditFile = () => {
@@ -298,12 +297,6 @@ export const ProfileItem = (props: Props) => {
         ))}
       </Menu>
 
-      <InfoViewer
-        open={editOpen}
-        itemData={itemData}
-        onClose={() => setEditOpen(false)}
-      />
-
       <EditorViewer
         uid={uid}
         open={fileOpen}
diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx
index 4094cad..f979665 100644
--- a/src/components/profile/profile-more.tsx
+++ b/src/components/profile/profile-more.tsx
@@ -14,7 +14,6 @@ import {
 import { FeaturedPlayListRounded } from "@mui/icons-material";
 import { viewProfile } from "@/services/cmds";
 import { Notice } from "@/components/base";
-import { InfoViewer } from "./info-viewer";
 import { EditorViewer } from "./editor-viewer";
 import { ProfileBox } from "./profile-box";
 import { LogViewer } from "./log-viewer";
@@ -29,6 +28,7 @@ interface Props {
   onMoveTop: () => void;
   onMoveEnd: () => void;
   onDelete: () => void;
+  onEdit: () => void;
 }
 
 // profile enhanced item
@@ -43,19 +43,19 @@ export const ProfileMore = (props: Props) => {
     onMoveTop,
     onMoveEnd,
     onDelete,
+    onEdit,
   } = props;
 
   const { uid, type } = itemData;
   const { t, i18n } = useTranslation();
   const [anchorEl, setAnchorEl] = useState<any>(null);
   const [position, setPosition] = useState({ left: 0, top: 0 });
-  const [editOpen, setEditOpen] = useState(false);
   const [fileOpen, setFileOpen] = useState(false);
   const [logOpen, setLogOpen] = useState(false);
 
   const onEditInfo = () => {
     setAnchorEl(null);
-    setEditOpen(true);
+    onEdit();
   };
 
   const onEditFile = () => {
@@ -219,12 +219,6 @@ export const ProfileMore = (props: Props) => {
           ))}
       </Menu>
 
-      <InfoViewer
-        open={editOpen}
-        itemData={itemData}
-        onClose={() => setEditOpen(false)}
-      />
-
       <EditorViewer
         uid={uid}
         open={fileOpen}
diff --git a/src/components/profile/profile-new.tsx b/src/components/profile/profile-new.tsx
deleted file mode 100644
index e7f92c9..0000000
--- a/src/components/profile/profile-new.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { useRef, useState } from "react";
-import { mutate } from "swr";
-import { useTranslation } from "react-i18next";
-import { useLockFn, useSetState } from "ahooks";
-import {
-  Button,
-  Collapse,
-  Dialog,
-  DialogActions,
-  DialogContent,
-  DialogTitle,
-  FormControl,
-  FormControlLabel,
-  IconButton,
-  InputLabel,
-  MenuItem,
-  Select,
-  Switch,
-  TextField,
-} from "@mui/material";
-import { Settings } from "@mui/icons-material";
-import { createProfile } from "@/services/cmds";
-import { Notice } from "@/components/base";
-import { FileInput } from "./file-input";
-
-interface Props {
-  open: boolean;
-  onClose: () => void;
-}
-
-// create a new profile
-// remote / local file / merge / script
-export const ProfileNew = (props: Props) => {
-  const { open, onClose } = props;
-
-  const { t } = useTranslation();
-  const [form, setForm] = useSetState({
-    type: "remote",
-    name: "",
-    desc: "",
-    url: "",
-  });
-
-  const [showOpt, setShowOpt] = useState(false);
-  // can add more option
-  const [option, setOption] = useSetState({
-    user_agent: "",
-    with_proxy: false,
-    self_proxy: false,
-  });
-  // file input
-  const fileDataRef = useRef<string | null>(null);
-
-  const onCreate = useLockFn(async () => {
-    if (!form.type) {
-      Notice.error("`Type` should not be null");
-      return;
-    }
-
-    try {
-      const name = form.name || `${form.type} file`;
-
-      if (form.type === "remote" && !form.url) {
-        throw new Error("The URL should not be null");
-      }
-
-      const option_ = form.type === "remote" ? option : undefined;
-      const item = { ...form, name, option: option_ };
-      const fileData = form.type === "local" ? fileDataRef.current : null;
-
-      await createProfile(item, fileData);
-
-      setForm({ type: "remote", name: "", desc: "", url: "" });
-      setOption({ user_agent: "" });
-      setShowOpt(false);
-      fileDataRef.current = null;
-
-      mutate("getProfiles");
-      onClose();
-    } catch (err: any) {
-      Notice.error(err.message || err.toString());
-    }
-  });
-
-  const textFieldProps = {
-    fullWidth: true,
-    size: "small",
-    margin: "normal",
-    variant: "outlined",
-  } as const;
-
-  return (
-    <Dialog open={open} onClose={onClose}>
-      <DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
-
-      <DialogContent sx={{ width: 336, pb: 1 }}>
-        <FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
-          <InputLabel>Type</InputLabel>
-          <Select
-            autoFocus
-            label={t("Type")}
-            value={form.type}
-            onChange={(e) => setForm({ type: e.target.value })}
-          >
-            <MenuItem value="remote">Remote</MenuItem>
-            <MenuItem value="local">Local</MenuItem>
-            <MenuItem value="script">Script</MenuItem>
-            <MenuItem value="merge">Merge</MenuItem>
-          </Select>
-        </FormControl>
-
-        <TextField
-          {...textFieldProps}
-          label={t("Name")}
-          autoComplete="off"
-          value={form.name}
-          onChange={(e) => setForm({ name: e.target.value })}
-        />
-
-        <TextField
-          {...textFieldProps}
-          label={t("Descriptions")}
-          autoComplete="off"
-          value={form.desc}
-          onChange={(e) => setForm({ desc: e.target.value })}
-        />
-
-        {form.type === "remote" && (
-          <TextField
-            {...textFieldProps}
-            label={t("Subscription URL")}
-            autoComplete="off"
-            value={form.url}
-            onChange={(e) => setForm({ url: e.target.value })}
-          />
-        )}
-
-        {form.type === "local" && (
-          <FileInput onChange={(val) => (fileDataRef.current = val)} />
-        )}
-
-        <Collapse
-          in={form.type === "remote" && showOpt}
-          timeout="auto"
-          unmountOnExit
-        >
-          <TextField
-            {...textFieldProps}
-            label="User Agent"
-            autoComplete="off"
-            value={option.user_agent}
-            onChange={(e) => setOption({ user_agent: e.target.value })}
-          />
-          <FormControlLabel
-            label={t("Use System Proxy")}
-            labelPlacement="start"
-            sx={{ ml: 0, my: 1 }}
-            control={
-              <Switch
-                color="primary"
-                checked={option.with_proxy}
-                onChange={(_e, c) =>
-                  setOption((o) => ({
-                    self_proxy: c ? false : o.self_proxy,
-                    with_proxy: c,
-                  }))
-                }
-              />
-            }
-          />
-          <FormControlLabel
-            label={t("Use Clash Proxy")}
-            labelPlacement="start"
-            sx={{ ml: 0, my: 1 }}
-            control={
-              <Switch
-                color="primary"
-                checked={option.self_proxy}
-                onChange={(_e, c) =>
-                  setOption((o) => ({
-                    with_proxy: c ? false : o.with_proxy,
-                    self_proxy: c,
-                  }))
-                }
-              />
-            }
-          />
-        </Collapse>
-      </DialogContent>
-
-      <DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
-        {form.type === "remote" && (
-          <IconButton
-            size="small"
-            color="inherit"
-            sx={{ position: "absolute", left: 18 }}
-            onClick={() => setShowOpt((o) => !o)}
-          >
-            <Settings />
-          </IconButton>
-        )}
-
-        <Button onClick={onClose} variant="outlined">
-          {t("Cancel")}
-        </Button>
-        <Button onClick={onCreate} variant="contained">
-          {t("Save")}
-        </Button>
-      </DialogActions>
-    </Dialog>
-  );
-};
diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx
new file mode 100644
index 0000000..563dbca
--- /dev/null
+++ b/src/components/profile/profile-viewer.tsx
@@ -0,0 +1,274 @@
+import {
+  forwardRef,
+  useEffect,
+  useImperativeHandle,
+  useRef,
+  useState,
+} from "react";
+import { useLockFn } from "ahooks";
+import { useTranslation } from "react-i18next";
+import { useForm, Controller } from "react-hook-form";
+import {
+  Box,
+  FormControl,
+  InputAdornment,
+  InputLabel,
+  MenuItem,
+  Select,
+  Switch,
+  styled,
+  TextField,
+} from "@mui/material";
+import { createProfile, patchProfile } from "@/services/cmds";
+import { BaseDialog, Notice } from "@/components/base";
+import { version } from "@root/package.json";
+import { FileInput } from "./file-input";
+
+interface Props {
+  onChange: () => void;
+}
+
+export interface ProfileViewerRef {
+  create: () => void;
+  edit: (item: IProfileItem) => void;
+}
+
+// create or edit the profile
+// remote / local / merge / script
+export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
+  (props, ref) => {
+    const { t } = useTranslation();
+    const [open, setOpen] = useState(false);
+    const [openType, setOpenType] = useState<"new" | "edit">("new");
+
+    // file input
+    const fileDataRef = useRef<string | null>(null);
+
+    const { control, watch, register, ...formIns } = useForm<IProfileItem>({
+      defaultValues: {
+        type: "remote",
+        name: "Remote File",
+        desc: "",
+        url: "",
+        option: {
+          // user_agent: "",
+          with_proxy: false,
+          self_proxy: false,
+        },
+      },
+    });
+
+    useImperativeHandle(ref, () => ({
+      create: () => {
+        setOpenType("new");
+        setOpen(true);
+      },
+      edit: (item) => {
+        if (item) {
+          Object.entries(item).forEach(([key, value]) => {
+            formIns.setValue(key as any, value);
+          });
+        }
+        setOpenType("edit");
+        setOpen(true);
+      },
+    }));
+
+    const selfProxy = watch("option.self_proxy");
+    const withProxy = watch("option.with_proxy");
+
+    useEffect(() => {
+      if (selfProxy) formIns.setValue("option.with_proxy", false);
+    }, [selfProxy]);
+
+    useEffect(() => {
+      if (withProxy) formIns.setValue("option.self_proxy", false);
+    }, [withProxy]);
+
+    const handleOk = useLockFn(
+      formIns.handleSubmit(async (form) => {
+        try {
+          if (!form.type) throw new Error("`Type` should not be null");
+          if (form.type === "remote" && !form.url) {
+            throw new Error("The URL should not be null");
+          }
+          if (form.type !== "remote" && form.type !== "local") {
+            delete form.option;
+          }
+          if (form.option?.update_interval) {
+            form.option.update_interval = +form.option.update_interval;
+          }
+          const name = form.name || `${form.type} file`;
+          const item = { ...form, name };
+
+          // 创建
+          if (openType === "new") {
+            await createProfile(item, fileDataRef.current);
+          }
+          // 编辑
+          else {
+            if (!form.uid) throw new Error("UID not found");
+            await patchProfile(form.uid, item);
+          }
+          setOpen(false);
+          setTimeout(() => formIns.reset(), 500);
+          fileDataRef.current = null;
+          props.onChange();
+        } catch (err: any) {
+          Notice.error(err.message);
+        }
+      })
+    );
+
+    const handleClose = () => {
+      setOpen(false);
+      fileDataRef.current = null;
+      setTimeout(() => formIns.reset(), 500);
+    };
+
+    const text = {
+      fullWidth: true,
+      size: "small",
+      margin: "normal",
+      variant: "outlined",
+      autoComplete: "off",
+      autoCorrect: "off",
+    } as const;
+
+    const formType = watch("type");
+    const isRemote = formType === "remote";
+    const isLocal = formType === "local";
+
+    return (
+      <BaseDialog
+        open={open}
+        title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
+        contentSx={{ width: 375, pb: 0, maxHeight: 320 }}
+        okBtn={t("Save")}
+        cancelBtn={t("Cancel")}
+        onClose={handleClose}
+        onCancel={handleClose}
+        onOk={handleOk}
+      >
+        <Controller
+          name="type"
+          control={control}
+          render={({ field }) => (
+            <FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
+              <InputLabel>{t("Type")}</InputLabel>
+              <Select {...field} autoFocus label={t("Type")}>
+                <MenuItem value="remote">Remote</MenuItem>
+                <MenuItem value="local">Local</MenuItem>
+                <MenuItem value="script">Script</MenuItem>
+                <MenuItem value="merge">Merge</MenuItem>
+              </Select>
+            </FormControl>
+          )}
+        />
+
+        <Controller
+          name="name"
+          control={control}
+          render={({ field }) => (
+            <TextField {...text} {...field} label={t("Name")} />
+          )}
+        />
+
+        <Controller
+          name="desc"
+          control={control}
+          render={({ field }) => (
+            <TextField {...text} {...field} label={t("Descriptions")} />
+          )}
+        />
+
+        {isRemote && (
+          <>
+            <Controller
+              name="url"
+              control={control}
+              render={({ field }) => (
+                <TextField {...text} {...field} label={t("Subscription URL")} />
+              )}
+            />
+
+            <Controller
+              name="option.user_agent"
+              control={control}
+              render={({ field }) => (
+                <TextField
+                  {...text}
+                  {...field}
+                  placeholder={`clash-verge/v${version}`}
+                  label="User Agent"
+                />
+              )}
+            />
+          </>
+        )}
+
+        {(isRemote || isLocal) && (
+          <Controller
+            name="option.update_interval"
+            control={control}
+            render={({ field }) => (
+              <TextField
+                {...text}
+                {...field}
+                onChange={(e) => {
+                  e.target.value = e.target.value
+                    ?.replace(/\D/, "")
+                    .slice(0, 10);
+                  field.onChange(e);
+                }}
+                label={t("Update Interval")}
+                InputProps={{
+                  endAdornment: (
+                    <InputAdornment position="end">mins</InputAdornment>
+                  ),
+                }}
+              />
+            )}
+          />
+        )}
+
+        {isLocal && openType === "new" && (
+          <FileInput onChange={(val) => (fileDataRef.current = val)} />
+        )}
+
+        {isRemote && (
+          <>
+            <Controller
+              name="option.with_proxy"
+              control={control}
+              render={({ field }) => (
+                <StyledBox>
+                  <InputLabel>{t("Use System Proxy")}</InputLabel>
+                  <Switch checked={field.value} {...field} color="primary" />
+                </StyledBox>
+              )}
+            />
+
+            <Controller
+              name="option.self_proxy"
+              control={control}
+              render={({ field }) => (
+                <StyledBox>
+                  <InputLabel>{t("Use Clash Proxy")}</InputLabel>
+                  <Switch checked={field.value} {...field} color="primary" />
+                </StyledBox>
+              )}
+            />
+          </>
+        )}
+      </BaseDialog>
+    );
+  }
+);
+
+const StyledBox = styled(Box)(() => ({
+  margin: "8px 0 8px 8px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+}));
diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts
index 17b2886..9b6c5fe 100644
--- a/src/hooks/use-profiles.ts
+++ b/src/hooks/use-profiles.ts
@@ -6,17 +6,20 @@ import {
 } from "@/services/cmds";
 
 export const useProfiles = () => {
-  const { data: profiles, mutate } = useSWR("getProfiles", getProfiles);
+  const { data: profiles, mutate: mutateProfiles } = useSWR(
+    "getProfiles",
+    getProfiles
+  );
 
   const patchProfiles = async (value: Partial<IProfilesConfig>) => {
     await patchProfilesConfig(value);
-    mutate();
+    mutateProfiles();
   };
 
   const patchCurrent = async (value: Partial<IProfileItem>) => {
     if (profiles?.current) {
       await patchProfile(profiles.current, value);
-      mutate();
+      mutateProfiles();
     }
   };
 
@@ -25,5 +28,6 @@ export const useProfiles = () => {
     current: profiles?.items?.find((p) => p.uid === profiles.current),
     patchProfiles,
     patchCurrent,
+    mutateProfiles,
   };
 };
diff --git a/src/locales/en.json b/src/locales/en.json
index e6e7774..9c139c9 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -50,7 +50,7 @@
   "Name": "Name",
   "Descriptions": "Descriptions",
   "Subscription URL": "Subscription URL",
-  "Update Interval(mins)": "Update Interval(mins)",
+  "Update Interval": "Update Interval",
 
   "Settings": "Settings",
   "Clash Setting": "Clash Setting",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index eb9d627..d836e5c 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -50,7 +50,9 @@
   "Name": "名称",
   "Descriptions": "描述",
   "Subscription URL": "订阅链接",
-  "Update Interval(mins)": "更新间隔(分钟)",
+  "Update Interval": "æ›´æ–°é—´éš”",
+  "Use System Proxy": "使用系统代理更新",
+  "Use Clash Proxy": "使用Clash代理更新",
 
   "Settings": "设置",
   "Clash Setting": "Clash 设置",
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 8e618f0..ff4ab8b 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -1,6 +1,6 @@
 import useSWR, { mutate } from "swr";
 import { useLockFn } from "ahooks";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
 import { useSetRecoilState } from "recoil";
 import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
 import { CachedRounded } from "@mui/icons-material";
@@ -8,27 +8,39 @@ import { useTranslation } from "react-i18next";
 import {
   getProfiles,
   patchProfile,
-  patchProfilesConfig,
   importProfile,
   enhanceProfiles,
+  getRuntimeLogs,
+  deleteProfile,
 } from "@/services/cmds";
 import { closeAllConnections, getProxies, updateProxy } from "@/services/api";
 import { atomCurrentProfile } from "@/services/states";
 import { BasePage, Notice } from "@/components/base";
-import { ProfileNew } from "@/components/profile/profile-new";
+import {
+  ProfileViewer,
+  ProfileViewerRef,
+} from "@/components/profile/profile-viewer";
 import { ProfileItem } from "@/components/profile/profile-item";
-import { EnhancedMode } from "@/components/profile/enhanced";
+import { ProfileMore } from "@/components/profile/profile-more";
+import { useProfiles } from "@/hooks/use-profiles";
 
 const ProfilePage = () => {
   const { t } = useTranslation();
 
   const [url, setUrl] = useState("");
   const [disabled, setDisabled] = useState(false);
-  const [dialogOpen, setDialogOpen] = useState(false);
 
   const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
 
-  const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
+  const { profiles = {}, patchProfiles, mutateProfiles } = useProfiles();
+
+  const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
+    "getRuntimeLogs",
+    getRuntimeLogs
+  );
+
+  const chain = profiles.chain || [];
+  const viewerRef = useRef<ProfileViewerRef>(null);
 
   // distinguish type
   const { regularItems, enhanceItems } = useMemo(() => {
@@ -40,9 +52,7 @@ const ProfilePage = () => {
 
     const regularItems = items.filter((i) => type1.includes(i.type!));
     const restItems = items.filter((i) => type2.includes(i.type!));
-
     const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
-
     const enhanceItems = chain
       .map((i) => restMap[i]!)
       .concat(restItems.filter((i) => !chain.includes(i.uid)));
@@ -75,8 +85,9 @@ const ProfilePage = () => {
 
       const { global, groups } = proxiesData;
       [global, ...groups].forEach((group) => {
-        const { name, now } = group;
+        const { type, name, now } = group;
 
+        if (type !== "Selector" && type !== "Fallback") return;
         if (!now || selectedMap[name] === now) return;
         if (selectedMap[name] == null) {
           selectedMap[name] = now!;
@@ -114,13 +125,13 @@ const ProfilePage = () => {
 
         if (!newProfiles.current && remoteItem) {
           const current = remoteItem.uid;
-          patchProfilesConfig({ current });
-          mutate("getProfiles", { ...newProfiles, current }, true);
-          mutate("getRuntimeLogs");
+          patchProfiles({ current });
+          mutateProfiles();
+          mutateLogs();
         }
       });
-    } catch {
-      Notice.error("Failed to import profile.");
+    } catch (err: any) {
+      Notice.error(err.message || err.toString());
     } finally {
       setDisabled(false);
     }
@@ -128,12 +139,10 @@ const ProfilePage = () => {
 
   const onSelect = useLockFn(async (current: string, force: boolean) => {
     if (!force && current === profiles.current) return;
-
     try {
-      await patchProfilesConfig({ current });
+      await patchProfiles({ current });
       setCurrentProfile(current);
-      mutate("getProfiles", { ...profiles, current: current }, true);
-      mutate("getRuntimeLogs");
+      mutateLogs();
       closeAllConnections();
       Notice.success("Refresh clash config", 1000);
     } catch (err: any) {
@@ -144,13 +153,52 @@ const ProfilePage = () => {
   const onEnhance = useLockFn(async () => {
     try {
       await enhanceProfiles();
-      mutate("getRuntimeLogs");
-      // Notice.success("Refresh clash config", 1000);
+      mutateLogs();
+      Notice.success("Refresh clash config", 1000);
     } catch (err: any) {
       Notice.error(err.message || err.toString(), 3000);
     }
   });
 
+  const onEnable = useLockFn(async (uid: string) => {
+    if (chain.includes(uid)) return;
+    const newChain = [...chain, uid];
+    await patchProfiles({ chain: newChain });
+    mutateLogs();
+  });
+
+  const onDisable = useLockFn(async (uid: string) => {
+    if (!chain.includes(uid)) return;
+    const newChain = chain.filter((i) => i !== uid);
+    await patchProfiles({ chain: newChain });
+    mutateLogs();
+  });
+
+  const onDelete = useLockFn(async (uid: string) => {
+    try {
+      await onDisable(uid);
+      await deleteProfile(uid);
+      mutateProfiles();
+      mutateLogs();
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString());
+    }
+  });
+
+  const onMoveTop = useLockFn(async (uid: string) => {
+    if (!chain.includes(uid)) return;
+    const newChain = [uid].concat(chain.filter((i) => i !== uid));
+    await patchProfiles({ chain: newChain });
+    mutateLogs();
+  });
+
+  const onMoveEnd = useLockFn(async (uid: string) => {
+    if (!chain.includes(uid)) return;
+    const newChain = chain.filter((i) => i !== uid).concat([uid]);
+    await patchProfiles({ chain: newChain });
+    mutateLogs();
+  });
+
   return (
     <BasePage
       title={t("Profiles")}
@@ -191,7 +239,7 @@ const ProfilePage = () => {
         <Button
           variant="contained"
           size="small"
-          onClick={() => setDialogOpen(true)}
+          onClick={viewerRef.current?.create}
         >
           {t("New")}
         </Button>
@@ -205,6 +253,7 @@ const ProfilePage = () => {
                 selected={profiles.current === item.uid}
                 itemData={item}
                 onSelect={(f) => onSelect(item.uid, f)}
+                onEdit={() => viewerRef.current?.edit(item)}
               />
             </Grid>
           ))}
@@ -212,10 +261,27 @@ const ProfilePage = () => {
       </Box>
 
       {enhanceItems.length > 0 && (
-        <EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
+        <Grid container spacing={{ xs: 2, lg: 3 }}>
+          {enhanceItems.map((item) => (
+            <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
+              <ProfileMore
+                selected={!!chain.includes(item.uid)}
+                itemData={item}
+                enableNum={chain.length || 0}
+                logInfo={chainLogs[item.uid]}
+                onEnable={() => onEnable(item.uid)}
+                onDisable={() => onDisable(item.uid)}
+                onDelete={() => onDelete(item.uid)}
+                onMoveTop={() => onMoveTop(item.uid)}
+                onMoveEnd={() => onMoveEnd(item.uid)}
+                onEdit={() => viewerRef.current?.edit(item)}
+              />
+            </Grid>
+          ))}
+        </Grid>
       )}
 
-      <ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
+      <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
     </BasePage>
   );
 };
diff --git a/src/services/types.d.ts b/src/services/types.d.ts
index 17fb5bf..35fb437 100644
--- a/src/services/types.d.ts
+++ b/src/services/types.d.ts
@@ -91,8 +91,6 @@ interface IConnections {
  * Some interface for command
  */
 
-type IProfileType = "local" | "remote" | "merge" | "script";
-
 interface IClashInfo {
   // status: string;
   port?: number; // clash mixed port
@@ -102,7 +100,7 @@ interface IClashInfo {
 
 interface IProfileItem {
   uid: string;
-  type?: IProfileType | string;
+  type?: "local" | "remote" | "merge" | "script";
   name?: string;
   desc?: string;
   file?: string;
-- 
GitLab