2024-08-31 22:41:06 +08:00

321 lines
12 KiB
TypeScript

import { Avatar, Button, Card, CardBody, Chip } from '@nextui-org/react'
import BasePage from '@renderer/components/base/base-page'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import {
mihomoChangeProxy,
mihomoCloseAllConnections,
mihomoGroupDelay,
mihomoGroups,
mihomoProxyDelay
} from '@renderer/utils/ipc'
import { CgDetailsLess, CgDetailsMore } from 'react-icons/cg'
import { FaBoltLightning } from 'react-icons/fa6'
import { TbCircleLetterD } from 'react-icons/tb'
import { FaLocationCrosshairs } from 'react-icons/fa6'
import { RxLetterCaseCapitalize } from 'react-icons/rx'
import { useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { GroupedVirtuoso, GroupedVirtuosoHandle } from 'react-virtuoso'
import ProxyItem from '@renderer/components/proxies/proxy-item'
import { IoIosArrowBack } from 'react-icons/io'
import { MdOutlineSpeed } from 'react-icons/md'
const Proxies: React.FC = () => {
const { data: groups = [], mutate } = useSWR('mihomoGroups', mihomoGroups)
const { appConfig, patchAppConfig } = useAppConfig()
const {
proxyDisplayMode = 'simple',
proxyDisplayOrder = 'default',
autoCloseConnection = true,
proxyCols = 'auto'
} = appConfig || {}
const [cols, setCols] = useState(1)
const [isOpen, setIsOpen] = useState(Array(groups.length).fill(false))
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
const { groupCounts, allProxies } = useMemo(() => {
const groupCounts = groups.map((group, index) => {
if (!isOpen[index]) return 0
const count = Math.floor(group.all.length / cols)
return group.all.length % cols === 0 ? count : count + 1
})
const allProxies: (IMihomoProxy | IMihomoGroup)[][] = []
groups.forEach((group, index) => {
if (isOpen[index]) {
let groupProxies = [...group.all]
if (proxyDisplayOrder === 'delay') {
groupProxies = groupProxies.sort((a, b) => {
if (a.history.length === 0) return -1
if (b.history.length === 0) return 1
if (a.history[a.history.length - 1].delay === 0) return 1
if (b.history[b.history.length - 1].delay === 0) return -1
return a.history[a.history.length - 1].delay - b.history[b.history.length - 1].delay
})
}
if (proxyDisplayOrder === 'name') {
groupProxies = groupProxies.sort((a, b) => a.name.localeCompare(b.name))
}
allProxies.push(groupProxies)
} else {
allProxies.push([])
}
})
return { groupCounts, allProxies }
}, [groups, isOpen, proxyDisplayOrder, cols])
const onChangeProxy = async (group: string, proxy: string): Promise<void> => {
await mihomoChangeProxy(group, proxy)
if (autoCloseConnection) {
await mihomoCloseAllConnections()
}
mutate()
}
const onProxyDelay = async (proxy: string, url?: string): Promise<IMihomoDelay> => {
return await mihomoProxyDelay(proxy, url)
}
const onGroupDelay = async (group: string, url?: string): Promise<void> => {
PubSub.publish(`${group}-delay`)
await mihomoGroupDelay(group, url)
}
const calcCols = (): number => {
if (window.matchMedia('(min-width: 1536px)').matches) {
return 5
} else if (window.matchMedia('(min-width: 1280px)').matches) {
return 4
} else if (window.matchMedia('(min-width: 1024px)').matches) {
return 3
} else {
return 2
}
}
useEffect(() => {
if (proxyCols !== 'auto') {
setCols(parseInt(proxyCols))
return
}
setCols(calcCols())
window.onresize = (): void => {
setCols(calcCols())
}
return (): void => {
window.onresize = null
}
}, [])
return (
<BasePage
title="代理组"
header={
<>
<Button
size="sm"
isIconOnly
className="app-nodrag"
onPress={() => {
patchAppConfig({
proxyDisplayOrder:
proxyDisplayOrder === 'default'
? 'delay'
: proxyDisplayOrder === 'delay'
? 'name'
: 'default'
})
}}
>
{proxyDisplayOrder === 'default' ? (
<TbCircleLetterD size={20} title="默认" />
) : proxyDisplayOrder === 'delay' ? (
<FaBoltLightning size={20} title="延迟" />
) : (
<RxLetterCaseCapitalize size={20} title="名称" />
)}
</Button>
<Button
size="sm"
isIconOnly
className="app-nodrag"
onPress={() => {
patchAppConfig({
proxyDisplayMode: proxyDisplayMode === 'simple' ? 'full' : 'simple'
})
}}
>
{proxyDisplayMode === 'simple' ? (
<CgDetailsMore size={20} title="详细信息" />
) : (
<CgDetailsLess size={20} title="简洁信息" />
)}
</Button>
</>
}
>
<div className="h-[calc(100vh-50px)]">
<GroupedVirtuoso
ref={virtuosoRef}
groupCounts={groupCounts}
groupContent={(index) => {
return groups[index] ? (
<div
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
>
<Card
isPressable
fullWidth
onPress={() => {
setIsOpen((prev) => {
const newOpen = [...prev]
newOpen[index] = !prev[index]
return newOpen
})
}}
>
<CardBody className="w-full">
<div className="flex justify-between">
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
{groups[index].icon ? (
<Avatar
className="bg-transparent mr-2"
size="sm"
onLoad={() => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = (): void => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx?.drawImage(img, 0, 0)
const data = canvas.toDataURL('image/png')
console.log('set')
localStorage.setItem(groups[index].icon, data)
}
img.src = groups[index].icon
}}
radius="sm"
src={localStorage.getItem(groups[index].icon) || groups[index].icon}
/>
) : null}
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
<div
title={groups[index].name}
className="inline flag-emoji h-[32px] text-md leading-[32px]"
>
{groups[index].name}
</div>
{proxyDisplayMode === 'full' && (
<div
title={groups[index].type}
className="inline ml-2 text-sm text-default-500"
>
{groups[index].type}
</div>
)}
{proxyDisplayMode === 'full' && (
<div className="inline flag-emoji ml-2 text-sm text-default-500">
{groups[index].now}
</div>
)}
</div>
</div>
<div className="flex">
{proxyDisplayMode === 'full' && (
<Chip size="sm" className="my-1 mr-2">
{groups[index].all.length}
</Chip>
)}
<Button
title="定位到当前节点"
variant="light"
size="sm"
isIconOnly
onPress={() => {
if (!isOpen[index]) return
let i = 0
for (let j = 0; j < index; j++) {
i += groupCounts[j]
}
i += Math.floor(
allProxies[index].findIndex(
(proxy) => proxy.name === groups[index].now
) / cols
)
virtuosoRef.current?.scrollToIndex({
index: Math.floor(i),
align: 'start'
})
}}
>
<FaLocationCrosshairs className="text-lg text-default-500" />
</Button>
<Button
title="延迟测试"
variant="light"
size="sm"
isIconOnly
onPress={() => {
onGroupDelay(groups[index].name, groups[index].testUrl)
}}
>
<MdOutlineSpeed className="text-lg text-default-500" />
</Button>
<IoIosArrowBack
className={`transition duration-200 ml-2 h-[32px] text-lg text-default-500 ${isOpen[index] ? '-rotate-90' : ''}`}
/>
</div>
</div>
</CardBody>
</Card>
</div>
) : (
<div>Never See This</div>
)
}}
itemContent={(index, groupIndex) => {
let innerIndex = index
groupCounts.slice(0, groupIndex).forEach((count) => {
innerIndex -= count
})
return allProxies[groupIndex] ? (
<div
style={
proxyCols !== 'auto'
? { gridTemplateColumns: `repeat(${proxyCols}, minmax(0, 1fr))` }
: {}
}
className={`grid ${proxyCols === 'auto' ? 'sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' : ''} gap-2 pt-2 mx-2`}
>
{Array.from({ length: cols }).map((_, i) => {
if (!allProxies[groupIndex][innerIndex * cols + i]) return null
return (
<ProxyItem
key={allProxies[groupIndex][innerIndex * cols + i].name}
mutateProxies={mutate}
onProxyDelay={onProxyDelay}
onSelect={onChangeProxy}
proxy={allProxies[groupIndex][innerIndex * cols + i]}
group={groups[groupIndex]}
proxyDisplayMode={proxyDisplayMode}
selected={
allProxies[groupIndex][innerIndex * cols + i]?.name ===
groups[groupIndex].now
}
/>
)
})}
</div>
) : (
<div>Never See This</div>
)
}}
/>
</div>
</BasePage>
)
}
export default Proxies