import { useEffect, useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, IconButton, MenuItem, Select } from "@mui/material"; import { useRecoilState } from "recoil"; import { Virtuoso } from "react-virtuoso"; import { useTranslation } from "react-i18next"; import { TableChartRounded, TableRowsRounded } from "@mui/icons-material"; import { closeAllConnections } from "@/services/api"; import { atomConnectionSetting } from "@/services/states"; import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; import { useWebsocket } from "@/hooks/use-websocket"; import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { ConnectionDetail, ConnectionDetailRef, } from "@/components/connection/connection-detail"; import parseTraffic from "@/utils/parse-traffic"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ConnectionsPage = () => { const { t, i18n } = useTranslation(); const { clashInfo } = useClashInfo(); const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; const [filterText, setFilterText] = useState(""); const [curOrderOpt, setOrderOpt] = useState("Default"); const [connData, setConnData] = useState(initConn); const [setting, setSetting] = useRecoilState(atomConnectionSetting); const isTableLayout = setting.layout === "table"; const orderOpts: Record = { Default: (list) => list, "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), "Download Speed": (list) => list.sort((a, b) => b.curDownload! - a.curDownload!), }; const [filterConn, download, upload] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; let connections = connData.connections.filter((conn) => (conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText) ); if (orderFunc) connections = orderFunc(connections); let download = 0; let upload = 0; connections.forEach((x) => { download += x.download; upload += x.upload; }); return [connections, download, upload]; }, [connData, filterText, curOrderOpt]); const { connect, disconnect } = useWebsocket( (event) => { // meta v1.15.0 出现data.connections为null的情况 const data = JSON.parse(event.data) as IConnections; // 尽量与前一次connections的展示顺序保持一致 setConnData((old) => { const oldConn = old.connections; const maxLen = data.connections?.length; const connections: typeof oldConn = []; 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; 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; } } return { ...data, connections }; }); }, { errorCount: 3, retryInterval: 1000 } ); useEffect(() => { if (!clashInfo) return; const { server = "", secret = "" } = clashInfo; connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`); return () => { disconnect(); }; }, [clashInfo]); const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!); return ( Download: {parseTraffic(download)} Upload: {parseTraffic(upload)} setSetting((o) => o.layout === "list" ? { ...o, layout: "table" } : { ...o, layout: "list" } ) } > {isTableLayout ? ( ) : ( )} } > {!isTableLayout && ( )} setFilterText(e.target.value)} /> {filterConn.length === 0 ? ( ) : isTableLayout ? ( detailRef.current?.open(detail)} /> ) : ( ( detailRef.current?.open(item)} /> )} /> )} ); }; export default ConnectionsPage;