import useSWR, { mutate } from "swr"; import { useEffect, useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; import { LoadingButton } from "@mui/lab"; import { ClearRounded, ContentPasteRounded, LocalFireDepartmentRounded, RefreshRounded, TextSnippetOutlined, } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { getProfiles, importProfile, enhanceProfiles, getRuntimeLogs, deleteProfile, updateProfile, reorderProfile, createProfile, } from "@/services/cmds"; import { useSetLoadingCache, useThemeMode } from "@/services/states"; import { closeAllConnections } from "@/services/api"; import { BasePage, DialogRef, Notice } from "@/components/base"; import { ProfileViewer, ProfileViewerRef, } from "@/components/profile/profile-viewer"; import { ProfileMore } from "@/components/profile/profile-more"; import { ProfileItem } from "@/components/profile/profile-item"; import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { throttle } from "lodash-es"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; import { listen } from "@tauri-apps/api/event"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; const ProfilePage = () => { const { t } = useTranslation(); const [url, setUrl] = useState(""); const [disabled, setDisabled] = useState(false); const [activatings, setActivatings] = useState([]); const [loading, setLoading] = useState(false); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); useEffect(() => { const unlisten = listen("tauri://file-drop", async (event) => { const fileList = event.payload as string[]; for (let file of fileList) { if (!file.endsWith(".yaml") && !file.endsWith(".yml")) { Notice.error(t("Only YAML Files Supported")); continue; } const item = { type: "local", name: file.split(/\/|\\/).pop() ?? "New Profile", desc: "", url: "", option: { with_proxy: false, self_proxy: false, }, } as IProfileItem; let data = await readTextFile(file); await createProfile(item, data); await mutateProfiles(); } }); return () => { unlisten.then((fn) => fn()); }; }, []); const { profiles = {}, activateSelected, patchProfiles, mutateProfiles, } = useProfiles(); const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( "getRuntimeLogs", getRuntimeLogs ); const viewerRef = useRef(null); const configRef = useRef(null); // distinguish type const profileItems = useMemo(() => { const items = profiles.items || []; const type1 = ["local", "remote"]; const profileItems = items.filter((i) => i && type1.includes(i.type!)); return profileItems; }, [profiles]); const currentActivatings = () => { return [...new Set([profiles.current ?? ""])].filter(Boolean); }; const onImport = async () => { if (!url) return; setLoading(true); try { await importProfile(url); Notice.success(t("Profile Imported Successfully")); setUrl(""); setLoading(false); getProfiles().then(async (newProfiles) => { mutate("getProfiles", newProfiles); const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); const profilesCount = newProfiles.items?.filter( (e) => e.type === "remote" || e.type === "local" ).length as number; if (remoteItem && (profilesCount == 1 || !newProfiles.current)) { const current = remoteItem.uid; await patchProfiles({ current }); mutateLogs(); setTimeout(() => activateSelected(), 2000); } }); } catch (err: any) { Notice.error(err.message || err.toString()); setLoading(false); } finally { setDisabled(false); setLoading(false); } }; const onDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { if (active.id !== over.id) { await reorderProfile(active.id.toString(), over.id.toString()); mutateProfiles(); } } }; const onSelect = useLockFn(async (current: string, force: boolean) => { if (!force && current === profiles.current) return; // 避免大多数情况下loading态闪烁 const reset = setTimeout(() => { setActivatings([...currentActivatings(), current]); }, 100); try { await patchProfiles({ current }); await mutateLogs(); closeAllConnections(); activateSelected().then(() => { Notice.success(t("Profile Switched"), 1000); }); } catch (err: any) { Notice.error(err?.message || err.toString(), 4000); } finally { clearTimeout(reset); 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 onDelete = useLockFn(async (uid: string) => { const current = profiles.current === uid; try { setActivatings([...(current ? currentActivatings() : []), uid]); await deleteProfile(uid); mutateProfiles(); mutateLogs(); current && (await onEnhance()); } catch (err: any) { Notice.error(err?.message || err.toString()); } finally { setActivatings([]); } }); // 更新所有订阅 const setLoadingCache = useSetLoadingCache(); const onUpdateAll = useLockFn(async () => { const throttleMutate = throttle(mutateProfiles, 2000, { trailing: true, }); const updateOne = async (uid: string) => { try { await updateProfile(uid); throttleMutate(); } finally { setLoadingCache((cache) => ({ ...cache, [uid]: false })); } }; return new Promise((resolve) => { setLoadingCache((cache) => { // 获取没有正在更新的订阅 const items = profileItems.filter( (e) => e.type === "remote" && !cache[e.uid] ); const change = Object.fromEntries(items.map((e) => [e.uid, true])); Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve); return { ...cache, ...change }; }); }); }); const onCopyLink = async () => { const text = await readText(); if (text) setUrl(text); }; const mode = useThemeMode(); const islight = mode === "light" ? true : false; const dividercolor = islight ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; return ( configRef.current?.open()} > } > setUrl(e.target.value)} placeholder={t("Profile URL")} InputProps={{ sx: { pr: 1 }, endAdornment: !url ? ( ) : ( setUrl("")} > ), }} /> {t("Import")} { return x.uid; })} > {profileItems.map((item) => ( onSelect(item.uid, f)} onEdit={() => viewerRef.current?.edit(item)} onSave={async (prev, curr) => { if (prev !== curr && profiles.current === item.uid) { await onEnhance(); } }} onDelete={() => onDelete(item.uid)} /> ))} { if (prev !== curr) { await onEnhance(); } }} /> { if (prev !== curr) { await onEnhance(); } }} /> mutateProfiles()} /> ); }; export default ProfilePage;