Compare commits

..

2 Commits

6 changed files with 109 additions and 123 deletions

34
src-tauri/Cargo.lock generated
View File

@@ -147,7 +147,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot 0.12.5",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -1243,7 +1243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1935,7 +1935,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2191,7 +2191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3356,7 +3356,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.1",
"system-configuration",
"tokio",
"tower-service",
@@ -3376,7 +3376,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
"windows-core 0.61.2",
]
[[package]]
@@ -4456,7 +4456,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4950,7 +4950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.45.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5751,7 +5751,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -5788,9 +5788,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.1",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -6283,7 +6283,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6296,7 +6296,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7266,9 +7266,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.9.1"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a"
checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5"
dependencies = [
"anyhow",
"bytes",
@@ -7799,7 +7799,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.2",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -9165,7 +9165,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]

View File

@@ -43,7 +43,7 @@ serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.9.1", features = [
tauri = { version = "2.9.2", features = [
"protocol-asset",
"devtools",
"tray-icon",

View File

@@ -96,10 +96,17 @@ impl NotificationSystem {
let handle = Handle::global();
while !handle.is_exiting() {
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
match rx.recv() {
Ok(event) => Self::process_event(handle, event),
Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(e) => {
logging!(
error,
Type::System,
"receive event error, stop notification worker: {}",
e
);
break;
}
}
}
}

View File

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

View File

@@ -4,22 +4,12 @@ import { mutate } from "swr";
import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export const initConnData: ConnectionMonitorData = {
export const initConnData: IConnections = {
uploadTotal: 0,
downloadTotal: 0,
activeConnections: [],
closedConnections: [],
connections: [],
};
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}`;
@@ -28,11 +18,7 @@ export const useConnectionData = () => {
const wsFirstConnection = useRef<boolean>(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<
ConnectionMonitorData,
any,
string | null
>(
const response = useSWRSubscription<IConnections, any, string | null>(
subscriptKey,
(_key, { next }) => {
const reconnect = async () => {
@@ -55,44 +41,28 @@ export const useConnectionData = () => {
} else {
const data = JSON.parse(msg.data) as IConnections;
next(null, (old = initConnData) => {
const oldConn = old.activeConnections;
const oldConn = old.connections;
const maxLen = data.connections?.length;
const activeConns: IConnectionsItem[] = [];
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;
activeConns[index] = each;
connections[index] = each;
return false;
}
return true;
});
for (let i = 0; i < maxLen; ++i) {
if (!activeConns[i] && rest.length > 0) {
activeConns[i] = rest.shift()!;
activeConns[i].curUpload = 0;
activeConns[i].curDownload = 0;
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
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;
return { ...data, connections };
});
}
}
@@ -139,14 +109,5 @@ export const useConnectionData = () => {
setDate(Date.now());
};
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 };
return { response, refreshGetClashConnection };
};

View File

@@ -1,16 +1,10 @@
import {
Clear,
PauseCircleOutlineRounded,
PlayCircleOutlineRounded,
TableChartRounded,
TableRowsRounded,
} from "@mui/icons-material";
import {
Box,
Button,
ButtonGroup,
Fab,
IconButton,
MenuItem,
} from "@mui/material";
import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { useLockFn } from "ahooks";
import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -27,24 +21,28 @@ 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();
@@ -67,23 +65,43 @@ 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];
const conns =
(connectionsType === "active"
? connections?.activeConnections
: connections?.closedConnections) ?? [];
let matchConns = conns.filter((conn) => {
let conns = displayData.connections?.filter((conn) => {
const { host, destinationIP, process } = conn.metadata;
return (
match(host || "") || match(destinationIP || "") || match(process || "")
);
});
if (orderFunc) matchConns = orderFunc(matchConns ?? []);
if (orderFunc) conns = orderFunc(conns ?? []);
return [matchConns];
}, [connections, connectionsType, match, curOrderOpt, orderOpts]);
return [conns];
}, [displayData, match, curOrderOpt, orderOpts]);
const onCloseAll = useLockFn(closeAllConnections);
@@ -93,6 +111,21 @@ 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
@@ -107,10 +140,10 @@ const ConnectionsPage = () => {
header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}>
{t("Downloaded")}: {parseTraffic(connections?.downloadTotal)}
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
</Box>
<Box sx={{ mx: 1 }}>
{t("Uploaded")}: {parseTraffic(connections?.uploadTotal)}
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
</Box>
<IconButton
color="inherit"
@@ -129,6 +162,18 @@ 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>
@@ -149,22 +194,6 @@ 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}
@@ -203,17 +232,6 @@ 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>
);
};