diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 199bbd6f..6973d9de 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -187,7 +187,7 @@ export const EnhancedTrafficStats = () => { uploadTotalUnit, downloadTotal, downloadTotalUnit, - connectionsCount: connections?.connections.length, + connectionsCount: connections?.activeConnections.length, }; }, [traffic, memory, connections]); diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index f599882b..7d1c9569 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -4,12 +4,22 @@ import { mutate } from "swr"; import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; -export const initConnData: IConnections = { +export const initConnData: ConnectionMonitorData = { uploadTotal: 0, downloadTotal: 0, - connections: [], + activeConnections: [], + closedConnections: [], }; +export interface ConnectionMonitorData { + uploadTotal: 0; + downloadTotal: 0; + activeConnections: IConnectionsItem[]; + closedConnections: IConnectionsItem[]; +} + +const MAX_CLOSED_CONNS_NUM = 500; + export const useConnectionData = () => { const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); const subscriptKey = `getClashConnection-${date}`; @@ -18,7 +28,11 @@ export const useConnectionData = () => { const wsFirstConnection = useRef(true); const timeoutRef = useRef>(null); - const response = useSWRSubscription( + const response = useSWRSubscription< + ConnectionMonitorData, + any, + string | null + >( subscriptKey, (_key, { next }) => { const reconnect = async () => { @@ -41,28 +55,44 @@ export const useConnectionData = () => { } else { const data = JSON.parse(msg.data) as IConnections; next(null, (old = initConnData) => { - const oldConn = old.connections; + const oldConn = old.activeConnections; const maxLen = data.connections?.length; - const connections: IConnectionsItem[] = []; + const activeConns: IConnectionsItem[] = []; const rest = (data.connections || []).filter((each) => { const index = oldConn.findIndex((o) => o.id === each.id); if (index >= 0 && index < maxLen) { const old = oldConn[index]; each.curUpload = each.upload - old.upload; each.curDownload = each.download - old.download; - connections[index] = each; + activeConns[index] = each; return false; } return true; }); for (let i = 0; i < maxLen; ++i) { - if (!connections[i] && rest.length > 0) { - connections[i] = rest.shift()!; - connections[i].curUpload = 0; - connections[i].curDownload = 0; + if (!activeConns[i] && rest.length > 0) { + activeConns[i] = rest.shift()!; + activeConns[i].curUpload = 0; + activeConns[i].curDownload = 0; } } - return { ...data, connections }; + const currentClosedConns = oldConn.filter((each) => { + const index = activeConns.findIndex( + (o) => o.id === each.id, + ); + return index < 0; + }); + let closedConns = + old.closedConnections.concat(currentClosedConns); + if (closedConns.length > 500) { + closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM); + } + return { + uploadTotal: data.uploadTotal, + downloadTotal: data.downloadTotal, + activeConnections: activeConns, + closedConnections: closedConns, + } as ConnectionMonitorData; }); } } @@ -109,5 +139,12 @@ export const useConnectionData = () => { setDate(Date.now()); }; - return { response, refreshGetClashConnection }; + const clearClosedConnections = () => { + mutate(`$sub$${subscriptKey}`, { + activeConnections: response.data?.activeConnections, + closedConnections: [], + }); + }; + + return { response, refreshGetClashConnection, clearClosedConnections }; }; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index bb68f792..81cfe751 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,10 +1,16 @@ import { - PauseCircleOutlineRounded, - PlayCircleOutlineRounded, + Clear, TableChartRounded, TableRowsRounded, } from "@mui/icons-material"; -import { Box, Button, IconButton, MenuItem } from "@mui/material"; +import { + Box, + Button, + ButtonGroup, + Fab, + IconButton, + MenuItem, +} from "@mui/material"; import { useLockFn } from "ahooks"; import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -21,28 +27,24 @@ import { import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { useConnectionData } from "@/hooks/use-connection-data"; -import { useVisibility } from "@/hooks/use-visibility"; import { useConnectionSetting } from "@/services/states"; import parseTraffic from "@/utils/parse-traffic"; -const initConn: IConnections = { - uploadTotal: 0, - downloadTotal: 0, - connections: [], -}; - type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ConnectionsPage = () => { const { t } = useTranslation(); - const pageVisible = useVisibility(); const [match, setMatch] = useState<(input: string) => boolean>( () => () => true, ); const [curOrderOpt, setCurOrderOpt] = useState("Default"); + const [connectionsType, setConnectionsType] = useState<"active" | "closed">( + "active", + ); const { response: { data: connections }, + clearClosedConnections, } = useConnectionData(); const [setting, setSetting] = useConnectionSetting(); @@ -65,43 +67,23 @@ const ConnectionsPage = () => { [], ); - const [isPaused, setIsPaused] = useState(false); - const [frozenData, setFrozenData] = useState(null); - - // 使用全局连接数据 - const displayData = useMemo(() => { - if (!pageVisible) return initConn; - - if (isPaused) { - return ( - frozenData ?? { - uploadTotal: connections?.uploadTotal, - downloadTotal: connections?.downloadTotal, - connections: connections?.connections, - } - ); - } - - return { - uploadTotal: connections?.uploadTotal, - downloadTotal: connections?.downloadTotal, - connections: connections?.connections, - }; - }, [isPaused, frozenData, connections, pageVisible]); - const [filterConn] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; - let conns = displayData.connections?.filter((conn) => { + const conns = + (connectionsType === "active" + ? connections?.activeConnections + : connections?.closedConnections) ?? []; + let matchConns = conns.filter((conn) => { const { host, destinationIP, process } = conn.metadata; return ( match(host || "") || match(destinationIP || "") || match(process || "") ); }); - if (orderFunc) conns = orderFunc(conns ?? []); + if (orderFunc) matchConns = orderFunc(matchConns ?? []); - return [conns]; - }, [displayData, match, curOrderOpt, orderOpts]); + return [matchConns]; + }, [connections, connectionsType, match, curOrderOpt, orderOpts]); const onCloseAll = useLockFn(closeAllConnections); @@ -111,21 +93,6 @@ const ConnectionsPage = () => { setMatch(() => match); }, []); - const handlePauseToggle = useCallback(() => { - setIsPaused((prev) => { - if (!prev) { - setFrozenData({ - uploadTotal: connections?.uploadTotal ?? 0, - downloadTotal: connections?.downloadTotal ?? 0, - connections: connections?.connections ?? [], - }); - } else { - setFrozenData(null); - } - return !prev; - }); - }, [connections]); - return ( { header={ - {t("Downloaded")}: {parseTraffic(displayData.downloadTotal)} + {t("Downloaded")}: {parseTraffic(connections?.downloadTotal)} - {t("Uploaded")}: {parseTraffic(displayData.uploadTotal)} + {t("Uploaded")}: {parseTraffic(connections?.uploadTotal)} { )} - - {isPaused ? ( - - ) : ( - - )} - @@ -194,6 +149,22 @@ const ConnectionsPage = () => { zIndex: 2, }} > + + + + {!isTableLayout && ( { /> )} + {connectionsType === "closed" && ( + clearClosedConnections()} + > + + {t("Clear")} + + )} ); };