Forráskód Böngészése

feat: reactive after save when profile content changes

dongchengjie 11 hónapja
szülő
commit
9ee5390ec7

+ 2 - 2
src/components/profile/confirm-viewer.tsx

@@ -27,10 +27,10 @@ export const ConfirmViewer = (props: Props) => {
 
   return (
     <Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
-      <DialogTitle>{t(title)}</DialogTitle>
+      <DialogTitle>{title}</DialogTitle>
 
       <DialogContent sx={{ pb: 1, userSelect: "text" }}>
-        {t(message)}
+        {message}
       </DialogContent>
 
       <DialogActions>

+ 8 - 5
src/components/profile/editor-viewer.tsx

@@ -32,7 +32,7 @@ interface Props {
   language: "yaml" | "javascript" | "css";
   schema?: "clash" | "merge";
   onClose: () => void;
-  onChange?: (content?: string) => void;
+  onChange?: (prev?: string, curr?: string) => void;
 }
 
 // yaml worker
@@ -90,6 +90,7 @@ export const EditorViewer = (props: Props) => {
   const editorRef = useRef<any>();
   const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
   const themeMode = useThemeMode();
+  const prevData = useRef<string>();
 
   useEffect(() => {
     if (!open) return;
@@ -136,6 +137,8 @@ export const EditorViewer = (props: Props) => {
         fontLigatures: true, // 连字符
         smoothScrolling: true, // 平滑滚动
       });
+
+      prevData.current = data;
     });
 
     return () => {
@@ -147,15 +150,15 @@ export const EditorViewer = (props: Props) => {
   }, [open]);
 
   const onSave = useLockFn(async () => {
-    const value = instanceRef.current?.getValue();
+    const currData = instanceRef.current?.getValue();
 
-    if (value == null) return;
+    if (currData == null) return;
 
     try {
       if (mode === "profile") {
-        await saveProfileFile(property, value);
+        await saveProfileFile(property, currData);
       }
-      onChange?.(value);
+      onChange?.(prevData.current, currData);
       onClose();
     } catch (err: any) {
       Notice.error(err.message || err.toString());

+ 27 - 21
src/components/profile/profile-item.tsx

@@ -17,7 +17,7 @@ import {
 } from "@mui/material";
 import { RefreshRounded, DragIndicator } from "@mui/icons-material";
 import { useLoadingCache, useSetLoadingCache } from "@/services/states";
-import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
+import { updateProfile, viewProfile } from "@/services/cmds";
 import { Notice } from "@/components/base";
 import { EditorViewer } from "@/components/profile/editor-viewer";
 import { ProfileBox } from "./profile-box";
@@ -36,10 +36,20 @@ interface Props {
   itemData: IProfileItem;
   onSelect: (force: boolean) => void;
   onEdit: () => void;
+  onChange?: (prev?: string, curr?: string) => void;
+  onDelete: () => void;
 }
 
 export const ProfileItem = (props: Props) => {
-  const { selected, activating, itemData, onSelect, onEdit } = props;
+  const {
+    selected,
+    activating,
+    itemData,
+    onSelect,
+    onEdit,
+    onChange,
+    onDelete,
+  } = props;
   const { attributes, listeners, setNodeRef, transform, transition } =
     useSortable({ id: props.id });
 
@@ -53,6 +63,7 @@ export const ProfileItem = (props: Props) => {
 
   // local file mode
   // remote file mode
+  // remote file mode
   const hasUrl = !!itemData.url;
   const hasExtra = !!extra; // only subscription url has extra info
   const hasHome = !!itemData.home; // only subscription url has home page
@@ -162,16 +173,6 @@ export const ProfileItem = (props: Props) => {
     }
   });
 
-  const onDelete = useLockFn(async () => {
-    setAnchorEl(null);
-    try {
-      await deleteProfile(itemData.uid);
-      mutate("getProfiles");
-    } catch (err: any) {
-      Notice.error(err?.message || err.toString());
-    }
-  });
-
   const urlModeMenu = (
     hasHome ? [{ label: "Home", handler: onOpenHome }] : []
   ).concat([
@@ -242,7 +243,7 @@ export const ProfileItem = (props: Props) => {
               backdropFilter: "blur(2px)",
             }}
           >
-            <CircularProgress size={20} />
+            <CircularProgress color="inherit" size={20} />
           </Box>
         )}
         <Box position="relative">
@@ -312,7 +313,7 @@ export const ProfileItem = (props: Props) => {
                 </Typography>
               ) : (
                 hasUrl && (
-                  <Typography noWrap title={`From ${from}`}>
+                  <Typography noWrap title={`${t("From")} ${from}`}>
                     {from}
                   </Typography>
                 )
@@ -323,7 +324,7 @@ export const ProfileItem = (props: Props) => {
                   flex="1 0 auto"
                   fontSize={14}
                   textAlign="right"
-                  title={`Updated Time: ${parseExpire(updated)}`}
+                  title={`${t("Update Time")}: ${parseExpire(updated)}`}
                 >
                   {updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
                 </Typography>
@@ -334,17 +335,21 @@ export const ProfileItem = (props: Props) => {
         {/* the third line show extra info or last updated time */}
         {hasExtra ? (
           <Box sx={{ ...boxStyle, fontSize: 14 }}>
-            <span title="Used / Total">
+            <span title={t("Used / Total")}>
               {parseTraffic(upload + download)} / {parseTraffic(total)}
             </span>
-            <span title="Expire Time">{expire}</span>
+            <span title={t("Expire Time")}>{expire}</span>
           </Box>
         ) : (
           <Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
-            <span title="Updated Time">{parseExpire(updated)}</span>
+            <span title={t("Update Time")}>{parseExpire(updated)}</span>
           </Box>
         )}
-        <LinearProgress variant="determinate" value={progress} />
+        <LinearProgress
+          variant="determinate"
+          value={progress}
+          style={{ opacity: progress > 0 ? 1 : 0 }}
+        />
       </ProfileBox>
 
       <Menu
@@ -390,11 +395,12 @@ export const ProfileItem = (props: Props) => {
         open={fileOpen}
         language="yaml"
         schema="clash"
+        onChange={onChange}
         onClose={() => setFileOpen(false)}
       />
       <ConfirmViewer
-        title="Confirm deletion"
-        message="This operation is not reversible"
+        title={t("Confirm deletion")}
+        message={t("This operation is not reversible")}
         open={confirmOpen}
         onClose={() => setConfirmOpen(false)}
         onConfirm={() => {

+ 27 - 3
src/components/profile/profile-more.tsx

@@ -9,6 +9,7 @@ import {
   MenuItem,
   Menu,
   IconButton,
+  CircularProgress,
 } from "@mui/material";
 import { FeaturedPlayListRounded } from "@mui/icons-material";
 import { viewProfile } from "@/services/cmds";
@@ -20,6 +21,7 @@ import { ConfirmViewer } from "./confirm-viewer";
 
 interface Props {
   selected: boolean;
+  activating: boolean;
   itemData: IProfileItem;
   enableNum: number;
   logInfo?: [string, string][];
@@ -27,14 +29,16 @@ interface Props {
   onDisable: () => void;
   onMoveTop: () => void;
   onMoveEnd: () => void;
-  onDelete: () => void;
   onEdit: () => void;
+  onChange?: (prev?: string, curr?: string) => void;
+  onDelete: () => void;
 }
 
 // profile enhanced item
 export const ProfileMore = (props: Props) => {
   const {
     selected,
+    activating,
     itemData,
     enableNum,
     logInfo = [],
@@ -44,6 +48,7 @@ export const ProfileMore = (props: Props) => {
     onMoveEnd,
     onDelete,
     onEdit,
+    onChange,
   } = props;
 
   const { uid, type } = itemData;
@@ -132,6 +137,24 @@ export const ProfileMore = (props: Props) => {
           event.preventDefault();
         }}
       >
+        {activating && (
+          <Box
+            sx={{
+              position: "absolute",
+              display: "flex",
+              justifyContent: "center",
+              alignItems: "center",
+              top: 10,
+              left: 10,
+              right: 10,
+              bottom: 2,
+              zIndex: 10,
+              backdropFilter: "blur(2px)",
+            }}
+          >
+            <CircularProgress color="inherit" size={20} />
+          </Box>
+        )}
         <Box
           display="flex"
           justifyContent="space-between"
@@ -237,11 +260,12 @@ export const ProfileMore = (props: Props) => {
         open={fileOpen}
         language={type === "merge" ? "yaml" : "javascript"}
         schema={type === "merge" ? "merge" : undefined}
+        onChange={onChange}
         onClose={() => setFileOpen(false)}
       />
       <ConfirmViewer
-        title="Confirm deletion"
-        message="This operation is not reversible"
+        title={t("Confirm deletion")}
+        message={t("This operation is not reversible")}
         open={confirmOpen}
         onClose={() => setConfirmOpen(false)}
         onConfirm={() => {

+ 3 - 3
src/components/setting/mods/sysproxy-viewer.tsx

@@ -249,10 +249,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
                 property={value.pac_content ?? ""}
                 open={editorOpen}
                 language="javascript"
-                onChange={(content) => {
+                onChange={(_prev, curr) => {
                   let pac = DEFAULT_PAC;
-                  if (content && content.trim().length > 0) {
-                    pac = content;
+                  if (curr && curr.trim().length > 0) {
+                    pac = curr;
                   }
                   setValue((v) => ({ ...v, pac_content: pac }));
                 }}

+ 2 - 2
src/components/setting/mods/theme-viewer.tsx

@@ -129,8 +129,8 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
             property={theme.css_injection ?? ""}
             open={editorOpen}
             language="css"
-            onChange={(content) => {
-              theme.css_injection = content;
+            onChange={(_prev, curr) => {
+              theme.css_injection = curr;
               handleChange("css_injection");
             }}
             onClose={() => {

+ 7 - 3
src/locales/en.json

@@ -49,6 +49,10 @@
   "Paste": "Paste",
   "Profile URL": "Profile URL",
   "Import": "Import",
+  "From": "From",
+  "Update Time": "Update Time",
+  "Used / Total": "Used / Total",
+  "Expire Time": "Expire Time",
   "Create Profile": "Create Profile",
   "Edit Profile": "Edit Profile",
   "Type": "Type",
@@ -178,6 +182,9 @@
   "Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
   "Update GeoData": "Update GeoData",
 
+  "TG Channel": "Telegram Channel",
+  "Manual": "Manual",
+  "Github Repo": "Github Repo",
   "Verge Setting": "Verge Setting",
   "Language": "Language",
   "Theme Mode": "Theme Mode",
@@ -246,9 +253,6 @@
   "Open Dev Tools": "Open Dev Tools",
   "Exit": "Exit",
   "Verge Version": "Verge Version",
-  "TG Channel": "Telegram Channel",
-  "Doc": "Docs",
-  "Source Code": "Source Code",
 
   "ReadOnly": "ReadOnly",
   "ReadOnlyMessage": "Cannot edit in read-only editor",

+ 7 - 3
src/locales/fa.json

@@ -49,6 +49,10 @@
   "Paste": "چسباندن",
   "Profile URL": "آدرس پروفایل",
   "Import": "وارد کردن",
+  "From": "از",
+  "Update Time": "زمان به‌روزرسانی",
+  "Used / Total": "استفاده‌شده / کل",
+  "Expire Time": "زمان انقضا",
   "Create Profile": "ایجاد پروفایل",
   "Edit Profile": "ویرایش پروفایل",
   "Type": "نوع",
@@ -183,6 +187,9 @@
   "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
   "Update GeoData": "به‌روزرسانی GeoData",
 
+  "TG Channel": "کانال تلگرام",
+  "Manual": "راهنما",
+  "Github Repo": "مخزن GitHub",
   "Verge Setting": "تنظیمات Verge",
   "Language": "زبان",
   "Theme Mode": "حالت تم",
@@ -251,9 +258,6 @@
   "Open Dev Tools": "باز کردن ابزارهای توسعه‌دهنده",
   "Exit": "خروج",
   "Verge Version": "نسخه Verge",
-  "TG Channel": "کانال تلگرام",
-  "Doc": "سند",
-  "Source Code": "کد منبع",
 
   "ReadOnly": "فقط خواندنی",
   "ReadOnlyMessage": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد",

+ 7 - 3
src/locales/ru.json

@@ -49,6 +49,10 @@
   "Paste": "Вставить",
   "Profile URL": "URL профиля",
   "Import": "Импорт",
+  "From": "От",
+  "Update Time": "Время обновления",
+  "Used / Total": "Использовано / Всего",
+  "Expire Time": "Время окончания",
   "Create Profile": "Создать профиль",
   "Edit Profile": "Изменить профиль",
   "Type": "Тип",
@@ -183,6 +187,9 @@
   "Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
   "Update GeoData": "Обновление GeoData",
 
+  "TG Channel": "Канал Telegram",
+  "Manual": "Документация",
+  "Github Repo": "GitHub репозиторий",
   "Verge Setting": "Настройки Verge",
   "Language": "Язык",
   "Theme Mode": "Режим темы",
@@ -251,9 +258,6 @@
   "Open Dev Tools": "Открыть инструменты разработчика",
   "Exit": "Выход",
   "Verge Version": "Версия Verge",
-  "TG Channel": "Канал Telegram",
-  "Doc": "документ",
-  "Source Code": "Исходный код",
 
   "ReadOnly": "Только для чтения",
   "ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",

+ 8 - 4
src/locales/zh.json

@@ -49,6 +49,10 @@
   "Paste": "粘贴",
   "Profile URL": "订阅文件链接",
   "Import": "导入",
+  "From": "来自",
+  "Update Time": "更新时间",
+  "Used / Total": "已使用 / 总量",
+  "Expire Time": "到期时间",
   "Create Profile": "新建配置",
   "Edit Profile": "编辑配置",
   "Type": "类型",
@@ -154,6 +158,9 @@
   "Silent Start": "静默启动",
   "Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
 
+  "TG Channel": "Telegram 频道",
+  "Manual": "使用手册",
+  "Github Repo": "GitHub 项目地址",
   "Clash Setting": "Clash 设置",
   "Allow Lan": "局域网连接",
   "IPv6": "IPv6",
@@ -176,7 +183,7 @@
   "Upgrade": "升级内核",
   "Restart": "重启内核",
   "Release Version": "正式版",
-  "Alpha Version": "测版",
+  "Alpha Version": "测版",
   "Tun mode requires": "如需启用 Tun 模式需要授权",
   "Grant": "授权",
   "Open UWP tool": "UWP 工具",
@@ -251,9 +258,6 @@
   "Open Dev Tools": "打开开发者工具",
   "Exit": "退出",
   "Verge Version": "Verge 版本",
-  "TG Channel": "Telegram 频道",
-  "Doc": "文档",
-  "Source Code": "源代码",
 
   "ReadOnly": "只读",
   "ReadOnlyMessage": "无法在只读模式下编辑",

+ 48 - 12
src/pages/profiles.tsx

@@ -56,7 +56,7 @@ const ProfilePage = () => {
 
   const [url, setUrl] = useState("");
   const [disabled, setDisabled] = useState(false);
-  const [activating, setActivating] = useState("");
+  const [activatings, setActivatings] = useState<string[]>([]);
   const [loading, setLoading] = useState(false);
   const sensors = useSensors(
     useSensor(PointerSensor),
@@ -128,6 +128,10 @@ const ProfilePage = () => {
     return { regularItems, enhanceItems };
   }, [profiles]);
 
+  const currentActivatings = () => {
+    return [...new Set([profiles.current ?? "", ...chain])].filter(Boolean);
+  };
+
   const onImport = async () => {
     if (!url) return;
     setLoading(true);
@@ -138,13 +142,13 @@ const ProfilePage = () => {
       setUrl("");
       setLoading(false);
 
-      getProfiles().then((newProfiles) => {
+      getProfiles().then(async (newProfiles) => {
         mutate("getProfiles", newProfiles);
 
         const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
         if (!newProfiles.current && remoteItem) {
           const current = remoteItem.uid;
-          patchProfiles({ current });
+          await patchProfiles({ current });
           mutateLogs();
           setTimeout(() => activateSelected(), 2000);
         }
@@ -171,7 +175,9 @@ const ProfilePage = () => {
   const onSelect = useLockFn(async (current: string, force: boolean) => {
     if (!force && current === profiles.current) return;
     // 避免大多数情况下loading态闪烁
-    const reset = setTimeout(() => setActivating(current), 100);
+    const reset = setTimeout(() => {
+      setActivatings([...currentActivatings(), current]);
+    }, 100);
     try {
       await patchProfiles({ current });
       mutateLogs();
@@ -182,42 +188,64 @@ const ProfilePage = () => {
       Notice.error(err?.message || err.toString(), 4000);
     } finally {
       clearTimeout(reset);
-      setActivating("");
+      setActivatings([]);
     }
   });
 
   const onEnhance = useLockFn(async () => {
+    setActivatings(currentActivatings());
     try {
       await enhanceProfiles();
       mutateLogs();
       Notice.success(t("Profile Reactivated"), 1000);
     } catch (err: any) {
       Notice.error(err.message || err.toString(), 3000);
+    } finally {
+      setActivatings([]);
     }
   });
 
   const onEnable = useLockFn(async (uid: string) => {
     if (chain.includes(uid)) return;
-    const newChain = [...chain, uid];
-    await patchProfiles({ chain: newChain });
-    mutateLogs();
+    try {
+      setActivatings([...currentActivatings(), uid]);
+      const newChain = [...chain, uid];
+      await patchProfiles({ chain: newChain });
+      mutateLogs();
+    } catch (err: any) {
+      Notice.error(err.message || err.toString(), 3000);
+    } finally {
+      setActivatings([]);
+    }
   });
 
   const onDisable = useLockFn(async (uid: string) => {
     if (!chain.includes(uid)) return;
-    const newChain = chain.filter((i) => i !== uid);
-    await patchProfiles({ chain: newChain });
-    mutateLogs();
+    try {
+      setActivatings([...currentActivatings(), uid]);
+      const newChain = chain.filter((i) => i !== uid);
+      await patchProfiles({ chain: newChain });
+      mutateLogs();
+    } catch (err: any) {
+      Notice.error(err.message || err.toString(), 3000);
+    } finally {
+      setActivatings([]);
+    }
   });
 
   const onDelete = useLockFn(async (uid: string) => {
+    const current = profiles.current === uid;
     try {
       await onDisable(uid);
+      setActivatings([...(current ? currentActivatings() : []), uid]);
       await deleteProfile(uid);
       mutateProfiles();
       mutateLogs();
+      current && (await onEnhance());
     } catch (err: any) {
       Notice.error(err?.message || err.toString());
+    } finally {
+      setActivatings([]);
     }
   });
 
@@ -396,10 +424,14 @@ const ProfilePage = () => {
                     <ProfileItem
                       id={item.uid}
                       selected={profiles.current === item.uid}
-                      activating={activating === item.uid}
+                      activating={activatings.includes(item.uid)}
                       itemData={item}
                       onSelect={(f) => onSelect(item.uid, f)}
                       onEdit={() => viewerRef.current?.edit(item)}
+                      onChange={async (prev, curr) => {
+                        prev !== curr && (await onEnhance());
+                      }}
+                      onDelete={() => onDelete(item.uid)}
                     />
                   </Grid>
                 ))}
@@ -423,6 +455,7 @@ const ProfilePage = () => {
                 <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
                   <ProfileMore
                     selected={!!chain.includes(item.uid)}
+                    activating={activatings.includes(item.uid)}
                     itemData={item}
                     enableNum={chain.length || 0}
                     logInfo={chainLogs[item.uid]}
@@ -432,6 +465,9 @@ const ProfilePage = () => {
                     onMoveTop={() => onMoveTop(item.uid)}
                     onMoveEnd={() => onMoveEnd(item.uid)}
                     onEdit={() => viewerRef.current?.edit(item)}
+                    onChange={async (prev, curr) => {
+                      prev !== curr && (await onEnhance());
+                    }}
                   />
                 </Grid>
               ))}

+ 8 - 7
src/pages/settings.tsx

@@ -39,23 +39,24 @@ const SettingPage = () => {
           <IconButton
             size="medium"
             color="inherit"
-            title={t("TG Channel")}
-            onClick={toTelegramChannel}
+            title={t("Manual")}
+            onClick={toGithubDoc}
           >
-            <Telegram fontSize="inherit" />
+            <HelpOutlineSharp fontSize="inherit" />
           </IconButton>
           <IconButton
             size="medium"
             color="inherit"
-            title={t("Doc")}
-            onClick={toGithubDoc}
+            title={t("TG Channel")}
+            onClick={toTelegramChannel}
           >
-            <HelpOutlineSharp fontSize="inherit" />
+            <Telegram fontSize="inherit" />
           </IconButton>
+
           <IconButton
             size="medium"
             color="inherit"
-            title={t("Source Code")}
+            title={t("Github Repo")}
             onClick={toGithubRepo}
           >
             <GitHub fontSize="inherit" />