import { ReactNode, useEffect, useMemo, useState } from "react"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import { useTranslation } from "react-i18next"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; import { Autocomplete, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemText, TextField, styled, } from "@mui/material"; import { GroupItem } from "@/components/profile/group-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { Notice, Switch } from "@/components/base"; import getSystem from "@/utils/get-system"; import { BaseSearchBox } from "../base/base-search-box"; import { Virtuoso } from "react-virtuoso"; import MonacoEditor from "react-monaco-editor"; import { useThemeMode } from "@/services/states"; import { Controller, useForm } from "react-hook-form"; interface Props { proxiesUid: string; mergeUid: string; profileUid: string; property: string; open: boolean; onClose: () => void; onSave?: (prev?: string, curr?: string) => void; } const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; export const GroupsEditorViewer = (props: Props) => { const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } = props; const { t } = useTranslation(); const themeMode = useThemeMode(); const [prevData, setPrevData] = useState(""); const [currData, setCurrData] = useState(""); const [visualization, setVisualization] = useState(true); const [match, setMatch] = useState(() => (_: string) => true); const { control, watch, register, ...formIns } = useForm({ defaultValues: { type: "select", name: "", lazy: true, }, }); const [groupList, setGroupList] = useState([]); const [proxyPolicyList, setProxyPolicyList] = useState([]); const [proxyProviderList, setProxyProviderList] = useState([]); const [prependSeq, setPrependSeq] = useState([]); const [appendSeq, setAppendSeq] = useState([]); const [deleteSeq, setDeleteSeq] = useState([]); const filteredGroupList = useMemo( () => groupList.filter((group) => match(group.name)), [groupList, match] ); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const reorder = ( list: IProxyGroupConfig[], startIndex: number, endIndex: number ) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; const onPrependDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { if (active.id !== over.id) { let activeIndex = 0; let overIndex = 0; prependSeq.forEach((item, index) => { if (item.name === active.id) { activeIndex = index; } if (item.name === over.id) { overIndex = index; } }); setPrependSeq(reorder(prependSeq, activeIndex, overIndex)); } } }; const onAppendDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { if (active.id !== over.id) { let activeIndex = 0; let overIndex = 0; appendSeq.forEach((item, index) => { if (item.name === active.id) { activeIndex = index; } if (item.name === over.id) { overIndex = index; } }); setAppendSeq(reorder(appendSeq, activeIndex, overIndex)); } } }; const fetchContent = async () => { let data = await readProfileFile(property); let obj = yaml.load(data) as ISeqProfileConfig | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); setDeleteSeq(obj?.delete || []); setPrevData(data); setCurrData(data); }; useEffect(() => { if (currData === "") return; if (visualization !== true) return; let obj = yaml.load(currData) as { prepend: []; append: []; delete: []; } | null; setPrependSeq(obj?.prepend || []); setAppendSeq(obj?.append || []); setDeleteSeq(obj?.delete || []); }, [visualization]); useEffect(() => { if (prependSeq && appendSeq && deleteSeq) setCurrData( yaml.dump( { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { forceQuotes: true, } ) ); }, [prependSeq, appendSeq, deleteSeq]); const fetchProxyPolicy = async () => { let data = await readProfileFile(profileUid); let proxiesData = await readProfileFile(proxiesUid); let originGroupsObj = yaml.load(data) as { "proxy-groups": IProxyGroupConfig[]; } | null; let originProxiesObj = yaml.load(data) as { proxies: [] } | null; let originProxies = originProxiesObj?.proxies || []; let moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null; let morePrependProxies = moreProxiesObj?.prepend || []; let moreAppendProxies = moreProxiesObj?.append || []; let moreDeleteProxies = moreProxiesObj?.delete || ([] as string[] | { name: string }[]); let proxies = morePrependProxies.concat( originProxies.filter((proxy: any) => { if (proxy.name) { return !moreDeleteProxies.includes(proxy.name); } else { return !moreDeleteProxies.includes(proxy); } }), moreAppendProxies ); setProxyPolicyList( builtinProxyPolicies.concat( prependSeq.map((group: IProxyGroupConfig) => group.name), originGroupsObj?.["proxy-groups"] .map((group: IProxyGroupConfig) => group.name) .filter((name) => !deleteSeq.includes(name)) || [], appendSeq.map((group: IProxyGroupConfig) => group.name), proxies.map((proxy: any) => proxy.name) ) ); }; const fetchProfile = async () => { let data = await readProfileFile(profileUid); let mergeData = await readProfileFile(mergeUid); let globalMergeData = await readProfileFile("Merge"); let originGroupsObj = yaml.load(data) as { "proxy-groups": IProxyGroupConfig[]; } | null; let originProviderObj = yaml.load(data) as { "proxy-providers": {} } | null; let originProvider = originProviderObj?.["proxy-providers"] || {}; let moreProviderObj = yaml.load(mergeData) as { "proxy-providers": {}; } | null; let moreProvider = moreProviderObj?.["proxy-providers"] || {}; let globalProviderObj = yaml.load(globalMergeData) as { "proxy-providers": {}; } | null; let globalProvider = globalProviderObj?.["proxy-providers"] || {}; let provider = Object.assign( {}, originProvider, moreProvider, globalProvider ); setProxyProviderList(Object.keys(provider)); setGroupList(originGroupsObj?.["proxy-groups"] || []); }; useEffect(() => { fetchProxyPolicy(); }, [prependSeq, appendSeq, deleteSeq]); useEffect(() => { if (!open) return; fetchContent(); fetchProxyPolicy(); fetchProfile(); }, [open]); const validateGroup = () => { let group = formIns.getValues(); if (group.name === "") { throw new Error(t("Group Name Cannot Be Empty")); } }; const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); onSave?.(prevData, currData); onClose(); } catch (err: any) { Notice.error(err.message || err.toString()); } }); return ( { {t("Edit Groups")} } {visualization ? ( <> ( value && field.onChange(value)} renderInput={(params) => } /> )} /> ( )} /> ( )} /> ( value && field.onChange(value)} renderInput={(params) => } /> )} /> ( value && field.onChange(value)} renderInput={(params) => } /> )} /> ( )} /> ( { field.onChange(parseInt(e.target.value)); }} /> )} /> ( { field.onChange(parseInt(e.target.value)); }} /> )} /> ( { field.onChange(parseInt(e.target.value)); }} /> )} /> ( )} /> ( { field.onChange(parseInt(e.target.value)); }} /> )} /> ( )} /> ( )} /> ( )} /> ( { field.onChange(parseInt(e.target.value)); }} /> )} /> ( )} /> ( )} /> ( )} /> ( )} /> ( )} /> ( )} /> setMatch(() => match)} /> 0 ? 1 : 0) + (appendSeq.length > 0 ? 1 : 0) } increaseViewportBy={256} itemContent={(index) => { let shift = prependSeq.length > 0 ? 1 : 0; if (prependSeq.length > 0 && index === 0) { return ( { return x.name; })} > {prependSeq.map((item, index) => { return ( { setPrependSeq( prependSeq.filter( (v) => v.name !== item.name ) ); }} /> ); })} ); } else if (index < filteredGroupList.length + shift) { let newIndex = index - shift; return ( { if ( deleteSeq.includes(filteredGroupList[newIndex].name) ) { setDeleteSeq( deleteSeq.filter( (v) => v !== filteredGroupList[newIndex].name ) ); } else { setDeleteSeq((prev) => [ ...prev, filteredGroupList[newIndex].name, ]); } }} /> ); } else { return ( { return x.name; })} > {appendSeq.map((item, index) => { return ( { setAppendSeq( appendSeq.filter( (v) => v.name !== item.name ) ); }} /> ); })} ); } }} /> ) : ( = 1500, // 超过一定宽度显示minimap滚动条 }, mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例 quickSuggestions: { strings: true, // 字符串类型的建议 comments: true, // 注释类型的建议 other: true, // 其他类型的建议 }, padding: { top: 33, // 顶部padding防止遮挡snippets }, fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ getSystem() === "windows" ? ", twemoji mozilla" : "" }`, fontLigatures: true, // 连字符 smoothScrolling: true, // 平滑滚动 }} onChange={(value) => setCurrData(value)} /> )} ); }; const Item = styled(ListItem)(() => ({ padding: "5px 2px", }));