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:
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
import { LoadingButton } from "@mui/lab";
|
import { LoadingButton } from "@mui/lab";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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", "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user