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,