refactor: improve code readability and consistency in proxy-chain and uri-parser utilities
refactor: add keys to icons in routers for improved rendering and performance refactor: optimize RegExp polyfill by using Object.prototype.hasOwnProperty.call refactor: reorder imports in chain-proxy-provider for consistency refactor: remove unused "obfs-opts" property from IProxySnellConfig interface refactor: reorganize imports and enhance refresh logic in app data provider refactor: re-enable prop-types linting for better type safety in BaseDialog component refactor: update dependencies in effect hooks for improved stability and performance
This commit is contained in:
@@ -30,49 +30,49 @@ export const routers = [
|
||||
{
|
||||
label: "Label-Home",
|
||||
path: "/home",
|
||||
icon: [<HomeRoundedIcon />, <HomeSvg />],
|
||||
icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Proxies",
|
||||
path: "/",
|
||||
icon: [<WifiRoundedIcon />, <ProxiesSvg />],
|
||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||
element: <ProxiesPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Profiles",
|
||||
path: "/profile",
|
||||
icon: [<DnsRoundedIcon />, <ProfilesSvg />],
|
||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||
element: <ProfilesPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Connections",
|
||||
path: "/connections",
|
||||
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />],
|
||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||
element: <ConnectionsPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Rules",
|
||||
path: "/rules",
|
||||
icon: [<ForkRightRoundedIcon />, <RulesSvg />],
|
||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||
element: <RulesPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Logs",
|
||||
path: "/logs",
|
||||
icon: [<SubjectRoundedIcon />, <LogsSvg />],
|
||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||
element: <LogsPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Unlock",
|
||||
path: "/unlock",
|
||||
icon: [<LockOpenRoundedIcon />, <UnlockSvg />],
|
||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||
element: <UnlockPage />,
|
||||
},
|
||||
{
|
||||
label: "Label-Settings",
|
||||
path: "/settings",
|
||||
icon: [<SettingsRoundedIcon />, <SettingsSvg />],
|
||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||
element: <SettingsPage />,
|
||||
},
|
||||
].map((router) => ({
|
||||
|
||||
@@ -46,17 +46,21 @@ const ConnectionsPage = () => {
|
||||
|
||||
const isTableLayout = setting.layout === "table";
|
||||
|
||||
const orderOpts: Record<string, OrderFunc> = {
|
||||
Default: (list) =>
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
new Date(b.start || "0").getTime()! -
|
||||
new Date(a.start || "0").getTime()!,
|
||||
),
|
||||
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
"Download Speed": (list) =>
|
||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||
};
|
||||
const orderOpts = useMemo<Record<string, OrderFunc>>(
|
||||
() => ({
|
||||
Default: (list) =>
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
new Date(b.start || "0").getTime()! -
|
||||
new Date(a.start || "0").getTime()!,
|
||||
),
|
||||
"Upload Speed": (list) =>
|
||||
list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
"Download Speed": (list) =>
|
||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
||||
@@ -94,7 +98,7 @@ const ConnectionsPage = () => {
|
||||
if (orderFunc) conns = orderFunc(conns);
|
||||
|
||||
return [conns];
|
||||
}, [displayData, match, curOrderOpt]);
|
||||
}, [displayData, match, curOrderOpt, orderOpts]);
|
||||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import {
|
||||
RouterOutlined,
|
||||
SettingsOutlined,
|
||||
DnsOutlined,
|
||||
SpeedOutlined,
|
||||
HelpOutlineRounded,
|
||||
HistoryEduOutlined,
|
||||
RouterOutlined,
|
||||
SettingsOutlined,
|
||||
SpeedOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Grid,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useState, useMemo, Suspense, lazy, useCallback } from "react";
|
||||
import { Suspense, lazy, useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BasePage } from "@/components/base";
|
||||
@@ -264,7 +264,7 @@ const HomePage = () => {
|
||||
renderCard("network", <NetworkSettingsCard />),
|
||||
renderCard("mode", <ClashModeEnhancedCard />),
|
||||
],
|
||||
[homeCards, current, mutateProfiles, renderCard],
|
||||
[current, mutateProfiles, renderCard],
|
||||
);
|
||||
|
||||
// 新增:保存设置时用requestIdleCallback/setTimeout
|
||||
@@ -314,7 +314,7 @@ const HomePage = () => {
|
||||
</Suspense>,
|
||||
),
|
||||
],
|
||||
[homeCards, t, renderCard],
|
||||
[t, renderCard],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
@@ -19,14 +19,13 @@ import {
|
||||
TextSnippetOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { TauriEvent } from "@tauri-apps/api/event";
|
||||
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
|
||||
import { listen, TauriEvent } from "@tauri-apps/api/event";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { throttle } from "lodash-es";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useSWR, { mutate } from "swr";
|
||||
@@ -42,17 +41,17 @@ import {
|
||||
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import {
|
||||
importProfile,
|
||||
closeAllConnections,
|
||||
createProfile,
|
||||
deleteProfile,
|
||||
enhanceProfiles,
|
||||
getProfiles,
|
||||
//restartCore,
|
||||
getRuntimeLogs,
|
||||
deleteProfile,
|
||||
updateProfile,
|
||||
importProfile,
|
||||
reorderProfile,
|
||||
createProfile,
|
||||
getProfiles,
|
||||
updateProfile,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useSetLoadingCache, useThemeMode } from "@/services/states";
|
||||
@@ -117,41 +116,44 @@ const ProfilePage = () => {
|
||||
const pendingRequestRef = useRef<Promise<any> | null>(null);
|
||||
|
||||
// 处理profile切换中断
|
||||
const handleProfileInterrupt = (
|
||||
previousSwitching: string,
|
||||
newProfile: string,
|
||||
) => {
|
||||
debugProfileSwitch(
|
||||
"INTERRUPT_PREVIOUS",
|
||||
previousSwitching,
|
||||
`被 ${newProfile} 中断`,
|
||||
);
|
||||
const handleProfileInterrupt = useCallback(
|
||||
(previousSwitching: string, newProfile: string) => {
|
||||
debugProfileSwitch(
|
||||
"INTERRUPT_PREVIOUS",
|
||||
previousSwitching,
|
||||
`被 ${newProfile} 中断`,
|
||||
);
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
|
||||
}
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
|
||||
}
|
||||
|
||||
if (pendingRequestRef.current) {
|
||||
debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
|
||||
}
|
||||
if (pendingRequestRef.current) {
|
||||
debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
|
||||
}
|
||||
|
||||
setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
|
||||
showNotice(
|
||||
"info",
|
||||
`${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
|
||||
3000,
|
||||
);
|
||||
};
|
||||
setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
|
||||
showNotice(
|
||||
"info",
|
||||
`${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
|
||||
3000,
|
||||
);
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// 清理切换状态
|
||||
const cleanupSwitchState = (profile: string, sequence: number) => {
|
||||
setActivatings((prev) => prev.filter((id) => id !== profile));
|
||||
switchingProfileRef.current = null;
|
||||
abortControllerRef.current = null;
|
||||
pendingRequestRef.current = null;
|
||||
debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
|
||||
};
|
||||
const cleanupSwitchState = useCallback(
|
||||
(profile: string, sequence: number) => {
|
||||
setActivatings((prev) => prev.filter((id) => id !== profile));
|
||||
switchingProfileRef.current = null;
|
||||
abortControllerRef.current = null;
|
||||
pendingRequestRef.current = null;
|
||||
debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -160,6 +162,15 @@ const ProfilePage = () => {
|
||||
);
|
||||
const { current } = location.state || {};
|
||||
|
||||
const {
|
||||
profiles = {},
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
mutateProfiles,
|
||||
error,
|
||||
isStale,
|
||||
} = useProfiles();
|
||||
|
||||
useEffect(() => {
|
||||
const handleFileDrop = async () => {
|
||||
const unlisten = await addListener(
|
||||
@@ -197,16 +208,7 @@ const ProfilePage = () => {
|
||||
return () => {
|
||||
unsubscribe.then((cleanup) => cleanup());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
profiles = {},
|
||||
activateSelected,
|
||||
patchProfiles,
|
||||
mutateProfiles,
|
||||
error,
|
||||
isStale,
|
||||
} = useProfiles();
|
||||
}, [addListener, mutateProfiles, t]);
|
||||
|
||||
// 添加紧急恢复功能
|
||||
const onEmergencyRefresh = useLockFn(async () => {
|
||||
@@ -380,151 +382,167 @@ const ProfilePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const executeBackgroundTasks = async (
|
||||
profile: string,
|
||||
sequence: number,
|
||||
abortController: AbortController,
|
||||
) => {
|
||||
try {
|
||||
if (
|
||||
sequence === requestSequenceRef.current &&
|
||||
switchingProfileRef.current === profile &&
|
||||
!abortController.signal.aborted
|
||||
) {
|
||||
await activateSelected();
|
||||
console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"BACKGROUND_TASK_SKIPPED",
|
||||
profile,
|
||||
`序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn("Failed to activate selected proxies:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const activateProfile = async (profile: string, notifySuccess: boolean) => {
|
||||
if (profiles.current === profile && !notifySuccess) {
|
||||
console.log(`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSequence = ++requestSequenceRef.current;
|
||||
debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
// 处理中断逻辑
|
||||
const previousSwitching = switchingProfileRef.current;
|
||||
if (previousSwitching && previousSwitching !== profile) {
|
||||
handleProfileInterrupt(previousSwitching, profile);
|
||||
}
|
||||
|
||||
// 防止重复切换同一个profile
|
||||
if (switchingProfileRef.current === profile) {
|
||||
debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化切换状态
|
||||
switchingProfileRef.current = profile;
|
||||
debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
const currentAbortController = new AbortController();
|
||||
abortControllerRef.current = currentAbortController;
|
||||
|
||||
setActivatings((prev) => {
|
||||
if (prev.includes(profile)) return prev;
|
||||
return [...prev, profile];
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`,
|
||||
);
|
||||
|
||||
// 检查请求有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行切换请求
|
||||
const requestPromise = patchProfiles(
|
||||
{ current: profile },
|
||||
currentAbortController.signal,
|
||||
);
|
||||
pendingRequestRef.current = requestPromise;
|
||||
|
||||
const success = await requestPromise;
|
||||
|
||||
if (pendingRequestRef.current === requestPromise) {
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
|
||||
// 再次检查有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 完成切换
|
||||
await mutateLogs();
|
||||
closeAllConnections();
|
||||
|
||||
if (notifySuccess && success) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`,
|
||||
);
|
||||
|
||||
// 延迟执行后台任务
|
||||
setTimeout(
|
||||
() =>
|
||||
executeBackgroundTasks(
|
||||
const executeBackgroundTasks = useCallback(
|
||||
async (
|
||||
profile: string,
|
||||
sequence: number,
|
||||
abortController: AbortController,
|
||||
) => {
|
||||
try {
|
||||
if (
|
||||
sequence === requestSequenceRef.current &&
|
||||
switchingProfileRef.current === profile &&
|
||||
!abortController.signal.aborted
|
||||
) {
|
||||
await activateSelected();
|
||||
console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"BACKGROUND_TASK_SKIPPED",
|
||||
profile,
|
||||
currentSequence,
|
||||
currentAbortController,
|
||||
),
|
||||
50,
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (pendingRequestRef.current) {
|
||||
pendingRequestRef.current = null;
|
||||
`序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn("Failed to activate selected proxies:", err);
|
||||
}
|
||||
},
|
||||
[activateSelected],
|
||||
);
|
||||
|
||||
// 检查是否因为中断或过期而出错
|
||||
if (
|
||||
isOperationAborted(currentAbortController, profile) ||
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile)
|
||||
) {
|
||||
const activateProfile = useCallback(
|
||||
async (profile: string, notifySuccess: boolean) => {
|
||||
if (profiles.current === profile && !notifySuccess) {
|
||||
console.log(
|
||||
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[Profile] 切换失败:`, err);
|
||||
showNotice("error", err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
// 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
|
||||
if (
|
||||
switchingProfileRef.current === profile &&
|
||||
currentSequence === requestSequenceRef.current
|
||||
) {
|
||||
cleanupSwitchState(profile, currentSequence);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"CLEANUP_SKIPPED",
|
||||
profile,
|
||||
`序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
const currentSequence = ++requestSequenceRef.current;
|
||||
debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
// 处理中断逻辑
|
||||
const previousSwitching = switchingProfileRef.current;
|
||||
if (previousSwitching && previousSwitching !== profile) {
|
||||
handleProfileInterrupt(previousSwitching, profile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 防止重复切换同一个profile
|
||||
if (switchingProfileRef.current === profile) {
|
||||
debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化切换状态
|
||||
switchingProfileRef.current = profile;
|
||||
debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
const currentAbortController = new AbortController();
|
||||
abortControllerRef.current = currentAbortController;
|
||||
|
||||
setActivatings((prev) => {
|
||||
if (prev.includes(profile)) return prev;
|
||||
return [...prev, profile];
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`,
|
||||
);
|
||||
|
||||
// 检查请求有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行切换请求
|
||||
const requestPromise = patchProfiles(
|
||||
{ current: profile },
|
||||
currentAbortController.signal,
|
||||
);
|
||||
pendingRequestRef.current = requestPromise;
|
||||
|
||||
const success = await requestPromise;
|
||||
|
||||
if (pendingRequestRef.current === requestPromise) {
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
|
||||
// 再次检查有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 完成切换
|
||||
await mutateLogs();
|
||||
closeAllConnections();
|
||||
|
||||
if (notifySuccess && success) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`,
|
||||
);
|
||||
|
||||
// 延迟执行后台任务
|
||||
setTimeout(
|
||||
() =>
|
||||
executeBackgroundTasks(
|
||||
profile,
|
||||
currentSequence,
|
||||
currentAbortController,
|
||||
),
|
||||
50,
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (pendingRequestRef.current) {
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
|
||||
// 检查是否因为中断或过期而出错
|
||||
if (
|
||||
isOperationAborted(currentAbortController, profile) ||
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[Profile] 切换失败:`, err);
|
||||
showNotice("error", err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
// 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
|
||||
if (
|
||||
switchingProfileRef.current === profile &&
|
||||
currentSequence === requestSequenceRef.current
|
||||
) {
|
||||
cleanupSwitchState(profile, currentSequence);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"CLEANUP_SKIPPED",
|
||||
profile,
|
||||
`序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
profiles,
|
||||
patchProfiles,
|
||||
mutateLogs,
|
||||
t,
|
||||
executeBackgroundTasks,
|
||||
handleProfileInterrupt,
|
||||
cleanupSwitchState,
|
||||
],
|
||||
);
|
||||
const onSelect = async (current: string, force: boolean) => {
|
||||
// 阻止重复点击或已激活的profile
|
||||
if (switchingProfileRef.current === current) {
|
||||
@@ -547,7 +565,7 @@ const ProfilePage = () => {
|
||||
await activateProfile(current, false);
|
||||
}
|
||||
})();
|
||||
}, current);
|
||||
}, [current, activateProfile, mutateProfiles]);
|
||||
|
||||
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
|
||||
if (switchingProfileRef.current) {
|
||||
@@ -583,7 +601,9 @@ const ProfilePage = () => {
|
||||
await deleteProfile(uid);
|
||||
mutateProfiles();
|
||||
mutateLogs();
|
||||
current && (await onEnhance(false));
|
||||
if (current) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, ButtonGroup } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
getRuntimeProxyChainConfig,
|
||||
patchClashMode,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -44,7 +44,7 @@ const ProxyPage = () => {
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
const modeList = ["rule", "global", "direct"];
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"], []);
|
||||
|
||||
const curMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
@@ -100,7 +100,7 @@ const ProxyPage = () => {
|
||||
if (curMode && !modeList.includes(curMode)) {
|
||||
onChangeMode("rule");
|
||||
}
|
||||
}, [curMode]);
|
||||
}, [curMode, modeList, onChangeMode]);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { Box, Button, Grid } from "@mui/material";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// test icons
|
||||
@@ -39,32 +39,36 @@ const TestPage = () => {
|
||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||
|
||||
// test list
|
||||
const testList = verge?.test_list ?? [
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: apple,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: github,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: google,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Youtube",
|
||||
url: "https://www.youtube.com",
|
||||
icon: youtube,
|
||||
},
|
||||
];
|
||||
const testList = useMemo(
|
||||
() =>
|
||||
verge?.test_list ?? [
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: apple,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: github,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: google,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Youtube",
|
||||
url: "https://www.youtube.com",
|
||||
icon: youtube,
|
||||
},
|
||||
],
|
||||
[verge],
|
||||
);
|
||||
|
||||
const onTestListItemChange = (
|
||||
uid: string,
|
||||
@@ -117,7 +121,7 @@ const TestPage = () => {
|
||||
if (!verge?.test_list) {
|
||||
patchVerge({ test_list: testList });
|
||||
}
|
||||
}, [verge]);
|
||||
}, [verge, patchVerge, testList]);
|
||||
|
||||
const viewerRef = useRef<TestViewerRef>(null);
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
AccessTimeOutlined,
|
||||
CancelOutlined,
|
||||
CheckCircleOutlined,
|
||||
HelpOutline,
|
||||
PendingOutlined,
|
||||
RefreshRounded,
|
||||
AccessTimeOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Grid,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BasePage, BaseEmpty } from "@/components/base";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
interface UnlockItem {
|
||||
@@ -45,9 +45,9 @@ const UnlockPage = () => {
|
||||
const [isCheckingAll, setIsCheckingAll] = useState(false);
|
||||
const [loadingItems, setLoadingItems] = useState<string[]>([]);
|
||||
|
||||
const sortItemsByName = (items: UnlockItem[]) => {
|
||||
const sortItemsByName = useCallback((items: UnlockItem[]) => {
|
||||
return [...items].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 保存测试结果到本地存储
|
||||
const saveResultsToStorage = (items: UnlockItem[], time: string | null) => {
|
||||
@@ -82,6 +82,22 @@ const UnlockPage = () => {
|
||||
return { items: null, time: null };
|
||||
};
|
||||
|
||||
const getUnlockItems = useCallback(
|
||||
async (updateUI: boolean = true) => {
|
||||
try {
|
||||
const items = await invoke<UnlockItem[]>("get_unlock_items");
|
||||
const sortedItems = sortItemsByName(items);
|
||||
|
||||
if (updateUI) {
|
||||
setUnlockItems(sortedItems);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to get unlock items:", err);
|
||||
}
|
||||
},
|
||||
[sortItemsByName],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { items: storedItems } = loadResultsFromStorage();
|
||||
|
||||
@@ -91,20 +107,7 @@ const UnlockPage = () => {
|
||||
} else {
|
||||
getUnlockItems(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getUnlockItems = async (updateUI: boolean = true) => {
|
||||
try {
|
||||
const items = await invoke<UnlockItem[]>("get_unlock_items");
|
||||
const sortedItems = sortItemsByName(items);
|
||||
|
||||
if (updateUI) {
|
||||
setUnlockItems(sortedItems);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to get unlock items:", err);
|
||||
}
|
||||
};
|
||||
}, [getUnlockItems]);
|
||||
|
||||
const invokeWithTimeout = async <T,>(
|
||||
cmd: string,
|
||||
|
||||
Reference in New Issue
Block a user