add closed connections sub page (#180)

This commit is contained in:
DtHnAme 2024-09-23 22:45:45 +08:00 committed by pompurin404
parent bbb0efd1cf
commit 8879c9e165
No known key found for this signature in database
5 changed files with 119 additions and 28 deletions

View File

@ -30,6 +30,7 @@
"chokidar": "^4.0.0",
"dayjs": "^1.11.13",
"express": "^4.21.0",
"lodash": "^4.17.21",
"recharts": "^2.12.7",
"webdav": "^5.7.1",
"ws": "^8.18.0",

View File

@ -32,6 +32,9 @@ importers:
express:
specifier: ^4.21.0
version: 4.21.0
lodash:
specifier: ^4.17.21
version: 4.17.21
recharts:
specifier: ^2.12.7
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)

View File

@ -2,7 +2,7 @@ import { Button, Card, CardFooter, CardHeader, Chip } from '@nextui-org/react'
import { calcTraffic } from '@renderer/utils/calc'
import dayjs from 'dayjs'
import React, { useEffect } from 'react'
import { CgClose } from 'react-icons/cg'
import { CgClose, CgTrash } from 'react-icons/cg'
interface Props {
index: number
@ -10,7 +10,7 @@ interface Props {
selected: IMihomoConnectionDetail | undefined
setSelected: React.Dispatch<React.SetStateAction<IMihomoConnectionDetail | undefined>>
setIsDetailModalOpen: React.Dispatch<React.SetStateAction<boolean>>
close: (id: string) => Promise<void>
close: (id: string) => void
}
const ConnectionItem: React.FC<Props> = (props) => {
@ -35,7 +35,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
<div className="w-full flex justify-between">
<div className="w-[calc(100%-48px)]">
<CardHeader className="pb-0 gap-1">
<Chip color="primary" size="sm" radius="sm" variant="light">
<Chip color={`${info.isActive ? "primary": "danger"}`} size="sm" radius="sm" variant="dot">
{info.metadata.type}({info.metadata.network.toUpperCase()})
</Chip>
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
@ -54,7 +54,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
onWheel={(e) => {
e.currentTarget.scrollLeft += e.deltaY
}}
className="overscroll-contain pt-1 flex justify-start gap-1 overflow-x-auto no-scrollbar"
className="overscroll-contain pt-2 flex justify-start gap-1 overflow-x-auto no-scrollbar"
>
<Chip
className="flag-emoji text-ellipsis whitespace-nowrap overflow-hidden"
@ -76,7 +76,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
</CardFooter>
</div>
<Button
color="warning"
color={`${info.isActive ? "warning" : "danger"}`}
variant="light"
isIconOnly
className="mr-2 my-auto"
@ -84,7 +84,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
close(info.id)
}}
>
<CgClose className="text-lg" />
{info.isActive ? (<CgClose className="text-lg"/>) : (<CgTrash className="text-lg"/>)}
</Button>
</div>
</Card>

View File

