Przeglądaj źródła

feat: optimize profile page

GyDi 2 lat temu
rodzic
commit
33ce235713

+ 11 - 2
src/components/profile/enhanced.tsx

@@ -21,12 +21,16 @@ const EnhancedMode = (props: Props) => {
   const { items, chain } = props;
 
   const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
-  const { data: chainLogs = {} } = useSWR("getRuntimeLogs", getRuntimeLogs);
+  const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
+    "getRuntimeLogs",
+    getRuntimeLogs
+  );
 
   // handler
   const onEnhance = useLockFn(async () => {
     try {
       await enhanceProfiles();
+      mutateLogs();
       Notice.success("Refresh clash config", 1000);
     } catch (err: any) {
       Notice.error(err.message || err.toString());
@@ -39,6 +43,7 @@ const EnhancedMode = (props: Props) => {
     const newChain = [...chain, uid];
     await changeProfileChain(newChain);
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
+    mutateLogs();
   });
 
   const onEnhanceDisable = useLockFn(async (uid: string) => {
@@ -47,6 +52,7 @@ const EnhancedMode = (props: Props) => {
     const newChain = chain.filter((i) => i !== uid);
     await changeProfileChain(newChain);
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
+    mutateLogs();
   });
 
   const onEnhanceDelete = useLockFn(async (uid: string) => {
@@ -54,6 +60,7 @@ const EnhancedMode = (props: Props) => {
       await onEnhanceDisable(uid);
       await deleteProfile(uid);
       mutateProfiles();
+      mutateLogs();
     } catch (err: any) {
       Notice.error(err?.message || err.toString());
     }
@@ -65,6 +72,7 @@ const EnhancedMode = (props: Props) => {
     const newChain = [uid].concat(chain.filter((i) => i !== uid));
     await changeProfileChain(newChain);
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
+    mutateLogs();
   });
 
   const onMoveEnd = useLockFn(async (uid: string) => {
@@ -73,10 +81,11 @@ const EnhancedMode = (props: Props) => {
     const newChain = chain.filter((i) => i !== uid).concat([uid]);
     await changeProfileChain(newChain);
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
+    mutateLogs();
   });
 
   return (
-    <Box sx={{ mt: 4 }}>
+    <Box sx={{ mt: 2 }}>
       <Stack
         spacing={1}
         direction="row"

+ 3 - 1
src/components/profile/file-editor.tsx

@@ -82,7 +82,9 @@ const FileEditor = (props: Props) => {
       </DialogContent>
 
       <DialogActions>
-        <Button onClick={onClose}>{t("Cancel")}</Button>
+        <Button onClick={onClose} variant="outlined">
+          {t("Cancel")}
+        </Button>
         <Button onClick={onSave} variant="contained">
           {t("Save")}
         </Button>

+ 3 - 1
src/components/profile/file-input.tsx

@@ -1,5 +1,6 @@
 import { useRef, useState } from "react";
 import { useLockFn } from "ahooks";
+import { useTranslation } from "react-i18next";
 import { Box, Button, Typography } from "@mui/material";
 
 interface Props {
@@ -9,6 +10,7 @@ interface Props {
 const FileInput = (props: Props) => {
   const { onChange } = props;
 
+  const { t } = useTranslation();
   // file input
   const inputRef = useRef<any>();
   const [loading, setLoading] = useState(false);
@@ -40,7 +42,7 @@ const FileInput = (props: Props) => {
         sx={{ flex: "none" }}
         onClick={() => inputRef.current?.click()}
       >
-        Choose File
+        {t("Choose File")}
       </Button>
 
       <input

+ 8 - 6
src/components/profile/profile-edit.tsx → src/components/profile/info-editor.tsx

@@ -23,7 +23,7 @@ interface Props {
 
 // edit the profile item
 // remote / local file / merge / script
-const ProfileEdit = (props: Props) => {
+const InfoEditor = (props: Props) => {
   const { open, itemData, onClose } = props;
 
   const { t } = useTranslation();
@@ -56,7 +56,6 @@ const ProfileEdit = (props: Props) => {
       }
 
       await patchProfile(uid, { uid, name, desc, url, option: option_ });
-      setShowOpt(false);
       mutate("getProfiles");
       onClose();
     } catch (err: any) {
@@ -133,7 +132,7 @@ const ProfileEdit = (props: Props) => {
             value={option.update_interval}
             onChange={(e) => {
               const str = e.target.value?.replace(/\D/, "");
-              setOption({ update_interval: str != null ? +str : str });
+              setOption({ update_interval: !!str ? +str : undefined });
             }}
             onKeyDown={(e) => e.key === "Enter" && onUpdate()}
           />
@@ -144,6 +143,7 @@ const ProfileEdit = (props: Props) => {
         {form.type === "remote" && (
           <IconButton
             size="small"
+            color="inherit"
             sx={{ position: "absolute", left: 18 }}
             onClick={() => setShowOpt((o) => !o)}
           >
@@ -151,13 +151,15 @@ const ProfileEdit = (props: Props) => {
           </IconButton>
         )}
 
-        <Button onClick={onClose}>Cancel</Button>
+        <Button onClick={onClose} variant="outlined">
+          {t("Cancel")}
+        </Button>
         <Button onClick={onUpdate} variant="contained">
-          Update
+          {t("Save")}
         </Button>
       </DialogActions>
     </Dialog>
   );
 };
 
-export default ProfileEdit;
+export default InfoEditor;

+ 71 - 0
src/components/profile/log-viewer.tsx

@@ -0,0 +1,71 @@
+import { useTranslation } from "react-i18next";
+import {
+  Button,
+  Chip,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  Divider,
+  Typography,
+} from "@mui/material";
+import BaseEmpty from "../base/base-empty";
+import { Fragment } from "react";
+
+interface Props {
+  open: boolean;
+  logInfo: [string, string][];
+  onClose: () => void;
+}
+
+const LogViewer = (props: Props) => {
+  const { open, logInfo, onClose } = props;
+
+  const { t } = useTranslation();
+
+  return (
+    <Dialog open={open} onClose={onClose}>
+      <DialogTitle>{t("Script Console")}</DialogTitle>
+
+      <DialogContent
+        sx={{
+          width: 400,
+          height: 300,
+          overflowX: "hidden",
+          userSelect: "text",
+          pb: 1,
+        }}
+      >
+        {logInfo.map(([level, log], index) => (
+          <Fragment key={index.toString()}>
+            <Typography color="text.secondary" component="div">
+              <Chip
+                label={level}
+                size="small"
+                variant="outlined"
+                color={
+                  level === "error" || level === "exception"
+                    ? "error"
+                    : "default"
+                }
+                sx={{ mr: 1 }}
+              />
+              {log}
+            </Typography>
+            <Divider sx={{ my: 0.5 }} />
+          </Fragment>
+        ))}
+
+        {logInfo.length === 0 && <BaseEmpty />}
+      </DialogContent>
+
+      <DialogActions>
+        <Button onClick={onClose} variant="outlined">
+          {t("Back")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+};
+
+export default LogViewer;

+ 43 - 0
src/components/profile/profile-box.tsx

@@ -0,0 +1,43 @@
+import { alpha, Box, styled } from "@mui/material";
+
+const ProfileBox = styled(Box)(({ theme, "aria-selected": selected }) => {
+  const { mode, primary, text, grey, background } = theme.palette;
+  const key = `${mode}-${!!selected}`;
+
+  const backgroundColor = {
+    "light-true": alpha(primary.main, 0.2),
+    "light-false": alpha(background.paper, 0.75),
+    "dark-true": alpha(primary.main, 0.45),
+    "dark-false": alpha(grey[700], 0.45),
+  }[key]!;
+
+  const color = {
+    "light-true": text.secondary,
+    "light-false": text.secondary,
+    "dark-true": alpha(text.secondary, 0.85),
+    "dark-false": alpha(text.secondary, 0.65),
+  }[key]!;
+
+  const h2color = {
+    "light-true": primary.main,
+    "light-false": text.primary,
+    "dark-true": primary.light,
+    "dark-false": text.primary,
+  }[key]!;
+
+  return {
+    width: "100%",
+    display: "block",
+    cursor: "pointer",
+    textAlign: "left",
+    borderRadius: theme.shape.borderRadius,
+    boxShadow: theme.shadows[2],
+    padding: "8px 16px",
+    boxSizing: "border-box",
+    backgroundColor,
+    color,
+    "& h2": { color: h2color },
+  };
+});
+
+export default ProfileBox;

+ 56 - 98
src/components/profile/profile-item.tsx

@@ -1,13 +1,11 @@
 import dayjs from "dayjs";
+import { mutate } from "swr";
 import { useEffect, useState } from "react";
 import { useLockFn } from "ahooks";
-import { useSWRConfig } from "swr";
 import { useRecoilState } from "recoil";
 import { useTranslation } from "react-i18next";
 import {
-  alpha,
   Box,
-  styled,
   Typography,
   LinearProgress,
   IconButton,
@@ -19,21 +17,11 @@ import { RefreshRounded } from "@mui/icons-material";
 import { atomLoadingCache } from "@/services/states";
 import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
 import parseTraffic from "@/utils/parse-traffic";
-import ProfileEdit from "./profile-edit";
+import ProfileBox from "./profile-box";
+import InfoEditor from "./info-editor";
 import FileEditor from "./file-editor";
 import Notice from "../base/base-notice";
 
-const Wrapper = styled(Box)(({ theme }) => ({
-  width: "100%",
-  display: "block",
-  cursor: "pointer",
-  textAlign: "left",
-  borderRadius: theme.shape.borderRadius,
-  boxShadow: theme.shadows[2],
-  padding: "8px 16px",
-  boxSizing: "border-box",
-}));
-
 const round = keyframes`
   from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
@@ -49,7 +37,6 @@ const ProfileItem = (props: Props) => {
   const { selected, itemData, onSelect } = props;
 
   const { t } = useTranslation();
-  const { mutate } = useSWRConfig();
   const [anchorEl, setAnchorEl] = useState<any>(null);
   const [position, setPosition] = useState({ left: 0, top: 0 });
   const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
@@ -58,7 +45,6 @@ const ProfileItem = (props: Props) => {
 
   // local file mode
   // remote file mode
-  // subscription url mode
   const hasUrl = !!itemData.url;
   const hasExtra = !!extra; // only subscription url has extra info
 
@@ -79,7 +65,6 @@ const ProfileItem = (props: Props) => {
     const handler = () => {
       const now = Date.now();
       const lastUpdate = updated * 1000;
-
       // 大于一天的不管
       if (now - lastUpdate >= 24 * 36e5) return;
 
@@ -152,13 +137,6 @@ const ProfileItem = (props: Props) => {
     }
   });
 
-  const boxStyle = {
-    height: 26,
-    display: "flex",
-    alignItems: "center",
-    justifyContent: "space-between",
-  };
-
   const urlModeMenu = [
     { label: "Select", handler: onForceSelect },
     { label: "Edit Info", handler: onEditInfo },
@@ -176,36 +154,17 @@ const ProfileItem = (props: Props) => {
     { label: "Delete", handler: onDelete },
   ];
 
+  const boxStyle = {
+    height: 26,
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "space-between",
+  };
+
   return (
     <>
-      <Wrapper
-        sx={({ palette }) => {
-          const { mode, primary, text, grey } = palette;
-          const key = `${mode}-${selected}`;
-
-          const bgcolor = {
-            "light-true": alpha(primary.main, 0.15),
-            "light-false": palette.background.paper,
-            "dark-true": alpha(primary.main, 0.35),
-            "dark-false": alpha(grey[700], 0.35),
-          }[key]!;
-
-          const color = {
-            "light-true": text.secondary,
-            "light-false": text.secondary,
-            "dark-true": alpha(text.secondary, 0.75),
-            "dark-false": alpha(text.secondary, 0.75),
-          }[key]!;
-
-          const h2color = {
-            "light-true": primary.main,
-            "light-false": text.primary,
-            "dark-true": primary.light,
-            "dark-false": text.primary,
-          }[key]!;
-
-          return { bgcolor, color, "& h2": { color: h2color } };
-        }}
+      <ProfileBox
+        aria-selected={selected}
         onClick={() => onSelect(false)}
         onContextMenu={(event) => {
           const { clientX, clientY } = event;
@@ -214,9 +173,9 @@ const ProfileItem = (props: Props) => {
           event.preventDefault();
         }}
       >
-        <Box display="flex" justifyContent="space-between">
+        <Box position="relative">
           <Typography
-            width="calc(100% - 40px)"
+            width="calc(100% - 36px)"
             variant="h6"
             component="h2"
             noWrap
@@ -229,10 +188,13 @@ const ProfileItem = (props: Props) => {
           {hasUrl && (
             <IconButton
               sx={{
-                width: 26,
-                height: 26,
+                position: "absolute",
+                p: "3px",
+                top: -1,
+                right: -5,
                 animation: loading ? `1s linear infinite ${round}` : "none",
               }}
+              size="small"
               color="inherit"
               disabled={loading}
               onClick={(e) => {
@@ -240,47 +202,47 @@ const ProfileItem = (props: Props) => {
                 onUpdate(false);
               }}
             >
-              <RefreshRounded />
+              <RefreshRounded color="inherit" />
             </IconButton>
           )}
         </Box>
 
         {/* the second line show url's info or description */}
-        {hasUrl ? (
-          <Box sx={boxStyle}>
-            <Typography noWrap title={`From: ${from}`}>
-              {from}
-            </Typography>
-
-            <Typography
-              noWrap
-              flex="1 0 auto"
-              fontSize={14}
-              textAlign="right"
-              title="updated time"
-            >
-              {updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
-            </Typography>
-          </Box>
-        ) : (
-          <Box sx={boxStyle}>
+        <Box sx={boxStyle}>
+          {hasUrl ? (
+            <>
+              <Typography noWrap title={`From: ${from}`}>
+                {from}
+              </Typography>
+
+              <Typography
+                noWrap
+                flex="1 0 auto"
+                fontSize={14}
+                textAlign="right"
+                title={`Updated Time: ${parseExpire(updated)}`}
+              >
+                {updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
+              </Typography>
+            </>
+          ) : (
             <Typography noWrap title={itemData.desc}>
               {itemData.desc}
             </Typography>
-          </Box>
-        )}
+          )}
+        </Box>
 
         {/* the third line show extra info or last updated time */}
         {hasExtra ? (
           <Box sx={{ ...boxStyle, fontSize: 14 }}>
-            <span title="used / total">
+            <span title="Used / Total">
               {parseTraffic(upload + download)} / {parseTraffic(total)}
             </span>
-            <span title="expire time">{expire}</span>
+            <span title="Expire Time">{expire}</span>
           </Box>
         ) : (
           <Box sx={{ ...boxStyle, fontSize: 14, justifyContent: "flex-end" }}>
-            <span title="updated time">{parseExpire(updated)}</span>
+            <span title="Updated Time">{parseExpire(updated)}</span>
           </Box>
         )}
 
@@ -289,7 +251,7 @@ const ProfileItem = (props: Props) => {
           value={progress}
           color="inherit"
         />
-      </Wrapper>
+      </ProfileBox>
 
       <Menu
         open={!!anchorEl}
@@ -314,22 +276,18 @@ const ProfileItem = (props: Props) => {
         ))}
       </Menu>
 
-      {editOpen && (
-        <ProfileEdit
-          open={editOpen}
-          itemData={itemData}
-          onClose={() => setEditOpen(false)}
-        />
-      )}
-
-      {fileOpen && (
-        <FileEditor
-          uid={uid}
-          open={fileOpen}
-          mode="yaml"
-          onClose={() => setFileOpen(false)}
-        />
-      )}
+      <InfoEditor
+        open={editOpen}
+        itemData={itemData}
+        onClose={() => setEditOpen(false)}
+      />
+
+      <FileEditor
+        uid={uid}
+        open={fileOpen}
+        mode="yaml"
+        onClose={() => setFileOpen(false)}
+      />
     </>
   );
 };

+ 63 - 86
src/components/profile/profile-more.tsx

@@ -1,32 +1,24 @@
 import dayjs from "dayjs";
-import { useEffect, useState } from "react";
+import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useLockFn } from "ahooks";
 import {
-  alpha,
   Box,
+  Badge,
   Chip,
-  styled,
   Typography,
   MenuItem,
   Menu,
+  IconButton,
 } from "@mui/material";
+import { FeaturedPlayListRounded } from "@mui/icons-material";
 import { viewProfile } from "@/services/cmds";
-import ProfileEdit from "./profile-edit";
+import InfoEditor from "./info-editor";
 import FileEditor from "./file-editor";
+import ProfileBox from "./profile-box";
+import LogViewer from "./log-viewer";
 import Notice from "../base/base-notice";
 
-const Wrapper = styled(Box)(({ theme }) => ({
-  width: "100%",
-  display: "block",
-  cursor: "pointer",
-  textAlign: "left",
-  borderRadius: theme.shape.borderRadius,
-  boxShadow: theme.shadows[2],
-  padding: "8px 16px",
-  boxSizing: "border-box",
-}));
-
 interface Props {
   selected: boolean;
   itemData: CmdType.ProfileItem;
@@ -55,18 +47,11 @@ const ProfileMore = (props: Props) => {
 
   const { uid, type } = itemData;
   const { t } = 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 [status, setStatus] = useState(enhance.status(uid));
-
-  // unlisten when unmount
-  // useEffect(() => enhance.listen(uid, setStatus), [uid]);
-
-  // error during enhanced mode
-  const hasError = !!logInfo.find((e) => e[0] === "exception"); // selected && status?.status === "error";
+  const [logOpen, setLogOpen] = useState(false);
 
   const onEditInfo = () => {
     setAnchorEl(null);
@@ -92,6 +77,7 @@ const ProfileMore = (props: Props) => {
     return fn();
   };
 
+  const hasError = !!logInfo.find((e) => e[0] === "exception");
   const showMove = enableNum > 1 && !hasError;
 
   const enableMenu = [
@@ -122,39 +108,8 @@ const ProfileMore = (props: Props) => {
 
   return (
     <>
-      <Wrapper
-        sx={({ palette }) => {
-          // todo
-          // 区分 selected 和 error 和 mode 下各种颜色的排列组合
-          const { mode, primary, text, grey, error } = palette;
-          const key = `${mode}-${selected}`;
-          const bgkey = hasError ? `${mode}-err` : key;
-
-          const bgcolor = {
-            "light-true": alpha(primary.main, 0.15),
-            "light-false": palette.background.paper,
-            "dark-true": alpha(primary.main, 0.35),
-            "dark-false": alpha(grey[700], 0.35),
-            "light-err": alpha(error.main, 0.12),
-            "dark-err": alpha(error.main, 0.3),
-          }[bgkey]!;
-
-          const color = {
-            "light-true": text.secondary,
-            "light-false": text.secondary,
-            "dark-true": alpha(text.secondary, 0.6),
-            "dark-false": alpha(text.secondary, 0.6),
-          }[key]!;
-
-          const h2color = {
-            "light-true": primary.main,
-            "light-false": text.primary,
-            "dark-true": primary.light,
-            "dark-false": text.primary,
-          }[key]!;
-
-          return { bgcolor, color, "& h2": { color: h2color } };
-        }}
+      <ProfileBox
+        aria-selected={selected}
         // onClick={() => onSelect(false)}
         onContextMenu={(event) => {
           const { clientX, clientY } = event;
@@ -163,7 +118,12 @@ const ProfileMore = (props: Props) => {
           event.preventDefault();
         }}
       >
-        <Box display="flex" justifyContent="space-between" alignItems="center">
+        <Box
+          display="flex"
+          justifyContent="space-between"
+          alignItems="center"
+          mb={0.5}
+        >
           <Typography
             width="calc(100% - 52px)"
             variant="h6"
@@ -179,22 +139,33 @@ const ProfileMore = (props: Props) => {
             color="primary"
             size="small"
             variant="outlined"
-            sx={{ textTransform: "capitalize" }}
+            sx={{ height: 20, textTransform: "capitalize" }}
           />
         </Box>
 
         <Box sx={boxStyle}>
-          {hasError ? (
-            <Typography
-              noWrap
-              color="error"
-              sx={{ width: "calc(100% - 75px)" }}
-              // title={status.message}
-              title="error"
-            >
-              {/* {status.message} */}
-              error
-            </Typography>
+          {selected ? (
+            hasError ? (
+              <Badge color="primary" variant="dot" overlap="circular">
+                <IconButton
+                  size="small"
+                  edge="start"
+                  color="error"
+                  onClick={() => setLogOpen(true)}
+                >
+                  <FeaturedPlayListRounded fontSize="inherit" />
+                </IconButton>
+              </Badge>
+            ) : (
+              <IconButton
+                size="small"
+                edge="start"
+                color="inherit"
+                onClick={() => setLogOpen(true)}
+              >
+                <FeaturedPlayListRounded fontSize="inherit" />
+              </IconButton>
+            )
           ) : (
             <Typography
               noWrap
@@ -207,13 +178,15 @@ const ProfileMore = (props: Props) => {
 
           <Typography
             component="span"
-            title="updated time"
+            title={`Updated Time: ${parseExpire(itemData.updated)}`}
             style={{ fontSize: 14 }}
           >
-            {parseExpire(itemData.updated)}
+            {!!itemData.updated
+              ? dayjs(itemData.updated! * 1000).fromNow()
+              : ""}
           </Typography>
         </Box>
-      </Wrapper>
+      </ProfileBox>
 
       <Menu
         open={!!anchorEl}
@@ -240,20 +213,24 @@ const ProfileMore = (props: Props) => {
           ))}
       </Menu>
 
-      {editOpen && (
-        <ProfileEdit
-          open={editOpen}
-          itemData={itemData}
-          onClose={() => setEditOpen(false)}
-        />
-      )}
-
-      {fileOpen && (
-        <FileEditor
-          uid={uid}
-          open={fileOpen}
-          mode={type === "merge" ? "yaml" : "javascript"}
-          onClose={() => setFileOpen(false)}
+      <InfoEditor
+        open={editOpen}
+        itemData={itemData}
+        onClose={() => setEditOpen(false)}
+      />
+
+      <FileEditor
+        uid={uid}
+        open={fileOpen}
+        mode={type === "merge" ? "yaml" : "javascript"}
+        onClose={() => setFileOpen(false)}
+      />
+
+      {selected && (
+        <LogViewer
+          open={logOpen}
+          logInfo={logInfo}
+          onClose={() => setLogOpen(false)}
         />
       )}
     </>

+ 10 - 6
src/components/profile/profile-new.tsx

@@ -1,5 +1,6 @@
 import { useRef, useState } from "react";
-import { useSWRConfig } from "swr";
+import { mutate } from "swr";
+import { useTranslation } from "react-i18next";
 import { useLockFn, useSetState } from "ahooks";
 import {
   Button,
@@ -29,7 +30,7 @@ interface Props {
 const ProfileNew = (props: Props) => {
   const { open, onClose } = props;
 
-  const { mutate } = useSWRConfig();
+  const { t } = useTranslation();
   const [form, setForm] = useSetState({
     type: "remote",
     name: "",
@@ -83,7 +84,7 @@ const ProfileNew = (props: Props) => {
 
   return (
     <Dialog open={open} onClose={onClose}>
-      <DialogTitle sx={{ pb: 0.5 }}>Create Profile</DialogTitle>
+      <DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
 
       <DialogContent sx={{ width: 336, pb: 1 }}>
         <FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
@@ -120,7 +121,7 @@ const ProfileNew = (props: Props) => {
         {form.type === "remote" && (
           <TextField
             {...textFieldProps}
-            label="Subscription Url"
+            label="Subscription URL"
             autoComplete="off"
             value={form.url}
             onChange={(e) => setForm({ url: e.target.value })}
@@ -146,6 +147,7 @@ const ProfileNew = (props: Props) => {
         {form.type === "remote" && (
           <IconButton
             size="small"
+            color="inherit"
             sx={{ position: "absolute", left: 18 }}
             onClick={() => setShowOpt((o) => !o)}
           >
@@ -153,9 +155,11 @@ const ProfileNew = (props: Props) => {
           </IconButton>
         )}
 
-        <Button onClick={onClose}>Cancel</Button>
+        <Button onClick={onClose} variant="outlined">
+          {t("Cancel")}
+        </Button>
         <Button onClick={onCreate} variant="contained">
-          Create
+          {t("Save")}
         </Button>
       </DialogActions>
     </Dialog>

+ 2 - 0
src/locales/en.json

@@ -18,6 +18,8 @@
   "Profile URL": "Profile URL",
   "Import": "Import",
   "New": "New",
+  "Create Profile": "Create Profile",
+  "Choose File": "Choose File",
   "Close All": "Close All",
   "Select": "Select",
   "Edit Info": "Edit Info",

+ 2 - 0
src/locales/zh.json

@@ -18,6 +18,8 @@
   "Profile URL": "配置文件链接",
   "Import": "导入",
   "New": "新建",
+  "Create Profile": "新建配置",
+  "Choose File": "选择文件",
   "Close All": "关闭全部",
   "Select": "使用",
   "Edit Info": "编辑信息",

+ 22 - 14
src/pages/profiles.tsx

@@ -1,8 +1,8 @@
-import useSWR, { useSWRConfig } from "swr";
+import useSWR, { mutate } from "swr";
 import { useLockFn } from "ahooks";
 import { useEffect, useMemo, useState } from "react";
 import { useSetRecoilState } from "recoil";
-import { Box, Button, Grid, TextField } from "@mui/material";
+import { Button, Grid, Stack, TextField } from "@mui/material";
 import { useTranslation } from "react-i18next";
 import {
   getProfiles,
@@ -20,7 +20,6 @@ import EnhancedMode from "@/components/profile/enhanced";
 
 const ProfilePage = () => {
   const { t } = useTranslation();
-  const { mutate } = useSWRConfig();
 
   const [url, setUrl] = useState("");
   const [disabled, setDisabled] = useState(false);
@@ -110,10 +109,13 @@ const ProfilePage = () => {
       getProfiles().then((newProfiles) => {
         mutate("getProfiles", newProfiles);
 
-        if (!newProfiles.current && newProfiles.items?.length) {
-          const current = newProfiles.items[0].uid;
+        const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
+
+        if (!newProfiles.current && remoteItem) {
+          const current = remoteItem.uid;
           selectProfile(current);
           mutate("getProfiles", { ...newProfiles, current }, true);
+          mutate("getRuntimeLogs");
         }
       });
     } catch {
@@ -130,6 +132,7 @@ const ProfilePage = () => {
       await selectProfile(uid);
       setCurrentProfile(uid);
       mutate("getProfiles", { ...profiles, current: uid }, true);
+      mutate("getRuntimeLogs");
       if (force) Notice.success("Refresh clash config", 1000);
     } catch (err: any) {
       Notice.error(err?.message || err.toString());
@@ -138,29 +141,34 @@ const ProfilePage = () => {
 
   return (
     <BasePage title={t("Profiles")}>
-      <Box sx={{ display: "flex", mb: 2.5 }}>
+      <Stack direction="row" spacing={1} sx={{ mb: 2 }}>
         <TextField
-          id="clas_verge_profile_url"
-          name="profile_url"
-          label={t("Profile URL")}
-          size="small"
+          hiddenLabel
           fullWidth
+          size="small"
           value={url}
+          variant="outlined"
+          autoComplete="off"
           onChange={(e) => setUrl(e.target.value)}
-          sx={{ mr: 1 }}
+          sx={{ input: { py: 0.65, px: 1.25 } }}
+          placeholder={t("Profile URL")}
         />
         <Button
           disabled={!url || disabled}
           variant="contained"
+          size="small"
           onClick={onImport}
-          sx={{ mr: 1 }}
         >
           {t("Import")}
         </Button>
-        <Button variant="contained" onClick={() => setDialogOpen(true)}>
+        <Button
+          variant="contained"
+          size="small"
+          onClick={() => setDialogOpen(true)}
+        >
           {t("New")}
         </Button>
-      </Box>
+      </Stack>
 
       <Grid container spacing={2}>
         {regularItems.map((item) => (