mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
feat: refactor app data provider and context for improved data management and performance
This commit is contained in:
@@ -58,6 +58,32 @@ export default defineConfig([
|
||||
|
||||
"@eslint-react/no-forward-ref": "off",
|
||||
|
||||
// React performance and production quality rules
|
||||
"@eslint-react/no-array-index-key": "warn",
|
||||
"@eslint-react/no-children-count": "error",
|
||||
"@eslint-react/no-children-for-each": "error",
|
||||
"@eslint-react/no-children-map": "error",
|
||||
"@eslint-react/no-children-only": "error",
|
||||
"@eslint-react/no-children-prop": "error",
|
||||
"@eslint-react/no-children-to-array": "error",
|
||||
"@eslint-react/no-class-component": "error",
|
||||
"@eslint-react/no-clone-element": "error",
|
||||
"@eslint-react/no-create-ref": "error",
|
||||
"@eslint-react/no-default-props": "error",
|
||||
"@eslint-react/no-direct-mutation-state": "error",
|
||||
"@eslint-react/no-implicit-key": "error",
|
||||
"@eslint-react/no-prop-types": "error",
|
||||
"@eslint-react/no-set-state-in-component-did-mount": "error",
|
||||
"@eslint-react/no-set-state-in-component-did-update": "error",
|
||||
"@eslint-react/no-set-state-in-component-will-update": "error",
|
||||
"@eslint-react/no-string-refs": "error",
|
||||
"@eslint-react/no-unstable-context-value": "warn",
|
||||
"@eslint-react/no-unstable-default-props": "warn",
|
||||
"@eslint-react/no-unused-class-component-members": "error",
|
||||
"@eslint-react/no-unused-state": "error",
|
||||
"@eslint-react/no-useless-fragment": "warn",
|
||||
"@eslint-react/prefer-destructuring-assignment": "warn",
|
||||
|
||||
// TypeScript
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import {
|
||||
Button,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
||||
import { Typography, Stack, Divider } from "@mui/material";
|
||||
import { Divider, Stack, Typography } from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import {
|
||||
DirectionsRounded,
|
||||
LanguageRounded,
|
||||
MultipleStopRounded,
|
||||
DirectionsRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { Box, Typography, Paper, Stack } from "@mui/material";
|
||||
import { Box, Paper, Stack, Typography } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { closeAllConnections, patchClashMode } from "@/services/cmds";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {
|
||||
SignalWifi4Bar as SignalStrong,
|
||||
AccessTimeRounded,
|
||||
ChevronRight,
|
||||
WifiOff as SignalError,
|
||||
SignalWifi3Bar as SignalGood,
|
||||
SignalWifi2Bar as SignalMedium,
|
||||
SignalWifi1Bar as SignalWeak,
|
||||
SignalWifi0Bar as SignalNone,
|
||||
WifiOff as SignalError,
|
||||
ChevronRight,
|
||||
SortRounded,
|
||||
AccessTimeRounded,
|
||||
SignalWifi4Bar as SignalStrong,
|
||||
SignalWifi1Bar as SignalWeak,
|
||||
SortByAlphaRounded,
|
||||
SortRounded,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Button,
|
||||
alpha,
|
||||
useTheme,
|
||||
Select,
|
||||
MenuItem,
|
||||
Chip,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
// 本地存储的键名
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {
|
||||
ArrowUpwardRounded,
|
||||
ArrowDownwardRounded,
|
||||
MemoryRounded,
|
||||
LinkRounded,
|
||||
CloudUploadRounded,
|
||||
ArrowUpwardRounded,
|
||||
CloudDownloadRounded,
|
||||
CloudUploadRounded,
|
||||
LinkRounded,
|
||||
MemoryRounded,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
PaletteColor,
|
||||
Paper,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
PaletteColor,
|
||||
Grid,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { useRef, useCallback, memo, useMemo } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
@@ -24,8 +23,8 @@ import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary
|
||||
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { isDebugEnabled, gc } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { gc, isDebugEnabled } from "@/services/cmds";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
CloudUploadOutlined,
|
||||
StorageOutlined,
|
||||
UpdateOutlined,
|
||||
DnsOutlined,
|
||||
SpeedOutlined,
|
||||
EventOutlined,
|
||||
LaunchOutlined,
|
||||
SpeedOutlined,
|
||||
StorageOutlined,
|
||||
UpdateOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
LinearProgress,
|
||||
alpha,
|
||||
useTheme,
|
||||
Link,
|
||||
Stack,
|
||||
Typography,
|
||||
alpha,
|
||||
keyframes,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { openWebUrl, updateProfile } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
|
||||
import { RefreshRounded, StorageOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
alpha,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
@@ -21,7 +21,7 @@ import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { proxyProviderUpdate } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
@@ -35,7 +35,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getProxies,
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { ExpandMoreRounded } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Box,
|
||||
Chip,
|
||||
Typography,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import {
|
||||
providerHealthCheck,
|
||||
getGroupProxyDelays,
|
||||
updateProxyChainConfigInRuntime,
|
||||
getRuntimeConfig,
|
||||
providerHealthCheck,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useEffect, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { getRuntimeConfig } from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
import { filterSort } from "./use-filter-sort";
|
||||
import {
|
||||
useHeadStateNew,
|
||||
DEFAULT_STATE,
|
||||
useHeadStateNew,
|
||||
type HeadState,
|
||||
} from "./use-head-state";
|
||||
import { useWindowWidth } from "./use-window-width";
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
|
||||
import { RefreshRounded, StorageOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Divider,
|
||||
alpha,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
@@ -20,7 +20,7 @@ import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { ruleProviderUpdate } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ import { BaseFieldset } from "@/components/base/base-fieldset";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { EditorViewer } from "@/components/profile/editor-viewer";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { getClashConfig } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import {
|
||||
getAutotemProxy,
|
||||
getClashConfig,
|
||||
getNetworkInterfacesInfo,
|
||||
getSystemHostname,
|
||||
getSystemProxy,
|
||||
@@ -440,14 +440,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</Typography>
|
||||
</FlexBox>
|
||||
{!value.pac && (
|
||||
<>
|
||||
<FlexBox>
|
||||
<Typography className="label">{t("Server Addr")}</Typography>
|
||||
<Typography className="value">
|
||||
{getSystemProxyAddress}
|
||||
</Typography>
|
||||
</FlexBox>
|
||||
</>
|
||||
<FlexBox>
|
||||
<Typography className="label">{t("Server Addr")}</Typography>
|
||||
<Typography className="value">{getSystemProxyAddress}</Typography>
|
||||
</FlexBox>
|
||||
)}
|
||||
{value.pac && (
|
||||
<FlexBox>
|
||||
@@ -582,39 +578,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
)}
|
||||
|
||||
{value.pac && (
|
||||
<>
|
||||
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
||||
<ListItemText
|
||||
primary={t("PAC Script Content")}
|
||||
sx={{ padding: "3px 0" }}
|
||||
/>
|
||||
<Button
|
||||
startIcon={<EditRounded />}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setEditorOpen(true);
|
||||
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
|
||||
<ListItemText
|
||||
primary={t("PAC Script Content")}
|
||||
sx={{ padding: "3px 0" }}
|
||||
/>
|
||||
<Button
|
||||
startIcon={<EditRounded />}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("Edit")} PAC
|
||||
</Button>
|
||||
{editorOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Edit")} PAC`}
|
||||
initialData={Promise.resolve(value.pac_content ?? "")}
|
||||
language="javascript"
|
||||
onSave={(_prev, curr) => {
|
||||
let pac = DEFAULT_PAC;
|
||||
if (curr && curr.trim().length > 0) {
|
||||
pac = curr;
|
||||
}
|
||||
setValue((v) => ({ ...v, pac_content: pac }));
|
||||
}}
|
||||
>
|
||||
{t("Edit")} PAC
|
||||
</Button>
|
||||
{editorOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Edit")} PAC`}
|
||||
initialData={Promise.resolve(value.pac_content ?? "")}
|
||||
language="javascript"
|
||||
onSave={(_prev, curr) => {
|
||||
let pac = DEFAULT_PAC;
|
||||
if (curr && curr.trim().length > 0) {
|
||||
pac = curr;
|
||||
}
|
||||
setValue((v) => ({ ...v, pac_content: pac }));
|
||||
}}
|
||||
onClose={() => setEditorOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
</>
|
||||
onClose={() => setEditorOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
|
||||
// 定义代理组类型
|
||||
interface ProxyGroup {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { getAutotemProxy } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { closeAllConnections, getAutotemProxy } from "@/services/cmds";
|
||||
|
||||
// 系统代理状态检测统一逻辑
|
||||
export const useSystemProxyState = () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { closeAllConnections } from "@/services/cmds";
|
||||
import { useConnectionSetting } from "@/services/states";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
||||
import { ProviderButton } from "@/components/rule/provider-button";
|
||||
import RuleItem from "@/components/rule/rule-item";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
|
||||
const RulesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
53
src/providers/app-data-context.ts
Normal file
53
src/providers/app-data-context.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createContext, use } from "react";
|
||||
|
||||
export interface AppDataContextType {
|
||||
proxies: any;
|
||||
clashConfig: any;
|
||||
rules: any[];
|
||||
sysproxy: any;
|
||||
runningMode?: string;
|
||||
uptime: number;
|
||||
proxyProviders: any;
|
||||
ruleProviders: any;
|
||||
connections: {
|
||||
data: ConnectionWithSpeed[];
|
||||
count: number;
|
||||
uploadTotal: number;
|
||||
downloadTotal: number;
|
||||
};
|
||||
traffic: { up: number; down: number };
|
||||
memory: { inuse: number };
|
||||
systemProxyAddress: string;
|
||||
|
||||
refreshProxy: () => Promise<any>;
|
||||
refreshClashConfig: () => Promise<any>;
|
||||
refreshRules: () => Promise<any>;
|
||||
refreshSysproxy: () => Promise<any>;
|
||||
refreshProxyProviders: () => Promise<any>;
|
||||
refreshRuleProviders: () => Promise<any>;
|
||||
refreshAll: () => Promise<any>;
|
||||
}
|
||||
|
||||
export interface ConnectionWithSpeed extends IConnectionsItem {
|
||||
curUpload: number;
|
||||
curDownload: number;
|
||||
}
|
||||
|
||||
export interface ConnectionSpeedData {
|
||||
id: string;
|
||||
upload: number;
|
||||
download: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export const AppDataContext = createContext<AppDataContextType | null>(null);
|
||||
|
||||
export const useAppData = () => {
|
||||
const context = use(AppDataContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useAppData必须在AppDataProvider内使用");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import React, { createContext, use, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
@@ -20,50 +20,11 @@ import {
|
||||
getTrafficData,
|
||||
} from "@/services/cmds";
|
||||
|
||||
// 连接速度计算接口
|
||||
interface ConnectionSpeedData {
|
||||
id: string;
|
||||
upload: number;
|
||||
download: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ConnectionWithSpeed extends IConnectionsItem {
|
||||
curUpload: number;
|
||||
curDownload: number;
|
||||
}
|
||||
|
||||
// 定义AppDataContext类型 - 使用宽松类型
|
||||
interface AppDataContextType {
|
||||
proxies: any;
|
||||
clashConfig: any;
|
||||
rules: any[];
|
||||
sysproxy: any;
|
||||
runningMode?: string;
|
||||
uptime: number;
|
||||
proxyProviders: any;
|
||||
ruleProviders: any;
|
||||
connections: {
|
||||
data: ConnectionWithSpeed[];
|
||||
count: number;
|
||||
uploadTotal: number;
|
||||
downloadTotal: number;
|
||||
};
|
||||
traffic: { up: number; down: number };
|
||||
memory: { inuse: number };
|
||||
systemProxyAddress: string;
|
||||
|
||||
refreshProxy: () => Promise<any>;
|
||||
refreshClashConfig: () => Promise<any>;
|
||||
refreshRules: () => Promise<any>;
|
||||
refreshSysproxy: () => Promise<any>;
|
||||
refreshProxyProviders: () => Promise<any>;
|
||||
refreshRuleProviders: () => Promise<any>;
|
||||
refreshAll: () => Promise<any>;
|
||||
}
|
||||
|
||||
// 创建上下文
|
||||
const AppDataContext = createContext<AppDataContextType | null>(null);
|
||||
import {
|
||||
AppDataContext,
|
||||
type ConnectionSpeedData,
|
||||
type ConnectionWithSpeed,
|
||||
} from "./app-data-context";
|
||||
|
||||
// 全局数据提供者组件
|
||||
export const AppDataProvider = ({
|
||||
@@ -135,196 +96,227 @@ export const AppDataProvider = ({
|
||||
|
||||
// 监听profile和clash配置变更事件
|
||||
useEffect(() => {
|
||||
let profileUnlisten: Promise<() => void> | undefined;
|
||||
let lastProfileId: string | null = null;
|
||||
let lastUpdateTime = 0;
|
||||
const refreshThrottle = 500;
|
||||
|
||||
const setupEventListeners = async () => {
|
||||
try {
|
||||
// 监听profile切换事件
|
||||
profileUnlisten = listen<string>("profile-changed", (event) => {
|
||||
const newProfileId = event.payload;
|
||||
const now = Date.now();
|
||||
let isUnmounted = false;
|
||||
const scheduledTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||
const cleanupFns: Array<() => void> = [];
|
||||
const fallbackWindowListeners: Array<[string, EventListener]> = [];
|
||||
|
||||
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
|
||||
|
||||
if (
|
||||
lastProfileId === newProfileId &&
|
||||
now - lastUpdateTime < refreshThrottle
|
||||
) {
|
||||
console.log("[AppDataProvider] 重复事件被防抖,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
lastProfileId = newProfileId;
|
||||
lastUpdateTime = now;
|
||||
|
||||
setTimeout(() => {
|
||||
// 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading
|
||||
forceRefreshProxies()
|
||||
.catch((e) =>
|
||||
console.warn("[AppDataProvider] forceRefreshProxies 失败:", e),
|
||||
)
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] 普通刷新也失败:", e),
|
||||
);
|
||||
}, 200); // 200ms 延迟,保证后端缓存已清理
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// 监听Clash配置刷新事件(enhance操作等)
|
||||
const handleRefreshClash = () => {
|
||||
const now = Date.now();
|
||||
console.log("[AppDataProvider] Clash配置刷新事件");
|
||||
|
||||
if (now - lastUpdateTime > refreshThrottle) {
|
||||
lastUpdateTime = now;
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
|
||||
|
||||
// 添加超时保护
|
||||
const refreshPromise = Promise.race([
|
||||
forceRefreshProxies(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("forceRefreshProxies timeout")),
|
||||
8000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
await refreshPromise;
|
||||
await refreshProxy();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
|
||||
error,
|
||||
);
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听代理配置刷新事件(托盘代理切换等)
|
||||
const handleRefreshProxy = () => {
|
||||
const now = Date.now();
|
||||
console.log("[AppDataProvider] 代理配置刷新事件");
|
||||
|
||||
if (now - lastUpdateTime > refreshThrottle) {
|
||||
lastUpdateTime = now;
|
||||
|
||||
setTimeout(() => {
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] 代理刷新失败:", e),
|
||||
);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听强制代理刷新事件(托盘代理切换立即刷新)
|
||||
const handleForceRefreshProxies = () => {
|
||||
console.log("[AppDataProvider] 强制代理刷新事件");
|
||||
|
||||
// 立即刷新,无延迟,无防抖
|
||||
forceRefreshProxies()
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 强制刷新代理缓存完成");
|
||||
// 强制刷新完成后,立即刷新前端显示
|
||||
return refreshProxy();
|
||||
})
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 前端代理数据刷新完成");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("[AppDataProvider] 强制代理刷新失败:", e);
|
||||
// 如果强制刷新失败,尝试普通刷新
|
||||
refreshProxy().catch((e2) =>
|
||||
console.warn("[AppDataProvider] 普通代理刷新也失败:", e2),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 使用 Tauri 事件监听器替代 window 事件监听器
|
||||
const setupTauriListeners = async () => {
|
||||
try {
|
||||
const unlistenClash = await listen(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
const unlistenProxy = await listen(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
const unlistenForceRefresh = await listen(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlistenClash();
|
||||
unlistenProxy();
|
||||
unlistenForceRefresh();
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||||
|
||||
// 降级到 window 事件监听器
|
||||
window.addEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
window.addEventListener(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.addEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
window.removeEventListener(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupTauriListeners = setupTauriListeners();
|
||||
|
||||
return async () => {
|
||||
const cleanup = await cleanupTauriListeners;
|
||||
cleanup();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[AppDataProvider] 事件监听器设置失败:", error);
|
||||
return () => {};
|
||||
const registerCleanup = (fn: () => void) => {
|
||||
if (isUnmounted) {
|
||||
fn();
|
||||
} else {
|
||||
cleanupFns.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupPromise = setupEventListeners();
|
||||
const scheduleTimeout = (
|
||||
callback: () => void | Promise<void>,
|
||||
delay: number,
|
||||
) => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
scheduledTimeouts.delete(timeoutId);
|
||||
void callback();
|
||||
}, delay);
|
||||
|
||||
scheduledTimeouts.add(timeoutId);
|
||||
return timeoutId;
|
||||
};
|
||||
|
||||
const clearScheduledTimeout = (
|
||||
timeoutId: ReturnType<typeof setTimeout>,
|
||||
) => {
|
||||
if (scheduledTimeouts.has(timeoutId)) {
|
||||
clearTimeout(timeoutId);
|
||||
scheduledTimeouts.delete(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllTimeouts = () => {
|
||||
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
|
||||
scheduledTimeouts.clear();
|
||||
};
|
||||
|
||||
const withTimeout = async <T,>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
label: string,
|
||||
): Promise<T> => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = scheduleTimeout(() => reject(new Error(label)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
if (timeoutId !== null) {
|
||||
clearScheduledTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileChanged = (event: { payload: string }) => {
|
||||
const newProfileId = event.payload;
|
||||
const now = Date.now();
|
||||
|
||||
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
|
||||
|
||||
if (
|
||||
lastProfileId === newProfileId &&
|
||||
now - lastUpdateTime < refreshThrottle
|
||||
) {
|
||||
console.log("[AppDataProvider] 重复事件被防抖,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
lastProfileId = newProfileId;
|
||||
lastUpdateTime = now;
|
||||
|
||||
scheduleTimeout(() => {
|
||||
void forceRefreshProxies()
|
||||
.catch((error) => {
|
||||
console.warn("[AppDataProvider] forceRefreshProxies 失败:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
scheduleTimeout(() => {
|
||||
refreshProxy().catch((error) => {
|
||||
console.warn("[AppDataProvider] 普通刷新也失败:", error);
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleRefreshClash = () => {
|
||||
const now = Date.now();
|
||||
console.log("[AppDataProvider] Clash配置刷新事件");
|
||||
|
||||
if (now - lastUpdateTime <= refreshThrottle) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateTime = now;
|
||||
|
||||
scheduleTimeout(async () => {
|
||||
try {
|
||||
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
|
||||
await withTimeout(
|
||||
forceRefreshProxies(),
|
||||
8000,
|
||||
"forceRefreshProxies timeout",
|
||||
);
|
||||
await refreshProxy();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
|
||||
error,
|
||||
);
|
||||
refreshProxy().catch((e) =>
|
||||
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleRefreshProxy = () => {
|
||||
const now = Date.now();
|
||||
console.log("[AppDataProvider] 代理配置刷新事件");
|
||||
|
||||
if (now - lastUpdateTime <= refreshThrottle) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateTime = now;
|
||||
|
||||
scheduleTimeout(() => {
|
||||
refreshProxy().catch((error) =>
|
||||
console.warn("[AppDataProvider] 代理刷新失败:", error),
|
||||
);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleForceRefreshProxies = () => {
|
||||
console.log("[AppDataProvider] 强制代理刷新事件");
|
||||
|
||||
void forceRefreshProxies()
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 强制刷新代理缓存完成");
|
||||
return refreshProxy();
|
||||
})
|
||||
.then(() => {
|
||||
console.log("[AppDataProvider] 前端代理数据刷新完成");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("[AppDataProvider] 强制代理刷新失败:", error);
|
||||
refreshProxy().catch((fallbackError) => {
|
||||
console.warn(
|
||||
"[AppDataProvider] 普通代理刷新也失败:",
|
||||
fallbackError,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initializeListeners = async () => {
|
||||
try {
|
||||
const unlistenProfile = await listen<string>(
|
||||
"profile-changed",
|
||||
handleProfileChanged,
|
||||
);
|
||||
registerCleanup(unlistenProfile);
|
||||
} catch (error) {
|
||||
console.error("[AppDataProvider] 监听 Profile 事件失败:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const unlistenClash = await listen(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
const unlistenProxy = await listen(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
const unlistenForceRefresh = await listen(
|
||||
"verge://force-refresh-proxies",
|
||||
handleForceRefreshProxies,
|
||||
);
|
||||
|
||||
registerCleanup(() => {
|
||||
unlistenClash();
|
||||
unlistenProxy();
|
||||
unlistenForceRefresh();
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||||
|
||||
const fallbackHandlers: Array<[string, EventListener]> = [
|
||||
["verge://refresh-clash-config", handleRefreshClash],
|
||||
["verge://refresh-proxy-config", handleRefreshProxy],
|
||||
["verge://force-refresh-proxies", handleForceRefreshProxies],
|
||||
];
|
||||
|
||||
fallbackHandlers.forEach(([eventName, handler]) => {
|
||||
window.addEventListener(eventName, handler);
|
||||
fallbackWindowListeners.push([eventName, handler]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void initializeListeners();
|
||||
|
||||
return () => {
|
||||
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
|
||||
cleanupPromise.then((cleanup) => cleanup());
|
||||
isUnmounted = true;
|
||||
clearAllTimeouts();
|
||||
fallbackWindowListeners.splice(0).forEach(([eventName, handler]) => {
|
||||
window.removeEventListener(eventName, handler);
|
||||
});
|
||||
cleanupFns.splice(0).forEach((fn) => fn());
|
||||
};
|
||||
}, [refreshProxy]);
|
||||
|
||||
@@ -474,7 +466,7 @@ export const AppDataProvider = ({
|
||||
);
|
||||
|
||||
// 提供统一的刷新方法
|
||||
const refreshAll = React.useCallback(async () => {
|
||||
const refreshAll = useCallback(async () => {
|
||||
await Promise.all([
|
||||
refreshProxy(),
|
||||
refreshClashConfig(),
|
||||
@@ -585,14 +577,3 @@ export const AppDataProvider = ({
|
||||
|
||||
return <AppDataContext value={value}>{children}</AppDataContext>;
|
||||
};
|
||||
|
||||
// 自定义Hook访问全局数据
|
||||
export const useAppData = () => {
|
||||
const context = use(AppDataContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useAppData必须在AppDataProvider内使用");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user