Compare commits

...

3 Commits

3 changed files with 102 additions and 81 deletions

View File

@@ -187,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections?.connections.length,
connectionsCount: connections?.activeConnections.length,
};
}, [traffic, memory, connections]);

View File

@@ -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<boolean>(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<IConnections, any, string | null>(
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,14 @@ export const useConnectionData = () => {
setDate(Date.now());
};
return { response, refreshGetClashConnection };
const clearClosedConnections = () => {
mutate(`$sub$${subscriptKey}`, {
uploadTotal: response.data?.uploadTotal ?? 0,
downloadTotal: response.data?.downloadTotal ?? 0,
activeConnections: response.data?.activeConnections ?? [],
closedConnections: [],
} as ConnectionMonitorData);
};
return { response, refreshGetClashConnection, clearClosedConnections };
};

View File

@@ -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<IConnections | null>(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 (
<BasePage
full
@@ -140,10 +107,10 @@ const ConnectionsPage = () => {
header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}>
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
{t("Downloaded")}: {parseTraffic(connections?.downloadTotal)}
</Box>
<Box sx={{ mx: 1 }}>
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
{t("Uploaded")}: {parseTraffic(connections?.uploadTotal)}
</Box>
<IconButton
color="inherit"
@@ -162,18 +129,6 @@ const ConnectionsPage = () => {
<TableChartRounded titleAccess={t("Table View")} />
)}
</IconButton>
<IconButton
color="inherit"
size="small"
onClick={handlePauseToggle}
title={isPaused ? t("Resume") : t("Pause")}
>
{isPaused ? (
<PlayCircleOutlineRounded />
) : (
<PauseCircleOutlineRounded />
)}
</IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}>
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
</Button>
@@ -194,6 +149,22 @@ const ConnectionsPage = () => {
zIndex: 2,
}}
>
<ButtonGroup sx={{ mr: 1, flexBasis: "content" }}>
<Button
size="small"
variant={connectionsType === "active" ? "contained" : "outlined"}
onClick={() => setConnectionsType("active")}
>
{t("Active")} {connections?.activeConnections.length}
</Button>
<Button
size="small"
variant={connectionsType === "closed" ? "contained" : "outlined"}
onClick={() => setConnectionsType("closed")}
>
{t("Closed")} {connections?.closedConnections.length}
</Button>
</ButtonGroup>
{!isTableLayout && (
<BaseStyledSelect
value={curOrderOpt}
@@ -232,6 +203,17 @@ const ConnectionsPage = () => {
/>
)}
<ConnectionDetail ref={detailRef} />
{connectionsType === "closed" && (
<Fab
variant="extended"
sx={{ position: "absolute", right: 16, bottom: 16 }}
color="primary"
onClick={() => clearClosedConnections()}
>
<Clear sx={{ mr: 1 }} />
{t("Clear")}
</Fab>
)}
</BasePage>
);
};