import useSWR, { mutate } from "swr"; import { useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { useSetRecoilState } from "recoil"; 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, } from "@/services/cmds"; import { atomLoadingCache } from "@/services/states"; import { closeAllConnections } from "@/services/api"; import { BasePage, DialogRef, Notice } from "@/components/base"; import { ProfileViewer, ProfileViewerRef, } from "@/components/profile/profile-viewer"; import { ProfileItem } from "@/components/profile/profile-item"; import { ProfileMore } from "@/components/profile/profile-more"; import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { throttle } from "lodash-es"; import { useRecoilState } from "recoil"; import { atomThemeMode } from "@/services/states"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; const ProfilePage = () => { const { t } = useTranslation(); const [url, setUrl] = useState(""); const [disabled, setDisabled] = useState(false); const [activating, setActivating] = useState(""); const [loading, setLoading] = useState(false); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const { profiles = {}, activateSelected, patchProfiles, mutateProfiles, } = useProfiles(); const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( "getRuntimeLogs", getRuntimeLogs ); const chain = profiles.chain || []; const viewerRef = useRef(null); const configRef = useRef(null); // distinguish type const { regularItems, enhanceItems } = useMemo(() => { const items = profiles.items || []; const chain = profiles.chain || []; const type1 = ["local", "remote"]; const type2 = ["merge", "script"]; const regularItems = items.filter((i) => i && type1.includes(i.type!)); const restItems = items.filter((i) => i && type2.includes(i.type!)); const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i])); const enhanceItems = chain .map((i) => restMap[i]!) .filter(Boolean) .concat(restItems.filter((i) => !chain.includes(i.uid))); return { regularItems, enhanceItems }; }, [profiles]); const onImport = async () => { if (!url) return; setLoading(true); try { await importProfile(url); Notice.success("Successfully import profile."); setUrl(""); setLoading(false); getProfiles().then((newProfiles) => { mutate("getProfiles", newProfiles); const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); if (!newProfiles.current && remoteItem) { const current = remoteItem.uid; 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(() => setActivating(current), 100); try { await patchProfiles({ current }); mutateLogs(); closeAllConnections(); setTimeout(() => activateSelected(), 2000); Notice.success("Refresh clash config", 1000); } catch (err: any) { Notice.error(err?.message || err.toString(), 4000); } finally { clearTimeout(reset); setActivating(""); } }); const onEnhance = useLockFn(async () => { try { await enhanceProfiles(); 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(); }); // 更新所有订阅 const setLoadingCache = useSetRecoilState(atomLoadingCache); 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 = regularItems.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 navigator.clipboard.readText(); if (text) setUrl(text); }; const [mode] = useRecoilState(atomThemeMode); 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; })} > {regularItems.map((item) => ( onSelect(item.uid, f)} onEdit={() => viewerRef.current?.edit(item)} /> ))} {enhanceItems.length > 0 && ( )} {enhanceItems.length > 0 && ( {enhanceItems.map((item) => ( onEnable(item.uid)} onDisable={() => onDisable(item.uid)} onDelete={() => onDelete(item.uid)} onMoveTop={() => onMoveTop(item.uid)} onMoveEnd={() => onMoveEnd(item.uid)} onEdit={() => viewerRef.current?.edit(item)} /> ))} )} mutateProfiles()} /> ); }; export default ProfilePage;