import { useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { Virtuoso } from "react-virtuoso"; import { useTranslation } from "react-i18next"; import { TableChartRounded, TableRowsRounded } from "@mui/icons-material"; import { closeAllConnections } from "@/services/api"; import { useConnectionSetting } from "@/services/states"; import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; 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 { BaseSearchBox } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import useSWRSubscription from "swr/subscription"; import { createSockette } from "@/utils/websocket"; import { useTheme } from "@mui/material/styles"; import { useVisibility } from "@/hooks/use-visibility"; const initConn: IConnections = { uploadTotal: 0, downloadTotal: 0, connections: [], }; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ConnectionsPage = () => { const { t } = useTranslation(); const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); const theme = useTheme(); const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); const [curOrderOpt, setOrderOpt] = useState("Default"); const [setting, setSetting] = useConnectionSetting(); 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 { data: connData = initConn } = useSWRSubscription< IConnections, any, "getClashConnections" | null >( clashInfo && pageVisible ? "getClashConnections" : null, (_key, { next }) => { const { server = "", secret = "" } = clashInfo!; const s = createSockette( `ws://${server}/connections?token=${encodeURIComponent(secret)}`, { onmessage(event) { // meta v1.15.0 出现 data.connections 为 null 的情况 const data = JSON.parse(event.data) as IConnections; // 尽量与前一次 connections 的展示顺序保持一致 next(null, (old = initConn) => { const oldConn = old.connections; const maxLen = data.connections?.length; const connections: 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; 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 }; }); }, onerror(event) { next(event); }, }, 3, ); return () => { s.close(); }; }, ); const [filterConn, download, upload] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; let connections = connData.connections.filter((conn) => match(conn.metadata.host || conn.metadata.destinationIP || ""), ); 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, match, curOrderOpt]); const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!); return ( {t("Connections")}} contentStyle={{ height: "100%", display: "flex", flexDirection: "column", overflow: "auto", // margin: "0 10px", // backgroundColor: isDark ? "#282a36" : "#ffffff", borderRadius: "8px", }} header={ {t("Downloaded")}: {parseTraffic(download)} {t("Uploaded")}: {parseTraffic(upload)} setSetting((o) => o?.layout !== "table" ? { ...o, layout: "table" } : { ...o, layout: "list" }, ) } > {isTableLayout ? ( ) : ( )} } > {!isTableLayout && ( setOrderOpt(e.target.value)} > {Object.keys(orderOpts).map((opt) => ( {t(opt)} ))} )} setMatch(() => match)} /> {filterConn.length === 0 ? ( ) : isTableLayout ? ( detailRef.current?.open(detail)} /> ) : ( ( detailRef.current?.open(item)} /> )} /> )} ); }; export default ConnectionsPage;