diff --git a/src/components/profile/confirm-viewer.tsx b/src/components/profile/confirm-viewer.tsx index 382998b9..7610abea 100644 --- a/src/components/profile/confirm-viewer.tsx +++ b/src/components/profile/confirm-viewer.tsx @@ -27,10 +27,10 @@ export const ConfirmViewer = (props: Props) => { return ( - {t(title)} + {title} - {t(message)} + {message} diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index c64010f8..c1351d66 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/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(); const instanceRef = useRef(null); const themeMode = useThemeMode(); + const prevData = useRef(); 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()); diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 253c2405..1487cb82 100644 --- a/src/components/profile/profile-item.tsx +++ b/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)", }} > - + )} @@ -312,7 +313,7 @@ export const ProfileItem = (props: Props) => { ) : ( hasUrl && ( - + {from} ) @@ -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() : ""} @@ -334,17 +335,21 @@ export const ProfileItem = (props: Props) => { {/* the third line show extra info or last updated time */} {hasExtra ? ( - + {parseTraffic(upload + download)} / {parseTraffic(total)} - {expire} + {expire} ) : ( - {parseExpire(updated)} + {parseExpire(updated)} )} - + 0 ? 1 : 0 }} + /> { open={fileOpen} language="yaml" schema="clash" + onChange={onChange} onClose={() => setFileOpen(false)} /> setConfirmOpen(false)} onConfirm={() => { diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index d28191b0..94d02d97 100644 --- a/src/components/profile/profile-more.tsx +++ b/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 && ( + + + + )} { open={fileOpen} language={type === "merge" ? "yaml" : "javascript"} schema={type === "merge" ? "merge" : undefined} + onChange={onChange} onClose={() => setFileOpen(false)} /> setConfirmOpen(false)} onConfirm={() => { diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index b749220a..d5da90fb 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -249,10 +249,10 @@ export const SysproxyViewer = forwardRef((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 })); }} diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx index c6ac7d4d..fc74b251 100644 --- a/src/components/setting/mods/theme-viewer.tsx +++ b/src/components/setting/mods/theme-viewer.tsx @@ -129,8 +129,8 @@ export const ThemeViewer = forwardRef((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={() => { diff --git a/src/locales/en.json b/src/locales/en.json index 31a21b0d..c431e0ba 100644 --- a/src/locales/en.json +++ b/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", diff --git a/src/locales/fa.json b/src/locales/fa.json index f8d18ffd..ec9287dd 100644 --- a/src/locales/fa.json +++ b/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": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد", diff --git a/src/locales/ru.json b/src/locales/ru.json index 3c2cf98f..3fb89aef 100644 --- a/src/locales/ru.json +++ b/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": "Невозможно редактировать в режиме только для чтения", diff --git a/src/locales/zh.json b/src/locales/zh.json index be0bbd43..498e4af7 100644 --- a/src/locales/zh.json +++ b/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": "无法在只读模式下编辑", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index be92cc3f..edb0ba8e 100644 --- a/src/pages/profiles.tsx +++ b/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([]); 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 = () => { onSelect(item.uid, f)} onEdit={() => viewerRef.current?.edit(item)} + onChange={async (prev, curr) => { + prev !== curr && (await onEnhance()); + }} + onDelete={() => onDelete(item.uid)} /> ))} @@ -423,6 +455,7 @@ const ProfilePage = () => { { onMoveTop={() => onMoveTop(item.uid)} onMoveEnd={() => onMoveEnd(item.uid)} onEdit={() => viewerRef.current?.edit(item)} + onChange={async (prev, curr) => { + prev !== curr && (await onEnhance()); + }} /> ))} diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index b14b0098..d3b6064f 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -39,15 +39,7 @@ const SettingPage = () => { - - - @@ -55,7 +47,16 @@ const SettingPage = () => { + + + +