@ -1,29 +1,34 @@
import BasePage from '@renderer/components/base/base-page'
import { mihomoCloseAllConnections, mihomoCloseConnection } from '@renderer/utils/ipc'
import { useEffect, useMemo, useState } from 'react'
import { Badge, Button, Divider, Input, Select, SelectItem } from '@nextui-org/react'
import { Key, useEffect, useMemo, useState } from 'react'
import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@nextui-org/react'
import { calcTraffic } from '@renderer/utils/calc'
import ConnectionItem from '@renderer/components/connections/connection-item'
import { Virtuoso } from 'react-virtuoso'
import dayjs from 'dayjs'
import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal'
import { CgClose } from 'react-icons/cg'
import { CgClose, CgTrash } from 'react-icons/cg'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { HiSortAscending, HiSortDescending } from 'react-icons/hi'
import { includesIgnoreCase } from '@renderer/utils/includes'
import { differenceWith, unionWith } from 'lodash'
let preData: IMihomoConnectionDetail[] = []
let cachedConnections: IMihomoConnectionDetail[] = []
const Connections: React.FC = () => {
const [filter, setFilter] = useState('')
const { appConfig, patchAppConfig } = useAppConfig()
const { connectionDirection = 'asc', connectionOrderBy = 'time' } = appConfig || {}
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
const [connections, setConnections] = useState<IMihomoConnectionDetail[]>([])
const [allConnections, setAllConnections] = useState<IMihomoConnectionDetail[]>(cachedConnections)
const [activeConnections, setActiveConnections] = useState<IMihomoConnectionDetail[]>([])
const [closedConnections, setClosedConnections] = useState<IMihomoConnectionDetail[]>([])
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
const [selected, setSelected] = useState<IMihomoConnectionDetail>()
const [tab, setTab] = useState('active')
const filteredConnections = useMemo(() => {
const connections = tab === 'active' ? activeConnections : closedConnections
if (connectionOrderBy) {
connections.sort((a, b) => {
if (connectionDirection === 'asc') {
@ -60,29 +65,69 @@ const Connections: React.FC = () => {
const raw = JSON.stringify(connection)
return includesIgnoreCase(raw, filter)
})
}, [connections, filter, connectionDirection, connectionOrderBy])
}, [activeConnections, closedConnections, filter, connectionDirection, connectionOrderBy])
const closeAllConnections = () => {
tab === 'active' ? mihomoCloseAllConnections() : trashAllClosedConnection()
}
const closeConnection = (id: string) => {
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
}
const trashAllClosedConnection = () => {
const trashIds = closedConnections.map((conn) => conn.id)
setAllConnections((allConns) => allConns.filter((conn) => !trashIds.includes(conn.id)))
setClosedConnections([])
cachedConnections = allConnections
}
const trashClosedConnection = (id: string) => {
setAllConnections((allConns) => allConns.filter((conn) => conn.id != id))
setClosedConnections((closedConns) => closedConns.filter((conn) => conn.id != id))
cachedConnections = allConnections
}
useEffect(() => {
window.electron.ipcRenderer.on('mihomoConnections', (_e, info: IMihomoConnectionsInfo) => {
setConnectionsInfo(info)
const newConns: IMihomoConnectionDetail[] = []
for (const conn of info.connections ?? []) {
const preConn = preData?.find((c) => c.id === conn.id)
if (preConn) {
conn.downloadSpeed = conn.download - preConn.download
conn.uploadSpeed = conn.upload - preConn.upload
if (!info.connections) return
const allConns = unionWith(allConnections, activeConnections, (a, b) => a.id === b.id)
const activeConns = info.connections.map((conn) => {
const preConn = activeConnections.find((c) => c.id === conn.id)
const downloadSpeed = preConn ? conn.download - preConn.download : 0
const uploadSpeed = preConn ? conn.upload - preConn.upload : 0
return {
...conn,
isActive: true,
downloadSpeed: downloadSpeed,
uploadSpeed: uploadSpeed,
}
newConns.push(conn)
})
const closedConns = differenceWith(allConns, activeConns, (a, b) => a.id === b.id).map((conn) => {
return {
...conn,
isActive: false,
downloadSpeed: 0,
uploadSpeed: 0,
}
setConnections(newConns)
preData = newConns
})
setActiveConnections(activeConns)
setClosedConnections(closedConns)
setAllConnections(allConns.slice(-(activeConns.length + 200)))
cachedConnections = allConnections
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
}
}, [])
}, [allConnections, activeConnections, closedConnections])
return (
<BasePage
@ -112,15 +157,15 @@ const Connections: React.FC = () => {
variant="light"
onPress={() => {
if (filter === '') {
mihomoCloseAllConnections()
closeAllConnections()
} else {
filteredConnections.forEach((conn) => {
mihomoCloseConnection(conn.id)
closeConnection(conn.id)
})
}
}}
>
<CgClose className="text-lg" />
{tab === 'active' ? (<CgClose className="text-lg"/>) : (<CgTrash className="text-lg"/>)}
</Button>
</Badge>
</div>
@ -131,6 +176,47 @@ const Connections: React.FC = () => {
)}
<div className="overflow-x-auto sticky top-0 z-40">
<div className="flex p-2 gap-2">
<Tabs
size="sm"
color={`${tab === 'active' ? "primary" : "danger" }`}
selectedKey={tab}
variant="underlined"
className="w-fit h-[32px]"
onSelectionChange={(key: Key) => {
setTab(key as string)
}}
>
<Tab
key="active"
title={
<Badge
color={`${tab === 'active' ? "primary" : "default"}`}
size="sm"
shape="circle"
variant="flat"
content={activeConnections.length}
showOutline={false}
>
<span className="p-1"></span>
</Badge>
}
/>
<Tab
key="closed"
title={
<Badge
color={`${tab === 'closed' ? "danger" : "default"}`}
size="sm"
shape="circle"
variant="flat"
content={closedConnections.length}
showOutline={false}
>
<span className="p-1"></span>
</Badge>
}
/>
</Tabs>
<Input
variant="flat"
size="sm"
@ -142,7 +228,7 @@ const Connections: React.FC = () => {
<Select
size="sm"
className="w-[180px]"
className="w-[180px] min-w-[120px]"
selectedKeys={new Set([connectionOrderBy])}
onSelectionChange={async (v) => {
await patchAppConfig({
@ -188,7 +274,7 @@ const Connections: React.FC = () => {
setSelected={setSelected}
setIsDetailModalOpen={setIsDetailModalOpen}
selected={selected}
close={mihomoCloseConnection}
close={closeConnection}
index={i}
key={connection.id}
info={connection}

View File

@ -75,6 +75,7 @@ interface IMihomoConnectionsInfo {
interface IMihomoConnectionDetail {
id: string
isActive: boolean
metadata: {
network: 'tcp' | 'udp'
type: string