mirror of
https://github.com/pompurin404/mihomo-party.git
synced 2024-11-15 19:22:31 +08:00
add closed connections sub page (#180)
This commit is contained in:
parent
bbb0efd1cf
commit
8879c9e165
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
setConnections(newConns)
|
||||
preData = newConns
|
||||
})
|
||||
const closedConns = differenceWith(allConns, activeConns, (a, b) => a.id === b.id).map((conn) => {
|
||||
return {
|
||||
...conn,
|
||||
isActive: false,
|
||||
downloadSpeed: 0,
|
||||
uploadSpeed: 0,
|
||||
}
|
||||
})
|
||||
|
||||
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}
|
||||
|
|
1
src/shared/types.d.ts
vendored
1
src/shared/types.d.ts
vendored
|
@ -75,6 +75,7 @@ interface IMihomoConnectionsInfo {
|
|||
|
||||
interface IMihomoConnectionDetail {
|
||||
id: string
|
||||
isActive: boolean
|
||||
metadata: {
|
||||
network: 'tcp' | 'udp'
|
||||
type: string
|
||||
|
|
Loading…
Reference in New Issue
Block a user