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:
Tunglies
2025-09-20 02:49:11 +08:00
parent 7811714f89
commit d9a5c11d6a
15 changed files with 409 additions and 349 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { import {
Button, Button,

View File

@@ -1,19 +1,19 @@
import { import {
DndContext,
closestCenter, closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
DragEndEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { import {
arrayMove, arrayMove,
SortableContext, SortableContext,
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy, verticalListSortingStrategy,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { import {
Delete as DeleteIcon, Delete as DeleteIcon,
@@ -22,25 +22,25 @@ import {
LinkOff, LinkOff,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { import {
Alert,
Box, Box,
Button,
Chip,
IconButton,
Paper, Paper,
Typography, Typography,
IconButton,
Chip,
Alert,
useTheme, useTheme,
Button,
} from "@mui/material"; } from "@mui/material";
import { useState, useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useSWR from "swr"; import useSWR from "swr";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
import { import {
updateProxyChainConfigInRuntime,
updateProxyAndSync,
getProxies,
closeAllConnections, closeAllConnections,
getProxies,
updateProxyAndSync,
updateProxyChainConfigInRuntime,
} from "@/services/cmds"; } from "@/services/cmds";
interface ProxyChainItem { interface ProxyChainItem {

View File

@@ -30,49 +30,49 @@ export const routers = [
{ {
label: "Label-Home", label: "Label-Home",
path: "/home", path: "/home",
icon: [<HomeRoundedIcon />, <HomeSvg />], icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
element: <HomePage />, element: <HomePage />,
}, },
{ {
label: "Label-Proxies", label: "Label-Proxies",
path: "/", path: "/",
icon: [<WifiRoundedIcon />, <ProxiesSvg />], icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
element: <ProxiesPage />, element: <ProxiesPage />,
}, },
{ {
label: "Label-Profiles", label: "Label-Profiles",
path: "/profile", path: "/profile",
icon: [<DnsRoundedIcon />, <ProfilesSvg />], icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
element: <ProfilesPage />, element: <ProfilesPage />,
}, },
{ {
label: "Label-Connections", label: "Label-Connections",
path: "/connections", path: "/connections",
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />], icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
element: <ConnectionsPage />, element: <ConnectionsPage />,
}, },
{ {
label: "Label-Rules", label: "Label-Rules",
path: "/rules", path: "/rules",
icon: [<ForkRightRoundedIcon />, <RulesSvg />], icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
element: <RulesPage />, element: <RulesPage />,
}, },
{ {
label: "Label-Logs", label: "Label-Logs",
path: "/logs", path: "/logs",
icon: [<SubjectRoundedIcon />, <LogsSvg />], icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
element: <LogsPage />, element: <LogsPage />,
}, },
{ {
label: "Label-Unlock", label: "Label-Unlock",
path: "/unlock", path: "/unlock",
icon: [<LockOpenRoundedIcon />, <UnlockSvg />], icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
element: <UnlockPage />, element: <UnlockPage />,
}, },
{ {
label: "Label-Settings", label: "Label-Settings",
path: "/settings", path: "/settings",
icon: [<SettingsRoundedIcon />, <SettingsSvg />], icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
element: <SettingsPage />, element: <SettingsPage />,
}, },
].map((router) => ({ ].map((router) => ({

View File

@@ -46,17 +46,21 @@ const ConnectionsPage = () => {
const isTableLayout = setting.layout === "table"; const isTableLayout = setting.layout === "table";
const orderOpts: Record<string, OrderFunc> = { const orderOpts = useMemo<Record<string, OrderFunc>>(
Default: (list) => () => ({
list.sort( Default: (list) =>
(a, b) => list.sort(
new Date(b.start || "0").getTime()! - (a, b) =>
new Date(a.start || "0").getTime()!, 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) => "Upload Speed": (list) =>
list.sort((a, b) => b.curDownload! - a.curDownload!), 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 [isPaused, setIsPaused] = useState(false);
const [frozenData, setFrozenData] = useState<IConnections | null>(null); const [frozenData, setFrozenData] = useState<IConnections | null>(null);
@@ -94,7 +98,7 @@ const ConnectionsPage = () => {
if (orderFunc) conns = orderFunc(conns); if (orderFunc) conns = orderFunc(conns);
return [conns]; return [conns];
}, [displayData, match, curOrderOpt]); }, [displayData, match, curOrderOpt, orderOpts]);
const onCloseAll = useLockFn(closeAllConnections); const onCloseAll = useLockFn(closeAllConnections);

View File

@@ -1,28 +1,28 @@
import { import {
RouterOutlined,
SettingsOutlined,
DnsOutlined, DnsOutlined,
SpeedOutlined,
HelpOutlineRounded, HelpOutlineRounded,
HistoryEduOutlined, HistoryEduOutlined,
RouterOutlined,
SettingsOutlined,
SpeedOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { import {
Box, Box,
Button, Button,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormGroup,
FormControlLabel,
Checkbox, Checkbox,
Tooltip, Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
FormGroup,
Grid, Grid,
IconButton,
Skeleton, Skeleton,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; 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 { useTranslation } from "react-i18next";
import { BasePage } from "@/components/base"; import { BasePage } from "@/components/base";
@@ -264,7 +264,7 @@ const HomePage = () => {
renderCard("network", <NetworkSettingsCard />), renderCard("network", <NetworkSettingsCard />),
renderCard("mode", <ClashModeEnhancedCard />), renderCard("mode", <ClashModeEnhancedCard />),
], ],
[homeCards, current, mutateProfiles, renderCard], [current, mutateProfiles, renderCard],
); );
// 新增保存设置时用requestIdleCallback/setTimeout // 新增保存设置时用requestIdleCallback/setTimeout
@@ -314,7 +314,7 @@ const HomePage = () => {
</Suspense>, </Suspense>,
), ),
], ],
[homeCards, t, renderCard], [t, renderCard],
); );
return ( return (

View File

@@ -1,11 +1,11 @@
import { import {
DndContext,
closestCenter, closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
DragEndEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { import {
SortableContext, SortableContext,
@@ -19,14 +19,13 @@ import {
TextSnippetOutlined, TextSnippetOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material"; import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
import { listen } from "@tauri-apps/api/event"; import { listen, TauriEvent } from "@tauri-apps/api/event";
import { TauriEvent } from "@tauri-apps/api/event";
import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile } from "@tauri-apps/plugin-fs"; import { readTextFile } from "@tauri-apps/plugin-fs";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { throttle } from "lodash-es"; 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 { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
@@ -42,17 +41,17 @@ import {
import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { useListen } from "@/hooks/use-listen"; import { useListen } from "@/hooks/use-listen";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { closeAllConnections } from "@/services/cmds";
import { import {
importProfile, closeAllConnections,
createProfile,
deleteProfile,
enhanceProfiles, enhanceProfiles,
getProfiles,
//restartCore, //restartCore,
getRuntimeLogs, getRuntimeLogs,
deleteProfile, importProfile,
updateProfile,
reorderProfile, reorderProfile,
createProfile, updateProfile,
getProfiles,
} from "@/services/cmds"; } from "@/services/cmds";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { useSetLoadingCache, useThemeMode } from "@/services/states"; import { useSetLoadingCache, useThemeMode } from "@/services/states";
@@ -117,41 +116,44 @@ const ProfilePage = () => {
const pendingRequestRef = useRef<Promise<any> | null>(null); const pendingRequestRef = useRef<Promise<any> | null>(null);
// 处理profile切换中断 // 处理profile切换中断
const handleProfileInterrupt = ( const handleProfileInterrupt = useCallback(
previousSwitching: string, (previousSwitching: string, newProfile: string) => {
newProfile: string, debugProfileSwitch(
) => { "INTERRUPT_PREVIOUS",
debugProfileSwitch( previousSwitching,
"INTERRUPT_PREVIOUS", `${newProfile} 中断`,
previousSwitching, );
`${newProfile} 中断`,
);
if (abortControllerRef.current) { if (abortControllerRef.current) {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching); debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
} }
if (pendingRequestRef.current) { if (pendingRequestRef.current) {
debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching); debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
} }
setActivatings((prev) => prev.filter((id) => id !== previousSwitching)); setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
showNotice( showNotice(
"info", "info",
`${t("Profile switch interrupted by new selection")}: ${previousSwitching}${newProfile}`, `${t("Profile switch interrupted by new selection")}: ${previousSwitching}${newProfile}`,
3000, 3000,
); );
}; },
[t],
);
// 清理切换状态 // 清理切换状态
const cleanupSwitchState = (profile: string, sequence: number) => { const cleanupSwitchState = useCallback(
setActivatings((prev) => prev.filter((id) => id !== profile)); (profile: string, sequence: number) => {
switchingProfileRef.current = null; setActivatings((prev) => prev.filter((id) => id !== profile));
abortControllerRef.current = null; switchingProfileRef.current = null;
pendingRequestRef.current = null; abortControllerRef.current = null;
debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`); pendingRequestRef.current = null;
}; debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
},
[],
);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
@@ -160,6 +162,15 @@ const ProfilePage = () => {
); );
const { current } = location.state || {}; const { current } = location.state || {};
const {
profiles = {},
activateSelected,
patchProfiles,
mutateProfiles,
error,
isStale,
} = useProfiles();
useEffect(() => { useEffect(() => {
const handleFileDrop = async () => { const handleFileDrop = async () => {
const unlisten = await addListener( const unlisten = await addListener(
@@ -197,16 +208,7 @@ const ProfilePage = () => {
return () => { return () => {
unsubscribe.then((cleanup) => cleanup()); unsubscribe.then((cleanup) => cleanup());
}; };
}, []); }, [addListener, mutateProfiles, t]);
const {
profiles = {},
activateSelected,
patchProfiles,
mutateProfiles,
error,
isStale,
} = useProfiles();
// 添加紧急恢复功能 // 添加紧急恢复功能
const onEmergencyRefresh = useLockFn(async () => { const onEmergencyRefresh = useLockFn(async () => {
@@ -380,151 +382,167 @@ const ProfilePage = () => {
} }
}; };
const executeBackgroundTasks = async ( const executeBackgroundTasks = useCallback(
profile: string, async (
sequence: number, profile: string,
abortController: AbortController, sequence: number,
) => { abortController: AbortController,
try { ) => {
if ( try {
sequence === requestSequenceRef.current && if (
switchingProfileRef.current === profile && sequence === requestSequenceRef.current &&
!abortController.signal.aborted switchingProfileRef.current === profile &&
) { !abortController.signal.aborted
await activateSelected(); ) {
console.log(`[Profile] 后台处理完成,序列号: ${sequence}`); await activateSelected();
} else { console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
debugProfileSwitch( } else {
"BACKGROUND_TASK_SKIPPED", debugProfileSwitch(
profile, "BACKGROUND_TASK_SKIPPED",
`序列号过期或被中断: ${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(
profile, profile,
currentSequence, `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
currentAbortController, );
), }
50, } catch (err: any) {
); console.warn("Failed to activate selected proxies:", err);
} catch (err: any) {
if (pendingRequestRef.current) {
pendingRequestRef.current = null;
} }
},
[activateSelected],
);
// 检查是否因为中断或过期而出错 const activateProfile = useCallback(
if ( async (profile: string, notifySuccess: boolean) => {
isOperationAborted(currentAbortController, profile) || if (profiles.current === profile && !notifySuccess) {
isRequestOutdated(currentSequence, requestSequenceRef, profile) console.log(
) { `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
);
return; return;
} }
console.error(`[Profile] 切换失败:`, err); const currentSequence = ++requestSequenceRef.current;
showNotice("error", err?.message || err.toString(), 4000); debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
} finally {
// 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态 // 处理中断逻辑
if ( const previousSwitching = switchingProfileRef.current;
switchingProfileRef.current === profile && if (previousSwitching && previousSwitching !== profile) {
currentSequence === requestSequenceRef.current handleProfileInterrupt(previousSwitching, profile);
) {
cleanupSwitchState(profile, currentSequence);
} else {
debugProfileSwitch(
"CLEANUP_SKIPPED",
profile,
`序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
);
} }
}
}; // 防止重复切换同一个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) => { const onSelect = async (current: string, force: boolean) => {
// 阻止重复点击或已激活的profile // 阻止重复点击或已激活的profile
if (switchingProfileRef.current === current) { if (switchingProfileRef.current === current) {
@@ -547,7 +565,7 @@ const ProfilePage = () => {
await activateProfile(current, false); await activateProfile(current, false);
} }
})(); })();
}, current); }, [current, activateProfile, mutateProfiles]);
const onEnhance = useLockFn(async (notifySuccess: boolean) => { const onEnhance = useLockFn(async (notifySuccess: boolean) => {
if (switchingProfileRef.current) { if (switchingProfileRef.current) {
@@ -583,7 +601,9 @@ const ProfilePage = () => {
await deleteProfile(uid); await deleteProfile(uid);
mutateProfiles(); mutateProfiles();
mutateLogs(); mutateLogs();
current && (await onEnhance(false)); if (current) {
await onEnhance(false);
}
} catch (err: any) { } catch (err: any) {
showNotice("error", err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} finally { } finally {

View File

@@ -1,6 +1,6 @@
import { Box, Button, ButtonGroup } from "@mui/material"; import { Box, Button, ButtonGroup } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useSWR from "swr"; import useSWR from "swr";
@@ -12,9 +12,9 @@ import {
closeAllConnections, closeAllConnections,
getClashConfig, getClashConfig,
getRuntimeProxyChainConfig, getRuntimeProxyChainConfig,
patchClashMode,
updateProxyChainConfigInRuntime, updateProxyChainConfigInRuntime,
} from "@/services/cmds"; } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
const ProxyPage = () => { const ProxyPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -44,7 +44,7 @@ const ProxyPage = () => {
const { verge } = useVerge(); const { verge } = useVerge();
const modeList = ["rule", "global", "direct"]; const modeList = useMemo(() => ["rule", "global", "direct"], []);
const curMode = clashConfig?.mode?.toLowerCase(); const curMode = clashConfig?.mode?.toLowerCase();
@@ -100,7 +100,7 @@ const ProxyPage = () => {
if (curMode && !modeList.includes(curMode)) { if (curMode && !modeList.includes(curMode)) {
onChangeMode("rule"); onChangeMode("rule");
} }
}, [curMode]); }, [curMode, modeList, onChangeMode]);
return ( return (
<BasePage <BasePage

View File

@@ -1,11 +1,11 @@
import { import {
DndContext,
closestCenter, closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
DragEndEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { import {
SortableContext, SortableContext,
@@ -14,7 +14,7 @@ import {
import { Box, Button, Grid } from "@mui/material"; import { Box, Button, Grid } from "@mui/material";
import { emit } from "@tauri-apps/api/event"; import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
// test icons // test icons
@@ -39,32 +39,36 @@ const TestPage = () => {
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
// test list // test list
const testList = verge?.test_list ?? [ const testList = useMemo(
{ () =>
uid: nanoid(), verge?.test_list ?? [
name: "Apple", {
url: "https://www.apple.com", uid: nanoid(),
icon: apple, name: "Apple",
}, url: "https://www.apple.com",
{ icon: apple,
uid: nanoid(), },
name: "GitHub", {
url: "https://www.github.com", uid: nanoid(),
icon: github, name: "GitHub",
}, url: "https://www.github.com",
{ icon: github,
uid: nanoid(), },
name: "Google", {
url: "https://www.google.com", uid: nanoid(),
icon: google, name: "Google",
}, url: "https://www.google.com",
{ icon: google,
uid: nanoid(), },
name: "Youtube", {
url: "https://www.youtube.com", uid: nanoid(),
icon: youtube, name: "Youtube",
}, url: "https://www.youtube.com",
]; icon: youtube,
},
],
[verge],
);
const onTestListItemChange = ( const onTestListItemChange = (
uid: string, uid: string,
@@ -117,7 +121,7 @@ const TestPage = () => {
if (!verge?.test_list) { if (!verge?.test_list) {
patchVerge({ test_list: testList }); patchVerge({ test_list: testList });
} }
}, [verge]); }, [verge, patchVerge, testList]);
const viewerRef = useRef<TestViewerRef>(null); const viewerRef = useRef<TestViewerRef>(null);
const [showScrollTop, setShowScrollTop] = useState(false); const [showScrollTop, setShowScrollTop] = useState(false);

View File

@@ -1,30 +1,30 @@
import { import {
CheckCircleOutlined, AccessTimeOutlined,
CancelOutlined, CancelOutlined,
CheckCircleOutlined,
HelpOutline, HelpOutline,
PendingOutlined, PendingOutlined,
RefreshRounded, RefreshRounded,
AccessTimeOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { import {
Box, Box,
Button, Button,
Card, Card,
Divider,
Typography,
Chip, Chip,
Tooltip,
CircularProgress, CircularProgress,
Divider,
Grid,
Tooltip,
Typography,
alpha, alpha,
useTheme, useTheme,
Grid,
} from "@mui/material"; } from "@mui/material";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BasePage, BaseEmpty } from "@/components/base"; import { BaseEmpty, BasePage } from "@/components/base";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
interface UnlockItem { interface UnlockItem {
@@ -45,9 +45,9 @@ const UnlockPage = () => {
const [isCheckingAll, setIsCheckingAll] = useState(false); const [isCheckingAll, setIsCheckingAll] = useState(false);
const [loadingItems, setLoadingItems] = useState<string[]>([]); const [loadingItems, setLoadingItems] = useState<string[]>([]);
const sortItemsByName = (items: UnlockItem[]) => { const sortItemsByName = useCallback((items: UnlockItem[]) => {
return [...items].sort((a, b) => a.name.localeCompare(b.name)); return [...items].sort((a, b) => a.name.localeCompare(b.name));
}; }, []);
// 保存测试结果到本地存储 // 保存测试结果到本地存储
const saveResultsToStorage = (items: UnlockItem[], time: string | null) => { const saveResultsToStorage = (items: UnlockItem[], time: string | null) => {
@@ -82,6 +82,22 @@ const UnlockPage = () => {
return { items: null, time: null }; 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(() => { useEffect(() => {
const { items: storedItems } = loadResultsFromStorage(); const { items: storedItems } = loadResultsFromStorage();
@@ -91,20 +107,7 @@ const UnlockPage = () => {
} else { } else {
getUnlockItems(true); getUnlockItems(true);
} }
}, []); }, [getUnlockItems]);
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);
}
};
const invokeWithTimeout = async <T,>( const invokeWithTimeout = async <T,>(
cmd: string, cmd: string,

View File

@@ -11,13 +11,23 @@
} }
if (flags) { if (flags) {
if (!originalRegExp.prototype.hasOwnProperty("unicodeSets")) { if (
!Object.prototype.hasOwnProperty.call(
originalRegExp.prototype,
"unicodeSets",
)
) {
if (flags.includes("v")) { if (flags.includes("v")) {
flags = flags.replace("v", "u"); flags = flags.replace("v", "u");
} }
} }
if (!originalRegExp.prototype.hasOwnProperty("hasIndices")) { if (
!Object.prototype.hasOwnProperty.call(
originalRegExp.prototype,
"hasIndices",
)
) {
if (flags.includes("d")) { if (flags.includes("d")) {
flags = flags.replace("d", ""); flags = flags.replace("d", "");
} }

View File

@@ -12,20 +12,18 @@ import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility"; import { useVisibility } from "@/hooks/use-visibility";
import { import {
getProxies, forceRefreshProxies,
getRules, getAppUptime,
getClashConfig, getClashConfig,
getConnections,
getMemoryData,
getProxies,
getProxyProviders, getProxyProviders,
getRuleProviders, getRuleProviders,
getConnections, getRules,
getTrafficData,
getMemoryData,
} from "@/services/cmds";
import {
getSystemProxy,
getRunningMode, getRunningMode,
getAppUptime, getSystemProxy,
forceRefreshProxies, getTrafficData,
} from "@/services/cmds"; } from "@/services/cmds";
// 连接速度计算接口 // 连接速度计算接口
@@ -482,7 +480,7 @@ export const AppDataProvider = ({
); );
// 提供统一的刷新方法 // 提供统一的刷新方法
const refreshAll = async () => { const refreshAll = React.useCallback(async () => {
await Promise.all([ await Promise.all([
refreshProxy(), refreshProxy(),
refreshClashConfig(), refreshClashConfig(),
@@ -491,7 +489,14 @@ export const AppDataProvider = ({
refreshProxyProviders(), refreshProxyProviders(),
refreshRuleProviders(), refreshRuleProviders(),
]); ]);
}; }, [
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
]);
// 聚合所有数据 // 聚合所有数据
const value = useMemo(() => { const value = useMemo(() => {
@@ -581,6 +586,7 @@ export const AppDataProvider = ({
refreshSysproxy, refreshSysproxy,
refreshProxyProviders, refreshProxyProviders,
refreshRuleProviders, refreshRuleProviders,
refreshAll,
]); ]);
return ( return (

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useCallback } from "react"; import React, { createContext, useCallback, useContext, useState } from "react";
interface ChainProxyContextType { interface ChainProxyContextType {
isChainMode: boolean; isChainMode: boolean;

View File

@@ -739,7 +739,6 @@ interface IProxySnellConfig extends IProxyBaseConfig {
psk?: string; psk?: string;
udp?: boolean; udp?: boolean;
version?: number; version?: number;
"obfs-opts"?: {};
} }
interface IProxyConfig interface IProxyConfig
extends IProxyBaseConfig, extends IProxyBaseConfig,

View File

@@ -1,3 +1,3 @@
export default function isAsyncFunction(fn: Function): boolean { export default function isAsyncFunction(fn: (...args: any[]) => any): boolean {
return fn.constructor.name === "AsyncFunction"; return fn.constructor.name === "AsyncFunction";
} }

View File

@@ -648,7 +648,7 @@ function URI_VLESS(line: string): IProxyVlessConfig {
function URI_Trojan(line: string): IProxyTrojanConfig { function URI_Trojan(line: string): IProxyTrojanConfig {
line = line.split("trojan://")[1]; line = line.split("trojan://")[1];
let [__, password, server, ___, port, ____, addons = "", name] = const [, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
@@ -656,8 +656,10 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
portNum = 443; portNum = 443;
} }
let password = passwordRaw;
password = decodeURIComponent(password); password = decodeURIComponent(password);
let name = nameRaw;
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(name));
name = decodedName ?? `Trojan ${server}:${portNum}`; name = decodedName ?? `Trojan ${server}:${portNum}`;
@@ -672,8 +674,8 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
let path = ""; let path = "";
for (const addon of addons.split("&")) { for (const addon of addons.split("&")) {
let [key, value] = addon.split("="); const [key, valueRaw] = addon.split("=");
value = decodeURIComponent(value); const value = decodeURIComponent(valueRaw);
switch (key) { switch (key) {
case "type": case "type":
if (["ws", "h2"].includes(value)) { if (["ws", "h2"].includes(value)) {
@@ -704,14 +706,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
proxy["fingerprint"] = value; proxy["fingerprint"] = value;
break; break;
case "encryption": case "encryption":
const encryption = value.split(";"); {
if (encryption.length === 3) { const encryption = value.split(";");
proxy["ss-opts"] = { if (encryption.length === 3) {
enabled: true, proxy["ss-opts"] = {
method: encryption[1], enabled: true,
password: encryption[2], method: encryption[1],
}; password: encryption[2],
};
}
} }
break;
case "client-fingerprint": case "client-fingerprint":
proxy["client-fingerprint"] = value as ClientFingerprint; proxy["client-fingerprint"] = value as ClientFingerprint;
break; break;
@@ -736,17 +741,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
function URI_Hysteria2(line: string): IProxyHysteria2Config { function URI_Hysteria2(line: string): IProxyHysteria2Config {
line = line.split(/(hysteria2|hy2):\/\//)[2]; line = line.split(/(hysteria2|hy2):\/\//)[2];
let [__, password, server, ___, port, ____, addons = "", name] = const [, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
password = decodeURIComponent(password); const password = decodeURIComponent(passwordRaw);
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `Hysteria2 ${server}:${port}`; const name = decodedName ?? `Hysteria2 ${server}:${port}`;
const proxy: IProxyHysteria2Config = { const proxy: IProxyHysteria2Config = {
type: "hysteria2", type: "hysteria2",
@@ -783,15 +788,15 @@ function URI_Hysteria2(line: string): IProxyHysteria2Config {
function URI_Hysteria(line: string): IProxyHysteriaConfig { function URI_Hysteria(line: string): IProxyHysteriaConfig {
line = line.split(/(hysteria|hy):\/\//)[2]; line = line.split(/(hysteria|hy):\/\//)[2];
let [__, server, ___, port, ____, addons = "", name] = const [, server, , port, , addons = "", nameRaw] =
/^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `Hysteria ${server}:${port}`; const name = decodedName ?? `Hysteria ${server}:${port}`;
const proxy: IProxyHysteriaConfig = { const proxy: IProxyHysteriaConfig = {
type: "hysteria", type: "hysteria",
@@ -856,8 +861,10 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig {
break; break;
case "protocol": case "protocol":
proxy["protocol"] = value; proxy["protocol"] = value;
break;
case "sni": case "sni":
proxy["sni"] = value; proxy["sni"] = value;
break;
default: default:
break; break;
} }
@@ -879,17 +886,17 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig {
function URI_TUIC(line: string): IProxyTuicConfig { function URI_TUIC(line: string): IProxyTuicConfig {
line = line.split(/tuic:\/\//)[1]; line = line.split(/tuic:\/\//)[1];
let [__, uuid, password, server, ___, port, ____, addons = "", name] = const [, uuid, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; /^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
password = decodeURIComponent(password); const password = decodeURIComponent(passwordRaw);
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `TUIC ${server}:${port}`; const name = decodedName ?? `TUIC ${server}:${port}`;
const proxy: IProxyTuicConfig = { const proxy: IProxyTuicConfig = {
type: "tuic", type: "tuic",
@@ -958,17 +965,17 @@ function URI_TUIC(line: string): IProxyTuicConfig {
function URI_Wireguard(line: string): IProxyWireguardConfig { function URI_Wireguard(line: string): IProxyWireguardConfig {
line = line.split(/(wireguard|wg):\/\//)[2]; line = line.split(/(wireguard|wg):\/\//)[2];
let [__, ___, privateKey, server, ____, port, _____, addons = "", name] = const [, , privateKeyRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
privateKey = decodeURIComponent(privateKey); const privateKey = decodeURIComponent(privateKeyRaw);
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `WireGuard ${server}:${port}`; const name = decodedName ?? `WireGuard ${server}:${port}`;
const proxy: IProxyWireguardConfig = { const proxy: IProxyWireguardConfig = {
type: "wireguard", type: "wireguard",
name, name,
@@ -1007,12 +1014,14 @@ function URI_Wireguard(line: string): IProxyWireguardConfig {
proxy["pre-shared-key"] = value; proxy["pre-shared-key"] = value;
break; break;
case "reserved": case "reserved":
const parsed = value {
.split(",") const parsed = value
.map((i) => parseInt(i.trim(), 10)) .split(",")
.filter((i) => Number.isInteger(i)); .map((i) => parseInt(i.trim(), 10))
if (parsed.length === 3) { .filter((i) => Number.isInteger(i));
proxy["reserved"] = parsed; if (parsed.length === 3) {
proxy["reserved"] = parsed;
}
} }
break; break;
case "udp": case "udp":
@@ -1040,19 +1049,21 @@ function URI_Wireguard(line: string): IProxyWireguardConfig {
function URI_HTTP(line: string): IProxyHttpConfig { function URI_HTTP(line: string): IProxyHttpConfig {
line = line.split(/(http|https):\/\//)[2]; line = line.split(/(http|https):\/\//)[2];
let [__, ___, auth, server, ____, port, _____, addons = "", name] = const [, , authRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
let auth = authRaw;
if (auth) { if (auth) {
auth = decodeURIComponent(auth); auth = decodeURIComponent(auth);
} }
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `HTTP ${server}:${portNum}`; const name = decodedName ?? `HTTP ${server}:${portNum}`;
const proxy: IProxyHttpConfig = { const proxy: IProxyHttpConfig = {
type: "http", type: "http",
name, name,
@@ -1104,18 +1115,20 @@ function URI_HTTP(line: string): IProxyHttpConfig {
function URI_SOCKS(line: string): IProxySocks5Config { function URI_SOCKS(line: string): IProxySocks5Config {
line = line.split(/socks5:\/\//)[1]; line = line.split(/socks5:\/\//)[1];
let [__, ___, auth, server, ____, port, _____, addons = "", name] = const [, , authRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!; /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10); let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) { if (isNaN(portNum)) {
portNum = 443; portNum = 443;
} }
let auth = authRaw;
if (auth) { if (auth) {
auth = decodeURIComponent(auth); auth = decodeURIComponent(auth);
} }
const decodedName = trimStr(decodeURIComponent(name)); const decodedName = trimStr(decodeURIComponent(nameRaw));
name = decodedName ?? `SOCKS5 ${server}:${portNum}`; const name = decodedName ?? `SOCKS5 ${server}:${portNum}`;
const proxy: IProxySocks5Config = { const proxy: IProxySocks5Config = {
type: "socks5", type: "socks5",
name, name,