import { useMemo, useRef, useState, useCallback } 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, PlayCircleOutlineRounded, PauseCircleOutlineRounded, } 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, type SearchState, } 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 [isPaused, setIsPaused] = useState(false); const [frozenData, setFrozenData] = useState(null); 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) { const data = JSON.parse(event.data) as IConnections; 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 displayData = useMemo(() => { return isPaused ? (frozenData ?? connData) : connData; }, [isPaused, frozenData, connData]); const [filterConn] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; let connections = displayData.connections.filter((conn) => { const { host, destinationIP, process } = conn.metadata; return ( match(host || "") || match(destinationIP || "") || match(process || "") ); }); if (orderFunc) connections = orderFunc(connections); return [connections]; }, [displayData, match, curOrderOpt]); const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!); const handleSearch = useCallback((match: (content: string) => boolean) => { setMatch(() => match); }, []); const handlePauseToggle = useCallback(() => { setIsPaused((prev) => { if (!prev) { setFrozenData(connData); } else { setFrozenData(null); } return !prev; }); }, [connData]); return ( {t("Connections")}} contentStyle={{ height: "100%", display: "flex", flexDirection: "column", overflow: "auto", borderRadius: "8px", }} header={ {t("Downloaded")}: {parseTraffic(displayData.downloadTotal)} {t("Uploaded")}: {parseTraffic(displayData.uploadTotal)} setSetting((o) => o?.layout !== "table" ? { ...o, layout: "table" } : { ...o, layout: "list" }, ) } > {isTableLayout ? ( ) : ( )} {isPaused ? ( ) : ( )} } > {!isTableLayout && ( setOrderOpt(e.target.value)} > {Object.keys(orderOpts).map((opt) => ( {t(opt)} ))} )} {filterConn.length === 0 ? ( ) : isTableLayout ? ( detailRef.current?.open(detail)} /> ) : ( ( detailRef.current?.open(item)} /> )} /> )} ); }; export default ConnectionsPage;