refactor: profile components
This commit is contained in:
@@ -20,7 +20,7 @@ import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
|
|||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import { configureMonacoYaml } from "monaco-yaml";
|
import { configureMonacoYaml } from "monaco-yaml";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
import pac from "types-pac/pac.d.ts?raw";
|
import pac from "types-pac/pac.d.ts?raw";
|
||||||
@@ -63,13 +63,13 @@ const monacoInitialization = () => {
|
|||||||
{
|
{
|
||||||
uri: "http://example.com/meta-json-schema.json",
|
uri: "http://example.com/meta-json-schema.json",
|
||||||
fileMatch: ["**/*.clash.yaml"],
|
fileMatch: ["**/*.clash.yaml"],
|
||||||
// @ts-ignore
|
// @ts-expect-error -- meta schema JSON import does not satisfy JSONSchema7 at compile time
|
||||||
schema: metaSchema as JSONSchema7,
|
schema: metaSchema as JSONSchema7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uri: "http://example.com/clash-verge-merge-json-schema.json",
|
uri: "http://example.com/clash-verge-merge-json-schema.json",
|
||||||
fileMatch: ["**/*.merge.yaml"],
|
fileMatch: ["**/*.merge.yaml"],
|
||||||
// @ts-ignore
|
// @ts-expect-error -- merge schema JSON import does not satisfy JSONSchema7 at compile time
|
||||||
schema: mergeSchema as JSONSchema7,
|
schema: mergeSchema as JSONSchema7,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -87,8 +87,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
open = false,
|
open = false,
|
||||||
title = t("Edit File"),
|
title,
|
||||||
initialData = Promise.resolve(""),
|
initialData,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
language = "yaml",
|
language = "yaml",
|
||||||
schema,
|
schema,
|
||||||
@@ -97,6 +97,12 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
onClose,
|
onClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const resolvedTitle = title ?? t("Edit File");
|
||||||
|
const resolvedInitialData = useMemo(
|
||||||
|
() => initialData ?? Promise.resolve(""),
|
||||||
|
[initialData],
|
||||||
|
);
|
||||||
|
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined);
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined);
|
||||||
const prevData = useRef<string | undefined>("");
|
const prevData = useRef<string | undefined>("");
|
||||||
const currData = useRef<string | undefined>("");
|
const currData = useRef<string | undefined>("");
|
||||||
@@ -111,7 +117,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
|
||||||
// retrieve initial data
|
// retrieve initial data
|
||||||
await initialData.then((data) => {
|
await resolvedInitialData.then((data) => {
|
||||||
prevData.current = data;
|
prevData.current = data;
|
||||||
currData.current = data;
|
currData.current = data;
|
||||||
|
|
||||||
@@ -133,7 +139,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
!readOnly && onSave?.(prevData.current, currData.current);
|
if (!readOnly) {
|
||||||
|
onSave?.(prevData.current, currData.current);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
@@ -148,10 +156,14 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorResize = debounce(() => {
|
const editorResize = useMemo(
|
||||||
editorRef.current?.layout();
|
() =>
|
||||||
setTimeout(() => editorRef.current?.layout(), 500);
|
debounce(() => {
|
||||||
}, 100);
|
editorRef.current?.layout();
|
||||||
|
setTimeout(() => editorRef.current?.layout(), 500);
|
||||||
|
}, 100),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResized = debounce(() => {
|
const onResized = debounce(() => {
|
||||||
@@ -167,11 +179,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
editorRef.current?.dispose();
|
editorRef.current?.dispose();
|
||||||
editorRef.current = undefined;
|
editorRef.current = undefined;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [editorResize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{resolvedTitle}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -24,37 +24,50 @@ export const GroupItem = (props: Props) => {
|
|||||||
const sortable = type === "prepend" || type === "append";
|
const sortable = type === "prepend" || type === "append";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes: sortableAttributes,
|
||||||
listeners,
|
listeners: sortableListeners,
|
||||||
setNodeRef,
|
setNodeRef: sortableSetNodeRef,
|
||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = sortable
|
} = useSortable({
|
||||||
? useSortable({ id: group.name })
|
id: group.name,
|
||||||
: {
|
disabled: !sortable,
|
||||||
attributes: {},
|
});
|
||||||
listeners: {},
|
const dragAttributes = sortable ? sortableAttributes : undefined;
|
||||||
setNodeRef: null,
|
const dragListeners = sortable ? sortableListeners : undefined;
|
||||||
transform: null,
|
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
||||||
transition: null,
|
|
||||||
isDragging: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [iconCachePath, setIconCachePath] = useState("");
|
const [iconCachePath, setIconCachePath] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initIconCachePath();
|
let cancelled = false;
|
||||||
}, [group]);
|
const initIconCachePath = async () => {
|
||||||
|
const icon = group.icon?.trim() ?? "";
|
||||||
|
if (icon.startsWith("http")) {
|
||||||
|
try {
|
||||||
|
const fileName =
|
||||||
|
group.name.replaceAll(" ", "") + "-" + getFileName(icon);
|
||||||
|
const iconPath = await downloadIconCache(icon, fileName);
|
||||||
|
if (!cancelled) {
|
||||||
|
setIconCachePath(convertFileSrc(iconPath));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIconCachePath("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!cancelled) {
|
||||||
|
setIconCachePath("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function initIconCachePath() {
|
void initIconCachePath();
|
||||||
if (group.icon && group.icon.trim().startsWith("http")) {
|
|
||||||
const fileName =
|
return () => {
|
||||||
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
|
cancelled = true;
|
||||||
const iconPath = await downloadIconCache(group.icon, fileName);
|
};
|
||||||
setIconCachePath(convertFileSrc(iconPath));
|
}, [group.icon, group.name]);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileName(url: string) {
|
function getFileName(url: string) {
|
||||||
return url.substring(url.lastIndexOf("/") + 1);
|
return url.substring(url.lastIndexOf("/") + 1);
|
||||||
@@ -108,9 +121,9 @@ export const GroupItem = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ListItemText
|
<ListItemText
|
||||||
{...attributes}
|
{...(dragAttributes ?? {})}
|
||||||
{...listeners}
|
{...(dragListeners ?? {})}
|
||||||
ref={setNodeRef}
|
ref={dragNodeRef}
|
||||||
sx={{ cursor: sortable ? "move" : "" }}
|
sx={{ cursor: sortable ? "move" : "" }}
|
||||||
primary={
|
primary={
|
||||||
<StyledPrimary
|
<StyledPrimary
|
||||||
@@ -133,11 +146,13 @@ export const GroupItem = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{
|
slotProps={{
|
||||||
sx: {
|
secondary: {
|
||||||
display: "flex",
|
sx: {
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
color: "#ccc",
|
alignItems: "center",
|
||||||
|
color: "#ccc",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ import {
|
|||||||
cancelIdleCallback,
|
cancelIdleCallback,
|
||||||
} from "foxact/request-idle-callback";
|
} from "foxact/request-idle-callback";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
@@ -160,7 +166,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const fetchContent = async () => {
|
const fetchContent = useCallback(async () => {
|
||||||
const data = await readProfileFile(property);
|
const data = await readProfileFile(property);
|
||||||
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||||
|
|
||||||
@@ -170,21 +176,20 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
setPrevData(data);
|
setPrevData(data);
|
||||||
setCurrData(data);
|
setCurrData(data);
|
||||||
};
|
}, [property]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currData === "") return;
|
if (currData === "" || visualization !== true) {
|
||||||
if (visualization !== true) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const obj = yaml.load(currData) as {
|
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
||||||
prepend: [];
|
startTransition(() => {
|
||||||
append: [];
|
setPrependSeq(obj?.prepend ?? []);
|
||||||
delete: [];
|
setAppendSeq(obj?.append ?? []);
|
||||||
} | null;
|
setDeleteSeq(obj?.delete ?? []);
|
||||||
setPrependSeq(obj?.prepend || []);
|
});
|
||||||
setAppendSeq(obj?.append || []);
|
}, [currData, visualization]);
|
||||||
setDeleteSeq(obj?.delete || []);
|
|
||||||
}, [visualization]);
|
|
||||||
|
|
||||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -210,7 +215,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
const fetchProxyPolicy = async () => {
|
const fetchProxyPolicy = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid);
|
const data = await readProfileFile(profileUid);
|
||||||
const proxiesData = await readProfileFile(proxiesUid);
|
const proxiesData = await readProfileFile(proxiesUid);
|
||||||
const originGroupsObj = yaml.load(data) as {
|
const originGroupsObj = yaml.load(data) as {
|
||||||
@@ -246,8 +251,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
proxies.map((proxy: any) => proxy.name),
|
proxies.map((proxy: any) => proxy.name),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
}, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid);
|
const data = await readProfileFile(profileUid);
|
||||||
const mergeData = await readProfileFile(mergeUid);
|
const mergeData = await readProfileFile(mergeUid);
|
||||||
const globalMergeData = await readProfileFile("Merge");
|
const globalMergeData = await readProfileFile("Merge");
|
||||||
@@ -257,17 +262,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
const originProviderObj = yaml.load(data) as {
|
const originProviderObj = yaml.load(data) as {
|
||||||
"proxy-providers": {};
|
"proxy-providers": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const originProvider = originProviderObj?.["proxy-providers"] || {};
|
const originProvider = originProviderObj?.["proxy-providers"] || {};
|
||||||
|
|
||||||
const moreProviderObj = yaml.load(mergeData) as {
|
const moreProviderObj = yaml.load(mergeData) as {
|
||||||
"proxy-providers": {};
|
"proxy-providers": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const moreProvider = moreProviderObj?.["proxy-providers"] || {};
|
const moreProvider = moreProviderObj?.["proxy-providers"] || {};
|
||||||
|
|
||||||
const globalProviderObj = yaml.load(globalMergeData) as {
|
const globalProviderObj = yaml.load(globalMergeData) as {
|
||||||
"proxy-providers": {};
|
"proxy-providers": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const globalProvider = globalProviderObj?.["proxy-providers"] || {};
|
const globalProvider = globalProviderObj?.["proxy-providers"] || {};
|
||||||
|
|
||||||
@@ -280,21 +285,27 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
setProxyProviderList(Object.keys(provider));
|
setProxyProviderList(Object.keys(provider));
|
||||||
setGroupList(originGroupsObj?.["proxy-groups"] || []);
|
setGroupList(originGroupsObj?.["proxy-groups"] || []);
|
||||||
};
|
}, [mergeUid, profileUid]);
|
||||||
const getInterfaceNameList = async () => {
|
const getInterfaceNameList = useCallback(async () => {
|
||||||
const list = await getNetworkInterfaces();
|
const list = await getNetworkInterfaces();
|
||||||
setInterfaceNameList(list);
|
setInterfaceNameList(list);
|
||||||
};
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProxyPolicy();
|
fetchProxyPolicy();
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [fetchProxyPolicy]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchContent();
|
fetchContent();
|
||||||
fetchProxyPolicy();
|
fetchProxyPolicy();
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
getInterfaceNameList();
|
getInterfaceNameList();
|
||||||
}, [open]);
|
}, [
|
||||||
|
fetchContent,
|
||||||
|
fetchProfile,
|
||||||
|
fetchProxyPolicy,
|
||||||
|
getInterfaceNameList,
|
||||||
|
open,
|
||||||
|
]);
|
||||||
|
|
||||||
const validateGroup = () => {
|
const validateGroup = () => {
|
||||||
const group = formIns.getValues();
|
const group = formIns.getValues();
|
||||||
@@ -811,10 +822,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
return x.name;
|
return x.name;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredPrependSeq.map((item, index) => {
|
{filteredPrependSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<GroupItem
|
<GroupItem
|
||||||
key={`${item.name}-${index}`}
|
key={item.name}
|
||||||
type="prepend"
|
type="prepend"
|
||||||
group={item}
|
group={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
@@ -834,7 +845,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
const newIndex = index - shift;
|
const newIndex = index - shift;
|
||||||
return (
|
return (
|
||||||
<GroupItem
|
<GroupItem
|
||||||
key={`${filteredGroupList[newIndex].name}-${index}`}
|
key={filteredGroupList[newIndex].name}
|
||||||
type={
|
type={
|
||||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||||
? "delete"
|
? "delete"
|
||||||
@@ -871,10 +882,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
return x.name;
|
return x.name;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredAppendSeq.map((item, index) => {
|
{filteredAppendSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<GroupItem
|
<GroupItem
|
||||||
key={`${item.name}-${index}`}
|
key={item.name}
|
||||||
type="append"
|
type="append"
|
||||||
group={item}
|
group={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export const LogViewer = (props: Props) => {
|
|||||||
pb: 1,
|
pb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{logInfo.map(([level, log], index) => (
|
{logInfo.map(([level, log]) => (
|
||||||
<Fragment key={index.toString()}>
|
<Fragment key={`${level}-${log}`}>
|
||||||
<Typography color="text.secondary" component="div">
|
<Typography color="text.secondary" component="div">
|
||||||
<Chip
|
<Chip
|
||||||
label={level}
|
label={level}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useReducer, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ interface Props {
|
|||||||
|
|
||||||
export const ProfileItem = (props: Props) => {
|
export const ProfileItem = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
selected,
|
selected,
|
||||||
activating,
|
activating,
|
||||||
itemData,
|
itemData,
|
||||||
@@ -80,11 +81,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({
|
} = useSortable({
|
||||||
id: props.id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||||
const loadingCache = useLoadingCache();
|
const loadingCache = useLoadingCache();
|
||||||
const setLoadingCache = useSetLoadingCache();
|
const setLoadingCache = useSetLoadingCache();
|
||||||
@@ -166,37 +167,44 @@ export const ProfileItem = (props: Props) => {
|
|||||||
if (showNextUpdate) {
|
if (showNextUpdate) {
|
||||||
fetchNextUpdateTime();
|
fetchNextUpdateTime();
|
||||||
}
|
}
|
||||||
}, [showNextUpdate, itemData.option?.update_interval, updated]);
|
}, [
|
||||||
|
fetchNextUpdateTime,
|
||||||
|
showNextUpdate,
|
||||||
|
itemData.option?.update_interval,
|
||||||
|
updated,
|
||||||
|
]);
|
||||||
|
|
||||||
// 订阅定时器更新事件
|
// 订阅定时器更新事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let refreshTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
||||||
const handleTimerUpdate = (event: any) => {
|
const handleTimerUpdate = (event: Event) => {
|
||||||
const updatedUid = event.payload as string;
|
const source = event as CustomEvent<string> & { payload?: string };
|
||||||
|
const updatedUid = source.detail ?? source.payload;
|
||||||
|
|
||||||
// 只有当更新的是当前配置时才刷新显示
|
// 只有当更新的是当前配置时才刷新显示
|
||||||
if (updatedUid === itemData.uid && showNextUpdate) {
|
if (updatedUid === itemData.uid && showNextUpdate) {
|
||||||
console.log(`收到定时器更新事件: uid=${updatedUid}`);
|
console.log(`收到定时器更新事件: uid=${updatedUid}`);
|
||||||
setTimeout(() => {
|
if (refreshTimeout) {
|
||||||
|
clearTimeout(refreshTimeout);
|
||||||
|
}
|
||||||
|
refreshTimeout = window.setTimeout(() => {
|
||||||
fetchNextUpdateTime(true);
|
fetchNextUpdateTime(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只注册定时器更新事件监听
|
// 只注册定时器更新事件监听
|
||||||
window.addEventListener(
|
window.addEventListener("verge://timer-updated", handleTimerUpdate);
|
||||||
"verge://timer-updated",
|
|
||||||
handleTimerUpdate as EventListener,
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (refreshTimeout) {
|
||||||
|
clearTimeout(refreshTimeout);
|
||||||
|
}
|
||||||
// 清理事件监听
|
// 清理事件监听
|
||||||
window.removeEventListener(
|
window.removeEventListener("verge://timer-updated", handleTimerUpdate);
|
||||||
"verge://timer-updated",
|
|
||||||
handleTimerUpdate as EventListener,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}, [showNextUpdate, itemData.uid]);
|
}, [fetchNextUpdateTime, itemData.uid, showNextUpdate]);
|
||||||
|
|
||||||
// local file mode
|
// local file mode
|
||||||
// remote file mode
|
// remote file mode
|
||||||
@@ -217,11 +225,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
const loading = loadingCache[itemData.uid] ?? false;
|
const loading = loadingCache[itemData.uid] ?? false;
|
||||||
|
|
||||||
// interval update fromNow field
|
// interval update fromNow field
|
||||||
const [, setRefresh] = useState({});
|
const [, forceRefresh] = useReducer((value: number) => value + 1, 0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasUrl) return;
|
if (!hasUrl) return;
|
||||||
|
|
||||||
let timer: any = null;
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -232,7 +240,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4;
|
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4;
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
setRefresh({});
|
forceRefresh();
|
||||||
handler();
|
handler();
|
||||||
}, wait);
|
}, wait);
|
||||||
};
|
};
|
||||||
@@ -240,9 +248,12 @@ export const ProfileItem = (props: Props) => {
|
|||||||
handler();
|
handler();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [hasUrl, updated]);
|
}, [forceRefresh, hasUrl, updated]);
|
||||||
|
|
||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false);
|
||||||
const [rulesOpen, setRulesOpen] = useState(false);
|
const [rulesOpen, setRulesOpen] = useState(false);
|
||||||
@@ -382,7 +393,9 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
if (batchMode) {
|
if (batchMode) {
|
||||||
// If in batch mode, just toggle selection instead of showing delete confirmation
|
// If in batch mode, just toggle selection instead of showing delete confirmation
|
||||||
onSelectionChange && onSelectionChange();
|
if (onSelectionChange) {
|
||||||
|
onSelectionChange();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
}
|
}
|
||||||
@@ -426,7 +439,9 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
if (batchMode) {
|
if (batchMode) {
|
||||||
// If in batch mode, just toggle selection instead of showing delete confirmation
|
// If in batch mode, just toggle selection instead of showing delete confirmation
|
||||||
onSelectionChange && onSelectionChange();
|
if (onSelectionChange) {
|
||||||
|
onSelectionChange();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
}
|
}
|
||||||
@@ -444,14 +459,16 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 监听自动更新事件
|
// 监听自动更新事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUpdateStarted = (event: CustomEvent) => {
|
const handleUpdateStarted = (event: Event) => {
|
||||||
if (event.detail.uid === itemData.uid) {
|
const customEvent = event as CustomEvent<{ uid?: string }>;
|
||||||
|
if (customEvent.detail?.uid === itemData.uid) {
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCompleted = (event: CustomEvent) => {
|
const handleUpdateCompleted = (event: Event) => {
|
||||||
if (event.detail.uid === itemData.uid) {
|
const customEvent = event as CustomEvent<{ uid?: string }>;
|
||||||
|
if (customEvent.detail?.uid === itemData.uid) {
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
||||||
// 更新完成后刷新显示
|
// 更新完成后刷新显示
|
||||||
if (showNextUpdate) {
|
if (showNextUpdate) {
|
||||||
@@ -461,27 +478,18 @@ export const ProfileItem = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 注册事件监听
|
// 注册事件监听
|
||||||
window.addEventListener(
|
window.addEventListener("profile-update-started", handleUpdateStarted);
|
||||||
"profile-update-started",
|
window.addEventListener("profile-update-completed", handleUpdateCompleted);
|
||||||
handleUpdateStarted as EventListener,
|
|
||||||
);
|
|
||||||
window.addEventListener(
|
|
||||||
"profile-update-completed",
|
|
||||||
handleUpdateCompleted as EventListener,
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// 清理事件监听
|
// 清理事件监听
|
||||||
window.removeEventListener(
|
window.removeEventListener("profile-update-started", handleUpdateStarted);
|
||||||
"profile-update-started",
|
|
||||||
handleUpdateStarted as EventListener,
|
|
||||||
);
|
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"profile-update-completed",
|
"profile-update-completed",
|
||||||
handleUpdateCompleted as EventListener,
|
handleUpdateCompleted,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [itemData.uid, showNextUpdate]);
|
}, [fetchNextUpdateTime, itemData.uid, setLoadingCache, showNextUpdate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -506,7 +514,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
setPosition({ top: clientY, left: clientX });
|
setPosition({ top: clientY, left: clientX });
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget as HTMLElement);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -543,7 +551,9 @@ export const ProfileItem = (props: Props) => {
|
|||||||
sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }}
|
sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelectionChange && onSelectionChange();
|
if (onSelectionChange) {
|
||||||
|
onSelectionChange();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
@@ -737,7 +747,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
schema="clash"
|
schema="clash"
|
||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
await saveProfileFile(uid, curr ?? "");
|
await saveProfileFile(uid, curr ?? "");
|
||||||
onSave && onSave(prev, curr);
|
onSave?.(prev, curr);
|
||||||
}}
|
}}
|
||||||
onClose={() => setFileOpen(false)}
|
onClose={() => setFileOpen(false)}
|
||||||
/>
|
/>
|
||||||
@@ -783,7 +793,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
schema="clash"
|
schema="clash"
|
||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
await saveProfileFile(option?.merge ?? "", curr ?? "");
|
await saveProfileFile(option?.merge ?? "", curr ?? "");
|
||||||
onSave && onSave(prev, curr);
|
onSave?.(prev, curr);
|
||||||
}}
|
}}
|
||||||
onClose={() => setMergeOpen(false)}
|
onClose={() => setMergeOpen(false)}
|
||||||
/>
|
/>
|
||||||
@@ -795,7 +805,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
language="javascript"
|
language="javascript"
|
||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
await saveProfileFile(option?.script ?? "", curr ?? "");
|
await saveProfileFile(option?.script ?? "", curr ?? "");
|
||||||
onSave && onSave(prev, curr);
|
onSave?.(prev, curr);
|
||||||
}}
|
}}
|
||||||
onClose={() => setScriptOpen(false)}
|
onClose={() => setScriptOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ interface Props {
|
|||||||
onSave?: (prev?: string, curr?: string) => void;
|
onSave?: (prev?: string, curr?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_LOG_INFO: [string, string][] = [];
|
||||||
|
|
||||||
// profile enhanced item
|
// profile enhanced item
|
||||||
export const ProfileMore = (props: Props) => {
|
export const ProfileMore = (props: Props) => {
|
||||||
const { id, logInfo = [], onSave } = props;
|
const { id, logInfo, onSave } = props;
|
||||||
|
|
||||||
|
const entries = logInfo ?? EMPTY_LOG_INFO;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false);
|
||||||
const [logOpen, setLogOpen] = useState(false);
|
const [logOpen, setLogOpen] = useState(false);
|
||||||
@@ -49,7 +52,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasError = !!logInfo.find((e) => e[0] === "exception");
|
const hasError = entries.some(([level]) => level === "exception");
|
||||||
|
|
||||||
const itemMenu = [
|
const itemMenu = [
|
||||||
{ label: "Edit File", handler: onEditFile },
|
{ label: "Edit File", handler: onEditFile },
|
||||||
@@ -71,7 +74,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
setPosition({ top: clientY, left: clientX });
|
setPosition({ top: clientY, left: clientX });
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget as HTMLElement);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -173,7 +176,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
schema={id === "Merge" ? "clash" : undefined}
|
schema={id === "Merge" ? "clash" : undefined}
|
||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
await saveProfileFile(id, curr ?? "");
|
await saveProfileFile(id, curr ?? "");
|
||||||
onSave && onSave(prev, curr);
|
onSave?.(prev, curr);
|
||||||
}}
|
}}
|
||||||
onClose={() => setFileOpen(false)}
|
onClose={() => setFileOpen(false)}
|
||||||
/>
|
/>
|
||||||
@@ -181,7 +184,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
{logOpen && (
|
{logOpen && (
|
||||||
<LogViewer
|
<LogViewer
|
||||||
open={logOpen}
|
open={logOpen}
|
||||||
logInfo={logInfo}
|
logInfo={entries}
|
||||||
onClose={() => setLogOpen(false)}
|
onClose={() => setLogOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,23 +45,19 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
// file input
|
// file input
|
||||||
const fileDataRef = useRef<string | null>(null);
|
const fileDataRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const {
|
const { control, watch, setValue, reset, handleSubmit, getValues } =
|
||||||
control,
|
useForm<IProfileItem>({
|
||||||
watch,
|
defaultValues: {
|
||||||
register: _register,
|
type: "remote",
|
||||||
...formIns
|
name: "",
|
||||||
} = useForm<IProfileItem>({
|
desc: "",
|
||||||
defaultValues: {
|
url: "",
|
||||||
type: "remote",
|
option: {
|
||||||
name: "",
|
with_proxy: false,
|
||||||
desc: "",
|
self_proxy: false,
|
||||||
url: "",
|
},
|
||||||
option: {
|
|
||||||
with_proxy: false,
|
|
||||||
self_proxy: false,
|
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
create: () => {
|
create: () => {
|
||||||
@@ -71,7 +67,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
edit: (item: IProfileItem) => {
|
edit: (item: IProfileItem) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
Object.entries(item).forEach(([key, value]) => {
|
||||||
formIns.setValue(key as any, value);
|
setValue(key as any, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setOpenType("edit");
|
setOpenType("edit");
|
||||||
@@ -83,15 +79,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
const withProxy = watch("option.with_proxy");
|
const withProxy = watch("option.with_proxy");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selfProxy) formIns.setValue("option.with_proxy", false);
|
if (selfProxy) setValue("option.with_proxy", false);
|
||||||
}, [selfProxy]);
|
}, [selfProxy, setValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (withProxy) formIns.setValue("option.self_proxy", false);
|
if (withProxy) setValue("option.self_proxy", false);
|
||||||
}, [withProxy]);
|
}, [setValue, withProxy]);
|
||||||
|
|
||||||
const handleOk = useLockFn(
|
const handleOk = useLockFn(
|
||||||
formIns.handleSubmit(async (form) => {
|
handleSubmit(async (form) => {
|
||||||
if (form.option?.timeout_seconds) {
|
if (form.option?.timeout_seconds) {
|
||||||
form.option.timeout_seconds = +form.option.timeout_seconds;
|
form.option.timeout_seconds = +form.option.timeout_seconds;
|
||||||
}
|
}
|
||||||
@@ -183,7 +179,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
|
|
||||||
// 成功后的操作
|
// 成功后的操作
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setTimeout(() => formIns.reset(), 500);
|
setTimeout(() => reset(), 500);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
|
|
||||||
// 优化:UI先关闭,异步通知父组件
|
// 优化:UI先关闭,异步通知父组件
|
||||||
@@ -202,7 +198,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
try {
|
try {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
setTimeout(() => formIns.reset(), 500);
|
setTimeout(() => reset(), 500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ProfileViewer] handleClose error:", e);
|
console.warn("[ProfileViewer] handleClose error:", e);
|
||||||
}
|
}
|
||||||
@@ -341,7 +337,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{isLocal && openType === "new" && (
|
{isLocal && openType === "new" && (
|
||||||
<FileInput
|
<FileInput
|
||||||
onChange={(file, val) => {
|
onChange={(file, val) => {
|
||||||
formIns.setValue("name", formIns.getValues("name") || file.name);
|
setValue("name", getValues("name") || file.name);
|
||||||
fileDataRef.current = val;
|
fileDataRef.current = val;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
@@ -145,7 +151,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const lines = uris.trim().split("\n");
|
const lines = uris.trim().split("\n");
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const batchSize = 50;
|
const batchSize = 50;
|
||||||
function parseBatch() {
|
let parseTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const parseBatch = () => {
|
||||||
const end = Math.min(idx + batchSize, lines.length);
|
const end = Math.min(idx + batchSize, lines.length);
|
||||||
for (; idx < end; idx++) {
|
for (; idx < end; idx++) {
|
||||||
const uri = lines[idx];
|
const uri = lines[idx];
|
||||||
@@ -165,14 +173,18 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idx < lines.length) {
|
if (idx < lines.length) {
|
||||||
setTimeout(parseBatch, 0);
|
parseTimer = window.setTimeout(parseBatch, 0);
|
||||||
} else {
|
} else {
|
||||||
|
if (parseTimer) {
|
||||||
|
clearTimeout(parseTimer);
|
||||||
|
parseTimer = undefined;
|
||||||
|
}
|
||||||
cb(proxies);
|
cb(proxies);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
parseBatch();
|
parseBatch();
|
||||||
};
|
};
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid);
|
const data = await readProfileFile(profileUid);
|
||||||
|
|
||||||
const originProxiesObj = yaml.load(data) as {
|
const originProxiesObj = yaml.load(data) as {
|
||||||
@@ -180,9 +192,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
setProxyList(originProxiesObj?.proxies || []);
|
setProxyList(originProxiesObj?.proxies || []);
|
||||||
};
|
}, [profileUid]);
|
||||||
|
|
||||||
const fetchContent = async () => {
|
const fetchContent = useCallback(async () => {
|
||||||
const data = await readProfileFile(property);
|
const data = await readProfileFile(property);
|
||||||
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||||
|
|
||||||
@@ -192,50 +204,61 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
setPrevData(data);
|
setPrevData(data);
|
||||||
setCurrData(data);
|
setCurrData(data);
|
||||||
};
|
}, [property]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currData === "") return;
|
if (currData === "" || visualization !== true) {
|
||||||
if (visualization !== true) return;
|
return;
|
||||||
|
|
||||||
const obj = yaml.load(currData) as {
|
|
||||||
prepend: [];
|
|
||||||
append: [];
|
|
||||||
delete: [];
|
|
||||||
} | null;
|
|
||||||
setPrependSeq(obj?.prepend || []);
|
|
||||||
setAppendSeq(obj?.append || []);
|
|
||||||
setDeleteSeq(obj?.delete || []);
|
|
||||||
}, [visualization]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (prependSeq && appendSeq && deleteSeq) {
|
|
||||||
const serialize = () => {
|
|
||||||
try {
|
|
||||||
setCurrData(
|
|
||||||
yaml.dump(
|
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
|
||||||
{ forceQuotes: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
|
|
||||||
// 防止异常导致UI卡死
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (window.requestIdleCallback) {
|
|
||||||
window.requestIdleCallback(serialize);
|
|
||||||
} else {
|
|
||||||
setTimeout(serialize, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
||||||
|
startTransition(() => {
|
||||||
|
setPrependSeq(obj?.prepend ?? []);
|
||||||
|
setAppendSeq(obj?.append ?? []);
|
||||||
|
setDeleteSeq(obj?.delete ?? []);
|
||||||
|
});
|
||||||
|
}, [currData, visualization]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!(prependSeq && appendSeq && deleteSeq)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialize = () => {
|
||||||
|
try {
|
||||||
|
setCurrData(
|
||||||
|
yaml.dump(
|
||||||
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
|
{ forceQuotes: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
|
||||||
|
// 防止异常导致UI卡死
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let idleId: number | undefined;
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
idleId = window.requestIdleCallback(serialize);
|
||||||
|
} else {
|
||||||
|
timeoutId = window.setTimeout(serialize, 0);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (idleId !== undefined && window.cancelIdleCallback) {
|
||||||
|
window.cancelIdleCallback(idleId);
|
||||||
|
}
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchContent();
|
fetchContent();
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
}, [open]);
|
}, [fetchContent, fetchProfile, open]);
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -357,10 +380,10 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
return x.name;
|
return x.name;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredPrependSeq.map((item, index) => {
|
{filteredPrependSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
key={`${item.name}-${index}`}
|
key={item.name}
|
||||||
type="prepend"
|
type="prepend"
|
||||||
proxy={item}
|
proxy={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
@@ -380,7 +403,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const newIndex = index - shift;
|
const newIndex = index - shift;
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
key={`${filteredProxyList[newIndex].name}-${index}`}
|
key={filteredProxyList[newIndex].name}
|
||||||
type={
|
type={
|
||||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||||
? "delete"
|
? "delete"
|
||||||
@@ -417,10 +440,10 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
return x.name;
|
return x.name;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredAppendSeq.map((item, index) => {
|
{filteredAppendSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
key={`${item.name}-${index}`}
|
key={item.name}
|
||||||
type="append"
|
type="append"
|
||||||
proxy={item}
|
proxy={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
|
|||||||
@@ -21,22 +21,19 @@ export const ProxyItem = (props: Props) => {
|
|||||||
const sortable = type === "prepend" || type === "append";
|
const sortable = type === "prepend" || type === "append";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes: sortableAttributes,
|
||||||
listeners,
|
listeners: sortableListeners,
|
||||||
setNodeRef,
|
setNodeRef: sortableSetNodeRef,
|
||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = sortable
|
} = useSortable({
|
||||||
? useSortable({ id: proxy.name })
|
id: proxy.name,
|
||||||
: {
|
disabled: !sortable,
|
||||||
attributes: {},
|
});
|
||||||
listeners: {},
|
const dragAttributes = sortable ? sortableAttributes : undefined;
|
||||||
setNodeRef: null,
|
const dragListeners = sortable ? sortableListeners : undefined;
|
||||||
transform: null,
|
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
||||||
transition: null,
|
|
||||||
isDragging: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -60,9 +57,9 @@ export const ProxyItem = (props: Props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
{...attributes}
|
{...(dragAttributes ?? {})}
|
||||||
{...listeners}
|
{...(dragListeners ?? {})}
|
||||||
ref={setNodeRef}
|
ref={dragNodeRef}
|
||||||
sx={{ cursor: sortable ? "move" : "" }}
|
sx={{ cursor: sortable ? "move" : "" }}
|
||||||
primary={
|
primary={
|
||||||
<StyledPrimary
|
<StyledPrimary
|
||||||
@@ -86,11 +83,13 @@ export const ProxyItem = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{
|
slotProps={{
|
||||||
sx: {
|
secondary: {
|
||||||
display: "flex",
|
sx: {
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
color: "#ccc",
|
alignItems: "center",
|
||||||
|
color: "#ccc",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -95,11 +95,13 @@ export const RuleItem = (props: Props) => {
|
|||||||
</StyledSubtitle>
|
</StyledSubtitle>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{
|
slotProps={{
|
||||||
sx: {
|
secondary: {
|
||||||
display: "flex",
|
sx: {
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
color: "#ccc",
|
alignItems: "center",
|
||||||
|
color: "#ccc",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,7 +31,13 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
@@ -305,7 +311,7 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const fetchContent = async () => {
|
const fetchContent = useCallback(async () => {
|
||||||
const data = await readProfileFile(property);
|
const data = await readProfileFile(property);
|
||||||
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||||
|
|
||||||
@@ -315,42 +321,57 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
setPrevData(data);
|
setPrevData(data);
|
||||||
setCurrData(data);
|
setCurrData(data);
|
||||||
};
|
}, [property]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currData === "") return;
|
if (currData === "" || visualization !== true) {
|
||||||
if (visualization !== true) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
||||||
setPrependSeq(obj?.prepend || []);
|
startTransition(() => {
|
||||||
setAppendSeq(obj?.append || []);
|
setPrependSeq(obj?.prepend ?? []);
|
||||||
setDeleteSeq(obj?.delete || []);
|
setAppendSeq(obj?.append ?? []);
|
||||||
}, [visualization]);
|
setDeleteSeq(obj?.delete ?? []);
|
||||||
|
});
|
||||||
|
}, [currData, visualization]);
|
||||||
|
|
||||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prependSeq && appendSeq && deleteSeq) {
|
if (!(prependSeq && appendSeq && deleteSeq)) {
|
||||||
const serialize = () => {
|
return;
|
||||||
try {
|
|
||||||
setCurrData(
|
|
||||||
yaml.dump(
|
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
|
||||||
{ forceQuotes: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (window.requestIdleCallback) {
|
|
||||||
window.requestIdleCallback(serialize);
|
|
||||||
} else {
|
|
||||||
setTimeout(serialize, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serialize = () => {
|
||||||
|
try {
|
||||||
|
setCurrData(
|
||||||
|
yaml.dump(
|
||||||
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
|
{ forceQuotes: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let idleId: number | undefined;
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
idleId = window.requestIdleCallback(serialize);
|
||||||
|
} else {
|
||||||
|
timeoutId = window.setTimeout(serialize, 0);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (idleId !== undefined && window.cancelIdleCallback) {
|
||||||
|
window.cancelIdleCallback(idleId);
|
||||||
|
}
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq]);
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid); // 原配置文件
|
const data = await readProfileFile(profileUid); // 原配置文件
|
||||||
const groupsData = await readProfileFile(groupsUid); // groups配置文件
|
const groupsData = await readProfileFile(groupsUid); // groups配置文件
|
||||||
const mergeData = await readProfileFile(mergeUid); // merge配置文件
|
const mergeData = await readProfileFile(mergeUid); // merge配置文件
|
||||||
@@ -358,13 +379,25 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
const rulesObj = yaml.load(data) as { rules: [] } | null;
|
const rulesObj = yaml.load(data) as { rules: [] } | null;
|
||||||
|
|
||||||
const originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null;
|
const originGroupsObj = yaml.load(data) as {
|
||||||
|
"proxy-groups": IProxyGroupConfig[];
|
||||||
|
} | null;
|
||||||
const originGroups = originGroupsObj?.["proxy-groups"] || [];
|
const originGroups = originGroupsObj?.["proxy-groups"] || [];
|
||||||
const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
|
const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
|
||||||
const morePrependGroups = moreGroupsObj?.["prepend"] || [];
|
const rawPrependGroups = moreGroupsObj?.["prepend"];
|
||||||
const moreAppendGroups = moreGroupsObj?.["append"] || [];
|
const morePrependGroups = Array.isArray(rawPrependGroups)
|
||||||
const moreDeleteGroups =
|
? (rawPrependGroups as IProxyGroupConfig[])
|
||||||
moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]);
|
: [];
|
||||||
|
const rawAppendGroups = moreGroupsObj?.["append"];
|
||||||
|
const moreAppendGroups = Array.isArray(rawAppendGroups)
|
||||||
|
? (rawAppendGroups as IProxyGroupConfig[])
|
||||||
|
: [];
|
||||||
|
const rawDeleteGroups = moreGroupsObj?.["delete"];
|
||||||
|
const moreDeleteGroups: Array<string | { name: string }> = Array.isArray(
|
||||||
|
rawDeleteGroups,
|
||||||
|
)
|
||||||
|
? (rawDeleteGroups as Array<string | { name: string }>)
|
||||||
|
: [];
|
||||||
const groups = morePrependGroups.concat(
|
const groups = morePrependGroups.concat(
|
||||||
originGroups.filter((group: any) => {
|
originGroups.filter((group: any) => {
|
||||||
if (group.name) {
|
if (group.name) {
|
||||||
@@ -376,14 +409,16 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
moreAppendGroups,
|
moreAppendGroups,
|
||||||
);
|
);
|
||||||
|
|
||||||
const originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
|
const originRuleSetObj = yaml.load(data) as {
|
||||||
|
"rule-providers": Record<string, unknown>;
|
||||||
|
} | null;
|
||||||
const originRuleSet = originRuleSetObj?.["rule-providers"] || {};
|
const originRuleSet = originRuleSetObj?.["rule-providers"] || {};
|
||||||
const moreRuleSetObj = yaml.load(mergeData) as {
|
const moreRuleSetObj = yaml.load(mergeData) as {
|
||||||
"rule-providers": {};
|
"rule-providers": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
|
const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
|
||||||
const globalRuleSetObj = yaml.load(globalMergeData) as {
|
const globalRuleSetObj = yaml.load(globalMergeData) as {
|
||||||
"rule-providers": {};
|
"rule-providers": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
|
const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
|
||||||
const ruleSet = Object.assign(
|
const ruleSet = Object.assign(
|
||||||
@@ -393,12 +428,16 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
globalRuleSet,
|
globalRuleSet,
|
||||||
);
|
);
|
||||||
|
|
||||||
const originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null;
|
const originSubRuleObj = yaml.load(data) as {
|
||||||
|
"sub-rules": Record<string, unknown>;
|
||||||
|
} | null;
|
||||||
const originSubRule = originSubRuleObj?.["sub-rules"] || {};
|
const originSubRule = originSubRuleObj?.["sub-rules"] || {};
|
||||||
const moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null;
|
const moreSubRuleObj = yaml.load(mergeData) as {
|
||||||
|
"sub-rules": Record<string, unknown>;
|
||||||
|
} | null;
|
||||||
const moreSubRule = moreSubRuleObj?.["sub-rules"] || {};
|
const moreSubRule = moreSubRuleObj?.["sub-rules"] || {};
|
||||||
const globalSubRuleObj = yaml.load(globalMergeData) as {
|
const globalSubRuleObj = yaml.load(globalMergeData) as {
|
||||||
"sub-rules": {};
|
"sub-rules": Record<string, unknown>;
|
||||||
} | null;
|
} | null;
|
||||||
const globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
|
const globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
|
||||||
const subRule = Object.assign(
|
const subRule = Object.assign(
|
||||||
@@ -413,13 +452,13 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
setRuleSetList(Object.keys(ruleSet));
|
setRuleSetList(Object.keys(ruleSet));
|
||||||
setSubRuleList(Object.keys(subRule));
|
setSubRuleList(Object.keys(subRule));
|
||||||
setRuleList(rulesObj?.rules || []);
|
setRuleList(rulesObj?.rules || []);
|
||||||
};
|
}, [groupsUid, mergeUid, profileUid]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchContent();
|
fetchContent();
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
}, [open]);
|
}, [fetchContent, fetchProfile, open]);
|
||||||
|
|
||||||
const validateRule = () => {
|
const validateRule = () => {
|
||||||
if ((ruleType.required ?? true) && !ruleContent) {
|
if ((ruleType.required ?? true) && !ruleContent) {
|
||||||
@@ -626,10 +665,10 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
return x;
|
return x;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredPrependSeq.map((item, index) => {
|
{filteredPrependSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<RuleItem
|
<RuleItem
|
||||||
key={`${item}-${index}`}
|
key={item}
|
||||||
type="prepend"
|
type="prepend"
|
||||||
ruleRaw={item}
|
ruleRaw={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
@@ -647,7 +686,7 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
const newIndex = index - shift;
|
const newIndex = index - shift;
|
||||||
return (
|
return (
|
||||||
<RuleItem
|
<RuleItem
|
||||||
key={`${filteredRuleList[newIndex]}-${index}`}
|
key={filteredRuleList[newIndex]}
|
||||||
type={
|
type={
|
||||||
deleteSeq.includes(filteredRuleList[newIndex])
|
deleteSeq.includes(filteredRuleList[newIndex])
|
||||||
? "delete"
|
? "delete"
|
||||||
@@ -682,10 +721,10 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
return x;
|
return x;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredAppendSeq.map((item, index) => {
|
{filteredAppendSeq.map((item) => {
|
||||||
return (
|
return (
|
||||||
<RuleItem
|
<RuleItem
|
||||||
key={`${item}-${index}`}
|
key={item}
|
||||||
type="append"
|
type="append"
|
||||||
ruleRaw={item}
|
ruleRaw={item}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user