mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20:29 +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",
|
"chokidar": "^4.0.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"webdav": "^5.7.1",
|
"webdav": "^5.7.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
|||||||
express:
|
express:
|
||||||
specifier: ^4.21.0
|
specifier: ^4.21.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.7
|
specifier: ^2.12.7
|
||||||
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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 { calcTraffic } from '@renderer/utils/calc'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { CgClose } from 'react-icons/cg'
|
import { CgClose, CgTrash } from 'react-icons/cg'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number
|
index: number
|
||||||
@ -10,7 +10,7 @@ interface Props {
|
|||||||
selected: IMihomoConnectionDetail | undefined
|
selected: IMihomoConnectionDetail | undefined
|
||||||
setSelected: React.Dispatch<React.SetStateAction<IMihomoConnectionDetail | undefined>>
|
setSelected: React.Dispatch<React.SetStateAction<IMihomoConnectionDetail | undefined>>
|
||||||
setIsDetailModalOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setIsDetailModalOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
close: (id: string) => Promise<void>
|
close: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectionItem: React.FC<Props> = (props) => {
|
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-full flex justify-between">
|
||||||
<div className="w-[calc(100%-48px)]">
|
<div className="w-[calc(100%-48px)]">
|
||||||
<CardHeader className="pb-0 gap-1">
|
<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()})
|
{info.metadata.type}({info.metadata.network.toUpperCase()})
|
||||||
</Chip>
|
</Chip>
|
||||||
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||||
@ -54,7 +54,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
onWheel={(e) => {
|
onWheel={(e) => {
|
||||||
e.currentTarget.scrollLeft += e.deltaY
|
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
|
<Chip
|
||||||
className="flag-emoji text-ellipsis whitespace-nowrap overflow-hidden"
|
className="flag-emoji text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
@ -76,7 +76,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
</CardFooter>
|
</CardFooter>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color={`${info.isActive ? "warning" : "danger"}`}
|
||||||
variant="light"
|
variant="light"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="mr-2 my-auto"
|
className="mr-2 my-auto"
|
||||||
@ -84,7 +84,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
close(info.id)
|
close(info.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CgClose className="text-lg" />
|
{info.isActive ? (<CgClose className="text-lg"/>) : (<CgTrash className="text-lg"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,29 +1,34 @@
|
|||||||
import BasePage from '@renderer/components/base/base-page'
|
import BasePage from '@renderer/components/base/base-page'
|
||||||
import { mihomoCloseAllConnections, mihomoCloseConnection } from '@renderer/utils/ipc'
|
import { mihomoCloseAllConnections, mihomoCloseConnection } from '@renderer/utils/ipc'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { Key, useEffect, useMemo, useState } from 'react'
|
||||||
import { Badge, Button, Divider, Input, Select, SelectItem } from '@nextui-org/react'
|
import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@nextui-org/react'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
import ConnectionItem from '@renderer/components/connections/connection-item'
|
import ConnectionItem from '@renderer/components/connections/connection-item'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal'
|
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 { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { HiSortAscending, HiSortDescending } from 'react-icons/hi'
|
import { HiSortAscending, HiSortDescending } from 'react-icons/hi'
|
||||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||||
|
import { differenceWith, unionWith } from 'lodash'
|
||||||
|
|
||||||
let preData: IMihomoConnectionDetail[] = []
|
let cachedConnections: IMihomoConnectionDetail[] = []
|
||||||
|
|
||||||
const Connections: React.FC = () => {
|
const Connections: React.FC = () => {
|
||||||
const [filter, setFilter] = useState('')
|
const [filter, setFilter] = useState('')
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { connectionDirection = 'asc', connectionOrderBy = 'time' } = appConfig || {}
|
const { connectionDirection = 'asc', connectionOrderBy = 'time' } = appConfig || {}
|
||||||
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
|
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 [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
|
||||||
const [selected, setSelected] = useState<IMihomoConnectionDetail>()
|
const [selected, setSelected] = useState<IMihomoConnectionDetail>()
|
||||||
|
const [tab, setTab] = useState('active')
|
||||||
|
|
||||||
const filteredConnections = useMemo(() => {
|
const filteredConnections = useMemo(() => {
|
||||||
|
const connections = tab === 'active' ? activeConnections : closedConnections
|
||||||
if (connectionOrderBy) {
|
if (connectionOrderBy) {
|
||||||
connections.sort((a, b) => {
|
connections.sort((a, b) => {
|
||||||
if (connectionDirection === 'asc') {
|
if (connectionDirection === 'asc') {
|
||||||
@ -60,29 +65,69 @@ const Connections: React.FC = () => {
|
|||||||
const raw = JSON.stringify(connection)
|
const raw = JSON.stringify(connection)
|
||||||
return includesIgnoreCase(raw, filter)
|
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(() => {
|
useEffect(() => {
|
||||||
window.electron.ipcRenderer.on('mihomoConnections', (_e, info: IMihomoConnectionsInfo) => {
|
window.electron.ipcRenderer.on('mihomoConnections', (_e, info: IMihomoConnectionsInfo) => {
|
||||||
setConnectionsInfo(info)
|
setConnectionsInfo(info)
|
||||||
const newConns: IMihomoConnectionDetail[] = []
|
|
||||||
for (const conn of info.connections ?? []) {
|
|
||||||
const preConn = preData?.find((c) => c.id === conn.id)
|
|
||||||
|
|
||||||
if (preConn) {
|
if (!info.connections) return
|
||||||
conn.downloadSpeed = conn.download - preConn.download
|
const allConns = unionWith(allConnections, activeConnections, (a, b) => a.id === b.id)
|
||||||
conn.uploadSpeed = conn.upload - preConn.upload
|
|
||||||
|
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 => {
|
return (): void => {
|
||||||
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [allConnections, activeConnections, closedConnections])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
@ -112,15 +157,15 @@ const Connections: React.FC = () => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (filter === '') {
|
if (filter === '') {
|
||||||
mihomoCloseAllConnections()
|
closeAllConnections()
|
||||||
} else {
|
} else {
|
||||||
filteredConnections.forEach((conn) => {
|
filteredConnections.forEach((conn) => {
|
||||||
mihomoCloseConnection(conn.id)
|
closeConnection(conn.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CgClose className="text-lg" />
|
{tab === 'active' ? (<CgClose className="text-lg"/>) : (<CgTrash className="text-lg"/>)}
|
||||||
</Button>
|
</Button>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@ -131,6 +176,47 @@ const Connections: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="overflow-x-auto sticky top-0 z-40">
|
<div className="overflow-x-auto sticky top-0 z-40">
|
||||||
<div className="flex p-2 gap-2">
|
<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
|
<Input
|
||||||
variant="flat"
|
variant="flat"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -142,7 +228,7 @@ const Connections: React.FC = () => {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[180px]"
|
className="w-[180px] min-w-[120px]"
|
||||||
selectedKeys={new Set([connectionOrderBy])}
|
selectedKeys={new Set([connectionOrderBy])}
|
||||||
onSelectionChange={async (v) => {
|
onSelectionChange={async (v) => {
|
||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
@ -188,7 +274,7 @@ const Connections: React.FC = () => {
|
|||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
setIsDetailModalOpen={setIsDetailModalOpen}
|
setIsDetailModalOpen={setIsDetailModalOpen}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
close={mihomoCloseConnection}
|
close={closeConnection}
|
||||||
index={i}
|
index={i}
|
||||||
key={connection.id}
|
key={connection.id}
|
||||||
info={connection}
|
info={connection}
|
||||||
|
|||||||
1
src/shared/types.d.ts
vendored
1
src/shared/types.d.ts
vendored
@ -75,6 +75,7 @@ interface IMihomoConnectionsInfo {
|
|||||||
|
|
||||||
interface IMihomoConnectionDetail {
|
interface IMihomoConnectionDetail {
|
||||||
id: string
|
id: string
|
||||||
|
isActive: boolean
|
||||||
metadata: {
|
metadata: {
|
||||||
network: 'tcp' | 'udp'
|
network: 'tcp' | 'udp'
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user