diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx
index 219b5108..305d6026 100644
--- a/src/components/base/base-dialog.tsx
+++ b/src/components/base/base-dialog.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab";
import {
Button,
diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx
index aca70363..2b0a0d8c 100644
--- a/src/components/proxy/proxy-chain.tsx
+++ b/src/components/proxy/proxy-chain.tsx
@@ -1,19 +1,19 @@
import {
- DndContext,
closestCenter,
+ DndContext,
+ DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
- DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
+ useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
-import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
Delete as DeleteIcon,
@@ -22,25 +22,25 @@ import {
LinkOff,
} from "@mui/icons-material";
import {
+ Alert,
Box,
+ Button,
+ Chip,
+ IconButton,
Paper,
Typography,
- IconButton,
- Chip,
- Alert,
useTheme,
- Button,
} from "@mui/material";
-import { useState, useCallback, useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { useAppData } from "@/providers/app-data-provider";
import {
- updateProxyChainConfigInRuntime,
- updateProxyAndSync,
- getProxies,
closeAllConnections,
+ getProxies,
+ updateProxyAndSync,
+ updateProxyChainConfigInRuntime,
} from "@/services/cmds";
interface ProxyChainItem {
diff --git a/src/pages/_routers.tsx b/src/pages/_routers.tsx
index a9c7becb..d3ccfe9d 100644
--- a/src/pages/_routers.tsx
+++ b/src/pages/_routers.tsx
@@ -30,49 +30,49 @@ export const routers = [
{
label: "Label-Home",
path: "/home",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Proxies",
path: "/",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Profiles",
path: "/profile",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Connections",
path: "/connections",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Rules",
path: "/rules",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Logs",
path: "/logs",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Unlock",
path: "/unlock",
- icon: [, ],
+ icon: [, ],
element: ,
},
{
label: "Label-Settings",
path: "/settings",
- icon: [, ],
+ icon: [, ],
element: ,
},
].map((router) => ({
diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx
index d90fea23..c8f98723 100644
--- a/src/pages/connections.tsx
+++ b/src/pages/connections.tsx
@@ -46,17 +46,21 @@ const ConnectionsPage = () => {
const isTableLayout = setting.layout === "table";
- const orderOpts: Record = {
- Default: (list) =>
- list.sort(
- (a, b) =>
- new Date(b.start || "0").getTime()! -
- new Date(a.start || "0").getTime()!,
- ),
- "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
- "Download Speed": (list) =>
- list.sort((a, b) => b.curDownload! - a.curDownload!),
- };
+ const orderOpts = useMemo>(
+ () => ({
+ Default: (list) =>
+ list.sort(
+ (a, b) =>
+ new Date(b.start || "0").getTime()! -
+ new Date(a.start || "0").getTime()!,
+ ),
+ "Upload Speed": (list) =>
+ list.sort((a, b) => b.curUpload! - a.curUpload!),
+ "Download Speed": (list) =>
+ list.sort((a, b) => b.curDownload! - a.curDownload!),
+ }),
+ [],
+ );
const [isPaused, setIsPaused] = useState(false);
const [frozenData, setFrozenData] = useState(null);
@@ -94,7 +98,7 @@ const ConnectionsPage = () => {
if (orderFunc) conns = orderFunc(conns);
return [conns];
- }, [displayData, match, curOrderOpt]);
+ }, [displayData, match, curOrderOpt, orderOpts]);
const onCloseAll = useLockFn(closeAllConnections);
diff --git a/src/pages/home.tsx b/src/pages/home.tsx
index 730dad60..6632903d 100644
--- a/src/pages/home.tsx
+++ b/src/pages/home.tsx
@@ -1,28 +1,28 @@
import {
- RouterOutlined,
- SettingsOutlined,
DnsOutlined,
- SpeedOutlined,
HelpOutlineRounded,
HistoryEduOutlined,
+ RouterOutlined,
+ SettingsOutlined,
+ SpeedOutlined,
} from "@mui/icons-material";
import {
Box,
Button,
- IconButton,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- FormGroup,
- FormControlLabel,
Checkbox,
- Tooltip,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ FormGroup,
Grid,
+ IconButton,
Skeleton,
+ Tooltip,
} from "@mui/material";
import { useLockFn } from "ahooks";
-import { useState, useMemo, Suspense, lazy, useCallback } from "react";
+import { Suspense, lazy, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { BasePage } from "@/components/base";
@@ -264,7 +264,7 @@ const HomePage = () => {
renderCard("network", ),
renderCard("mode", ),
],
- [homeCards, current, mutateProfiles, renderCard],
+ [current, mutateProfiles, renderCard],
);
// 新增:保存设置时用requestIdleCallback/setTimeout
@@ -314,7 +314,7 @@ const HomePage = () => {
,
),
],
- [homeCards, t, renderCard],
+ [t, renderCard],
);
return (
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 8232c915..c7ac3e67 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -1,11 +1,11 @@
import {
- DndContext,
closestCenter,
+ DndContext,
+ DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
- DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
@@ -19,14 +19,13 @@ import {
TextSnippetOutlined,
} from "@mui/icons-material";
import { LoadingButton } from "@mui/lab";
-import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material";
-import { listen } from "@tauri-apps/api/event";
-import { TauriEvent } from "@tauri-apps/api/event";
+import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
+import { listen, TauriEvent } from "@tauri-apps/api/event";
import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { useLockFn } from "ahooks";
import { throttle } from "lodash-es";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import useSWR, { mutate } from "swr";
@@ -42,17 +41,17 @@ import {
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { useListen } from "@/hooks/use-listen";
import { useProfiles } from "@/hooks/use-profiles";
-import { closeAllConnections } from "@/services/cmds";
import {
- importProfile,
+ closeAllConnections,
+ createProfile,
+ deleteProfile,
enhanceProfiles,
+ getProfiles,
//restartCore,
getRuntimeLogs,
- deleteProfile,
- updateProfile,
+ importProfile,
reorderProfile,
- createProfile,
- getProfiles,
+ updateProfile,
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useSetLoadingCache, useThemeMode } from "@/services/states";
@@ -117,41 +116,44 @@ const ProfilePage = () => {
const pendingRequestRef = useRef | null>(null);
// 处理profile切换中断
- const handleProfileInterrupt = (
- previousSwitching: string,
- newProfile: string,
- ) => {
- debugProfileSwitch(
- "INTERRUPT_PREVIOUS",
- previousSwitching,
- `被 ${newProfile} 中断`,
- );
+ const handleProfileInterrupt = useCallback(
+ (previousSwitching: string, newProfile: string) => {
+ debugProfileSwitch(
+ "INTERRUPT_PREVIOUS",
+ previousSwitching,
+ `被 ${newProfile} 中断`,
+ );
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
- }
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
+ }
- if (pendingRequestRef.current) {
- debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
- }
+ if (pendingRequestRef.current) {
+ debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
+ }
- setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
- showNotice(
- "info",
- `${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
- 3000,
- );
- };
+ setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
+ showNotice(
+ "info",
+ `${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
+ 3000,
+ );
+ },
+ [t],
+ );
// 清理切换状态
- const cleanupSwitchState = (profile: string, sequence: number) => {
- setActivatings((prev) => prev.filter((id) => id !== profile));
- switchingProfileRef.current = null;
- abortControllerRef.current = null;
- pendingRequestRef.current = null;
- debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
- };
+ const cleanupSwitchState = useCallback(
+ (profile: string, sequence: number) => {
+ setActivatings((prev) => prev.filter((id) => id !== profile));
+ switchingProfileRef.current = null;
+ abortControllerRef.current = null;
+ pendingRequestRef.current = null;
+ debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
+ },
+ [],
+ );
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@@ -160,6 +162,15 @@ const ProfilePage = () => {
);
const { current } = location.state || {};
+ const {
+ profiles = {},
+ activateSelected,
+ patchProfiles,
+ mutateProfiles,
+ error,
+ isStale,
+ } = useProfiles();
+
useEffect(() => {
const handleFileDrop = async () => {
const unlisten = await addListener(
@@ -197,16 +208,7 @@ const ProfilePage = () => {
return () => {
unsubscribe.then((cleanup) => cleanup());
};
- }, []);
-
- const {
- profiles = {},
- activateSelected,
- patchProfiles,
- mutateProfiles,
- error,
- isStale,
- } = useProfiles();
+ }, [addListener, mutateProfiles, t]);
// 添加紧急恢复功能
const onEmergencyRefresh = useLockFn(async () => {
@@ -380,151 +382,167 @@ const ProfilePage = () => {
}
};
- const executeBackgroundTasks = async (
- profile: string,
- sequence: number,
- abortController: AbortController,
- ) => {
- try {
- if (
- sequence === requestSequenceRef.current &&
- switchingProfileRef.current === profile &&
- !abortController.signal.aborted
- ) {
- await activateSelected();
- console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
- } else {
- debugProfileSwitch(
- "BACKGROUND_TASK_SKIPPED",
- profile,
- `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
- );
- }
- } catch (err: any) {
- console.warn("Failed to activate selected proxies:", err);
- }
- };
-
- const activateProfile = async (profile: string, notifySuccess: boolean) => {
- if (profiles.current === profile && !notifySuccess) {
- console.log(`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`);
- return;
- }
-
- const currentSequence = ++requestSequenceRef.current;
- debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
-
- // 处理中断逻辑
- const previousSwitching = switchingProfileRef.current;
- if (previousSwitching && previousSwitching !== profile) {
- handleProfileInterrupt(previousSwitching, profile);
- }
-
- // 防止重复切换同一个profile
- if (switchingProfileRef.current === profile) {
- debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
- return;
- }
-
- // 初始化切换状态
- switchingProfileRef.current = profile;
- debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
-
- const currentAbortController = new AbortController();
- abortControllerRef.current = currentAbortController;
-
- setActivatings((prev) => {
- if (prev.includes(profile)) return prev;
- return [...prev, profile];
- });
-
- try {
- console.log(
- `[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`,
- );
-
- // 检查请求有效性
- if (
- isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
- isOperationAborted(currentAbortController, profile)
- ) {
- return;
- }
-
- // 执行切换请求
- const requestPromise = patchProfiles(
- { current: profile },
- currentAbortController.signal,
- );
- pendingRequestRef.current = requestPromise;
-
- const success = await requestPromise;
-
- if (pendingRequestRef.current === requestPromise) {
- pendingRequestRef.current = null;
- }
-
- // 再次检查有效性
- if (
- isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
- isOperationAborted(currentAbortController, profile)
- ) {
- return;
- }
-
- // 完成切换
- await mutateLogs();
- closeAllConnections();
-
- if (notifySuccess && success) {
- showNotice("success", t("Profile Switched"), 1000);
- }
-
- console.log(
- `[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`,
- );
-
- // 延迟执行后台任务
- setTimeout(
- () =>
- executeBackgroundTasks(
+ const executeBackgroundTasks = useCallback(
+ async (
+ profile: string,
+ sequence: number,
+ abortController: AbortController,
+ ) => {
+ try {
+ if (
+ sequence === requestSequenceRef.current &&
+ switchingProfileRef.current === profile &&
+ !abortController.signal.aborted
+ ) {
+ await activateSelected();
+ console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
+ } else {
+ debugProfileSwitch(
+ "BACKGROUND_TASK_SKIPPED",
profile,
- currentSequence,
- currentAbortController,
- ),
- 50,
- );
- } catch (err: any) {
- if (pendingRequestRef.current) {
- pendingRequestRef.current = null;
+ `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
+ );
+ }
+ } catch (err: any) {
+ console.warn("Failed to activate selected proxies:", err);
}
+ },
+ [activateSelected],
+ );
- // 检查是否因为中断或过期而出错
- if (
- isOperationAborted(currentAbortController, profile) ||
- isRequestOutdated(currentSequence, requestSequenceRef, profile)
- ) {
+ const activateProfile = useCallback(
+ async (profile: string, notifySuccess: boolean) => {
+ if (profiles.current === profile && !notifySuccess) {
+ console.log(
+ `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
+ );
return;
}
- console.error(`[Profile] 切换失败:`, err);
- showNotice("error", err?.message || err.toString(), 4000);
- } finally {
- // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
- if (
- switchingProfileRef.current === profile &&
- currentSequence === requestSequenceRef.current
- ) {
- cleanupSwitchState(profile, currentSequence);
- } else {
- debugProfileSwitch(
- "CLEANUP_SKIPPED",
- profile,
- `序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
- );
+ const currentSequence = ++requestSequenceRef.current;
+ debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
+
+ // 处理中断逻辑
+ const previousSwitching = switchingProfileRef.current;
+ if (previousSwitching && previousSwitching !== profile) {
+ handleProfileInterrupt(previousSwitching, profile);
}
- }
- };
+
+ // 防止重复切换同一个profile
+ if (switchingProfileRef.current === profile) {
+ debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
+ return;
+ }
+
+ // 初始化切换状态
+ switchingProfileRef.current = profile;
+ debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
+
+ const currentAbortController = new AbortController();
+ abortControllerRef.current = currentAbortController;
+
+ setActivatings((prev) => {
+ if (prev.includes(profile)) return prev;
+ return [...prev, profile];
+ });
+
+ try {
+ console.log(
+ `[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`,
+ );
+
+ // 检查请求有效性
+ if (
+ isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
+ isOperationAborted(currentAbortController, profile)
+ ) {
+ return;
+ }
+
+ // 执行切换请求
+ const requestPromise = patchProfiles(
+ { current: profile },
+ currentAbortController.signal,
+ );
+ pendingRequestRef.current = requestPromise;
+
+ const success = await requestPromise;
+
+ if (pendingRequestRef.current === requestPromise) {
+ pendingRequestRef.current = null;
+ }
+
+ // 再次检查有效性
+ if (
+ isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
+ isOperationAborted(currentAbortController, profile)
+ ) {
+ return;
+ }
+
+ // 完成切换
+ await mutateLogs();
+ closeAllConnections();
+
+ if (notifySuccess && success) {
+ showNotice("success", t("Profile Switched"), 1000);
+ }
+
+ console.log(
+ `[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`,
+ );
+
+ // 延迟执行后台任务
+ setTimeout(
+ () =>
+ executeBackgroundTasks(
+ profile,
+ currentSequence,
+ currentAbortController,
+ ),
+ 50,
+ );
+ } catch (err: any) {
+ if (pendingRequestRef.current) {
+ pendingRequestRef.current = null;
+ }
+
+ // 检查是否因为中断或过期而出错
+ if (
+ isOperationAborted(currentAbortController, profile) ||
+ isRequestOutdated(currentSequence, requestSequenceRef, profile)
+ ) {
+ return;
+ }
+
+ console.error(`[Profile] 切换失败:`, err);
+ showNotice("error", err?.message || err.toString(), 4000);
+ } finally {
+ // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
+ if (
+ switchingProfileRef.current === profile &&
+ currentSequence === requestSequenceRef.current
+ ) {
+ cleanupSwitchState(profile, currentSequence);
+ } else {
+ debugProfileSwitch(
+ "CLEANUP_SKIPPED",
+ profile,
+ `序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
+ );
+ }
+ }
+ },
+ [
+ profiles,
+ patchProfiles,
+ mutateLogs,
+ t,
+ executeBackgroundTasks,
+ handleProfileInterrupt,
+ cleanupSwitchState,
+ ],
+ );
const onSelect = async (current: string, force: boolean) => {
// 阻止重复点击或已激活的profile
if (switchingProfileRef.current === current) {
@@ -547,7 +565,7 @@ const ProfilePage = () => {
await activateProfile(current, false);
}
})();
- }, current);
+ }, [current, activateProfile, mutateProfiles]);
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
if (switchingProfileRef.current) {
@@ -583,7 +601,9 @@ const ProfilePage = () => {
await deleteProfile(uid);
mutateProfiles();
mutateLogs();
- current && (await onEnhance(false));
+ if (current) {
+ await onEnhance(false);
+ }
} catch (err: any) {
showNotice("error", err?.message || err.toString());
} finally {
diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx
index 44bf3d20..b115eb53 100644
--- a/src/pages/proxies.tsx
+++ b/src/pages/proxies.tsx
@@ -1,6 +1,6 @@
import { Box, Button, ButtonGroup } from "@mui/material";
import { useLockFn } from "ahooks";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
@@ -12,9 +12,9 @@ import {
closeAllConnections,
getClashConfig,
getRuntimeProxyChainConfig,
+ patchClashMode,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
-import { patchClashMode } from "@/services/cmds";
const ProxyPage = () => {
const { t } = useTranslation();
@@ -44,7 +44,7 @@ const ProxyPage = () => {
const { verge } = useVerge();
- const modeList = ["rule", "global", "direct"];
+ const modeList = useMemo(() => ["rule", "global", "direct"], []);
const curMode = clashConfig?.mode?.toLowerCase();
@@ -100,7 +100,7 @@ const ProxyPage = () => {
if (curMode && !modeList.includes(curMode)) {
onChangeMode("rule");
}
- }, [curMode]);
+ }, [curMode, modeList, onChangeMode]);
return (
{
const { verge, mutateVerge, patchVerge } = useVerge();
// test list
- const testList = verge?.test_list ?? [
- {
- uid: nanoid(),
- name: "Apple",
- url: "https://www.apple.com",
- icon: apple,
- },
- {
- uid: nanoid(),
- name: "GitHub",
- url: "https://www.github.com",
- icon: github,
- },
- {
- uid: nanoid(),
- name: "Google",
- url: "https://www.google.com",
- icon: google,
- },
- {
- uid: nanoid(),
- name: "Youtube",
- url: "https://www.youtube.com",
- icon: youtube,
- },
- ];
+ const testList = useMemo(
+ () =>
+ verge?.test_list ?? [
+ {
+ uid: nanoid(),
+ name: "Apple",
+ url: "https://www.apple.com",
+ icon: apple,
+ },
+ {
+ uid: nanoid(),
+ name: "GitHub",
+ url: "https://www.github.com",
+ icon: github,
+ },
+ {
+ uid: nanoid(),
+ name: "Google",
+ url: "https://www.google.com",
+ icon: google,
+ },
+ {
+ uid: nanoid(),
+ name: "Youtube",
+ url: "https://www.youtube.com",
+ icon: youtube,
+ },
+ ],
+ [verge],
+ );
const onTestListItemChange = (
uid: string,
@@ -117,7 +121,7 @@ const TestPage = () => {
if (!verge?.test_list) {
patchVerge({ test_list: testList });
}
- }, [verge]);
+ }, [verge, patchVerge, testList]);
const viewerRef = useRef(null);
const [showScrollTop, setShowScrollTop] = useState(false);
diff --git a/src/pages/unlock.tsx b/src/pages/unlock.tsx
index f380448b..562d534c 100644
--- a/src/pages/unlock.tsx
+++ b/src/pages/unlock.tsx
@@ -1,30 +1,30 @@
import {
- CheckCircleOutlined,
+ AccessTimeOutlined,
CancelOutlined,
+ CheckCircleOutlined,
HelpOutline,
PendingOutlined,
RefreshRounded,
- AccessTimeOutlined,
} from "@mui/icons-material";
import {
Box,
Button,
Card,
- Divider,
- Typography,
Chip,
- Tooltip,
CircularProgress,
+ Divider,
+ Grid,
+ Tooltip,
+ Typography,
alpha,
useTheme,
- Grid,
} from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { BasePage, BaseEmpty } from "@/components/base";
+import { BaseEmpty, BasePage } from "@/components/base";
import { showNotice } from "@/services/noticeService";
interface UnlockItem {
@@ -45,9 +45,9 @@ const UnlockPage = () => {
const [isCheckingAll, setIsCheckingAll] = useState(false);
const [loadingItems, setLoadingItems] = useState([]);
- const sortItemsByName = (items: UnlockItem[]) => {
+ const sortItemsByName = useCallback((items: UnlockItem[]) => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
- };
+ }, []);
// 保存测试结果到本地存储
const saveResultsToStorage = (items: UnlockItem[], time: string | null) => {
@@ -82,6 +82,22 @@ const UnlockPage = () => {
return { items: null, time: null };
};
+ const getUnlockItems = useCallback(
+ async (updateUI: boolean = true) => {
+ try {
+ const items = await invoke("get_unlock_items");
+ const sortedItems = sortItemsByName(items);
+
+ if (updateUI) {
+ setUnlockItems(sortedItems);
+ }
+ } catch (err: any) {
+ console.error("Failed to get unlock items:", err);
+ }
+ },
+ [sortItemsByName],
+ );
+
useEffect(() => {
const { items: storedItems } = loadResultsFromStorage();
@@ -91,20 +107,7 @@ const UnlockPage = () => {
} else {
getUnlockItems(true);
}
- }, []);
-
- const getUnlockItems = async (updateUI: boolean = true) => {
- try {
- const items = await invoke("get_unlock_items");
- const sortedItems = sortItemsByName(items);
-
- if (updateUI) {
- setUnlockItems(sortedItems);
- }
- } catch (err: any) {
- console.error("Failed to get unlock items:", err);
- }
- };
+ }, [getUnlockItems]);
const invokeWithTimeout = async (
cmd: string,
diff --git a/src/polyfills/RegExp.js b/src/polyfills/RegExp.js
index 7fe6f104..389ae27a 100644
--- a/src/polyfills/RegExp.js
+++ b/src/polyfills/RegExp.js
@@ -11,13 +11,23 @@
}
if (flags) {
- if (!originalRegExp.prototype.hasOwnProperty("unicodeSets")) {
+ if (
+ !Object.prototype.hasOwnProperty.call(
+ originalRegExp.prototype,
+ "unicodeSets",
+ )
+ ) {
if (flags.includes("v")) {
flags = flags.replace("v", "u");
}
}
- if (!originalRegExp.prototype.hasOwnProperty("hasIndices")) {
+ if (
+ !Object.prototype.hasOwnProperty.call(
+ originalRegExp.prototype,
+ "hasIndices",
+ )
+ ) {
if (flags.includes("d")) {
flags = flags.replace("d", "");
}
diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx
index 11b8bc81..16ad3962 100644
--- a/src/providers/app-data-provider.tsx
+++ b/src/providers/app-data-provider.tsx
@@ -12,20 +12,18 @@ import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import {
- getProxies,
- getRules,
+ forceRefreshProxies,
+ getAppUptime,
getClashConfig,
+ getConnections,
+ getMemoryData,
+ getProxies,
getProxyProviders,
getRuleProviders,
- getConnections,
- getTrafficData,
- getMemoryData,
-} from "@/services/cmds";
-import {
- getSystemProxy,
+ getRules,
getRunningMode,
- getAppUptime,
- forceRefreshProxies,
+ getSystemProxy,
+ getTrafficData,
} from "@/services/cmds";
// 连接速度计算接口
@@ -482,7 +480,7 @@ export const AppDataProvider = ({
);
// 提供统一的刷新方法
- const refreshAll = async () => {
+ const refreshAll = React.useCallback(async () => {
await Promise.all([
refreshProxy(),
refreshClashConfig(),
@@ -491,7 +489,14 @@ export const AppDataProvider = ({
refreshProxyProviders(),
refreshRuleProviders(),
]);
- };
+ }, [
+ refreshProxy,
+ refreshClashConfig,
+ refreshRules,
+ refreshSysproxy,
+ refreshProxyProviders,
+ refreshRuleProviders,
+ ]);
// 聚合所有数据
const value = useMemo(() => {
@@ -581,6 +586,7 @@ export const AppDataProvider = ({
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
+ refreshAll,
]);
return (
diff --git a/src/providers/chain-proxy-provider.tsx b/src/providers/chain-proxy-provider.tsx
index ca39d58c..2b834f89 100644
--- a/src/providers/chain-proxy-provider.tsx
+++ b/src/providers/chain-proxy-provider.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState, useCallback } from "react";
+import React, { createContext, useCallback, useContext, useState } from "react";
interface ChainProxyContextType {
isChainMode: boolean;
diff --git a/src/services/types.d.ts b/src/services/types.d.ts
index f63928b9..752c581d 100644
--- a/src/services/types.d.ts
+++ b/src/services/types.d.ts
@@ -739,7 +739,6 @@ interface IProxySnellConfig extends IProxyBaseConfig {
psk?: string;
udp?: boolean;
version?: number;
- "obfs-opts"?: {};
}
interface IProxyConfig
extends IProxyBaseConfig,
diff --git a/src/utils/is-async-function.ts b/src/utils/is-async-function.ts
index e8651152..4ef11c3d 100644
--- a/src/utils/is-async-function.ts
+++ b/src/utils/is-async-function.ts
@@ -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";
}
diff --git a/src/utils/uri-parser.ts b/src/utils/uri-parser.ts
index 2da8ccde..c3a622c5 100644
--- a/src/utils/uri-parser.ts
+++ b/src/utils/uri-parser.ts
@@ -648,7 +648,7 @@ function URI_VLESS(line: string): IProxyVlessConfig {
function URI_Trojan(line: string): IProxyTrojanConfig {
line = line.split("trojan://")[1];
- let [__, password, server, ___, port, ____, addons = "", name] =
+ const [, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10);
@@ -656,8 +656,10 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
portNum = 443;
}
+ let password = passwordRaw;
password = decodeURIComponent(password);
+ let name = nameRaw;
const decodedName = trimStr(decodeURIComponent(name));
name = decodedName ?? `Trojan ${server}:${portNum}`;
@@ -672,8 +674,8 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
let path = "";
for (const addon of addons.split("&")) {
- let [key, value] = addon.split("=");
- value = decodeURIComponent(value);
+ const [key, valueRaw] = addon.split("=");
+ const value = decodeURIComponent(valueRaw);
switch (key) {
case "type":
if (["ws", "h2"].includes(value)) {
@@ -704,14 +706,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
proxy["fingerprint"] = value;
break;
case "encryption":
- const encryption = value.split(";");
- if (encryption.length === 3) {
- proxy["ss-opts"] = {
- enabled: true,
- method: encryption[1],
- password: encryption[2],
- };
+ {
+ const encryption = value.split(";");
+ if (encryption.length === 3) {
+ proxy["ss-opts"] = {
+ enabled: true,
+ method: encryption[1],
+ password: encryption[2],
+ };
+ }
}
+ break;
case "client-fingerprint":
proxy["client-fingerprint"] = value as ClientFingerprint;
break;
@@ -736,17 +741,17 @@ function URI_Trojan(line: string): IProxyTrojanConfig {
function URI_Hysteria2(line: string): IProxyHysteria2Config {
line = line.split(/(hysteria2|hy2):\/\//)[2];
- let [__, password, server, ___, port, ____, addons = "", name] =
+ const [, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
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 = {
type: "hysteria2",
@@ -783,15 +788,15 @@ function URI_Hysteria2(line: string): IProxyHysteria2Config {
function URI_Hysteria(line: string): IProxyHysteriaConfig {
line = line.split(/(hysteria|hy):\/\//)[2];
- let [__, server, ___, port, ____, addons = "", name] =
+ const [, server, , port, , addons = "", nameRaw] =
/^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
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 = {
type: "hysteria",
@@ -856,8 +861,10 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig {
break;
case "protocol":
proxy["protocol"] = value;
+ break;
case "sni":
proxy["sni"] = value;
+ break;
default:
break;
}
@@ -879,17 +886,17 @@ function URI_Hysteria(line: string): IProxyHysteriaConfig {
function URI_TUIC(line: string): IProxyTuicConfig {
line = line.split(/tuic:\/\//)[1];
- let [__, uuid, password, server, ___, port, ____, addons = "", name] =
+ const [, uuid, passwordRaw, server, , port, , addons = "", nameRaw] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
portNum = 443;
}
- password = decodeURIComponent(password);
- const decodedName = trimStr(decodeURIComponent(name));
+ const password = decodeURIComponent(passwordRaw);
+ const decodedName = trimStr(decodeURIComponent(nameRaw));
- name = decodedName ?? `TUIC ${server}:${port}`;
+ const name = decodedName ?? `TUIC ${server}:${port}`;
const proxy: IProxyTuicConfig = {
type: "tuic",
@@ -958,17 +965,17 @@ function URI_TUIC(line: string): IProxyTuicConfig {
function URI_Wireguard(line: string): IProxyWireguardConfig {
line = line.split(/(wireguard|wg):\/\//)[2];
- let [__, ___, privateKey, server, ____, port, _____, addons = "", name] =
+ const [, , privateKeyRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
portNum = 443;
}
- privateKey = decodeURIComponent(privateKey);
- const decodedName = trimStr(decodeURIComponent(name));
+ const privateKey = decodeURIComponent(privateKeyRaw);
+ const decodedName = trimStr(decodeURIComponent(nameRaw));
- name = decodedName ?? `WireGuard ${server}:${port}`;
+ const name = decodedName ?? `WireGuard ${server}:${port}`;
const proxy: IProxyWireguardConfig = {
type: "wireguard",
name,
@@ -1007,12 +1014,14 @@ function URI_Wireguard(line: string): IProxyWireguardConfig {
proxy["pre-shared-key"] = value;
break;
case "reserved":
- const parsed = value
- .split(",")
- .map((i) => parseInt(i.trim(), 10))
- .filter((i) => Number.isInteger(i));
- if (parsed.length === 3) {
- proxy["reserved"] = parsed;
+ {
+ const parsed = value
+ .split(",")
+ .map((i) => parseInt(i.trim(), 10))
+ .filter((i) => Number.isInteger(i));
+ if (parsed.length === 3) {
+ proxy["reserved"] = parsed;
+ }
}
break;
case "udp":
@@ -1040,19 +1049,21 @@ function URI_Wireguard(line: string): IProxyWireguardConfig {
function URI_HTTP(line: string): IProxyHttpConfig {
line = line.split(/(http|https):\/\//)[2];
- let [__, ___, auth, server, ____, port, _____, addons = "", name] =
+ const [, , authRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
portNum = 443;
}
+ let auth = authRaw;
+
if (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 = {
type: "http",
name,
@@ -1104,18 +1115,20 @@ function URI_HTTP(line: string): IProxyHttpConfig {
function URI_SOCKS(line: string): IProxySocks5Config {
line = line.split(/socks5:\/\//)[1];
- let [__, ___, auth, server, ____, port, _____, addons = "", name] =
+ const [, , authRaw, server, , port, , addons = "", nameRaw] =
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
let portNum = parseInt(`${port}`, 10);
if (isNaN(portNum)) {
portNum = 443;
}
+
+ let auth = authRaw;
if (auth) {
auth = decodeURIComponent(auth);
}
- const decodedName = trimStr(decodeURIComponent(name));
- name = decodedName ?? `SOCKS5 ${server}:${portNum}`;
+ const decodedName = trimStr(decodeURIComponent(nameRaw));
+ const name = decodedName ?? `SOCKS5 ${server}:${portNum}`;
const proxy: IProxySocks5Config = {
type: "socks5",
name,