mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 18:52:12 +08:00
311 lines
8.7 KiB
TypeScript
311 lines
8.7 KiB
TypeScript
import { CheckCircleOutlineRounded } from '@mui/icons-material'
|
|
import { alpha, Box, ListItemButton, styled, Typography } from '@mui/material'
|
|
import { useLockFn } from 'ahooks'
|
|
import { useCallback, useEffect, useReducer } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { BaseLoading } from '@/components/base'
|
|
import { useVerge } from '@/hooks/use-verge'
|
|
import delayManager, { DelayUpdate } from '@/services/delay'
|
|
|
|
interface Props {
|
|
group: IProxyGroupItem
|
|
proxy: IProxyItem
|
|
selected: boolean
|
|
showType?: boolean
|
|
onClick?: (name: string) => void
|
|
}
|
|
|
|
// 多列布局
|
|
export const ProxyItemMini = (props: Props) => {
|
|
const { group, proxy, selected, showType = true, onClick } = props
|
|
|
|
const { t } = useTranslation()
|
|
|
|
const presetList = ['DIRECT', 'REJECT', 'REJECT-DROP', 'PASS', 'COMPATIBLE']
|
|
const isPreset = presetList.includes(proxy.name)
|
|
// -1/<=0 为不显示,-2 为 loading
|
|
const [delayState, setDelayState] = useReducer(
|
|
(_: DelayUpdate, next: DelayUpdate) => next,
|
|
{
|
|
delay: -1,
|
|
updatedAt: 0,
|
|
},
|
|
)
|
|
const { verge } = useVerge()
|
|
const timeout = verge?.default_latency_timeout || 10000
|
|
|
|
useEffect(() => {
|
|
if (isPreset) return
|
|
delayManager.setListener(proxy.name, group.name, setDelayState)
|
|
|
|
return () => {
|
|
delayManager.removeListener(proxy.name, group.name)
|
|
}
|
|
}, [isPreset, proxy.name, group.name])
|
|
|
|
const updateDelay = useCallback(() => {
|
|
if (!proxy) return
|
|
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name)
|
|
if (cachedUpdate) {
|
|
setDelayState({ ...cachedUpdate })
|
|
return
|
|
}
|
|
|
|
const fallbackDelay = delayManager.getDelayFix(proxy, group.name)
|
|
if (fallbackDelay === -1) {
|
|
setDelayState({ delay: -1, updatedAt: 0 })
|
|
return
|
|
}
|
|
|
|
let updatedAt = 0
|
|
const history = proxy.history
|
|
if (history && history.length > 0) {
|
|
const lastRecord = history[history.length - 1]
|
|
const parsed = Date.parse(lastRecord.time)
|
|
if (!Number.isNaN(parsed)) {
|
|
updatedAt = parsed
|
|
}
|
|
}
|
|
|
|
setDelayState({
|
|
delay: fallbackDelay,
|
|
updatedAt,
|
|
})
|
|
}, [proxy, group.name])
|
|
|
|
useEffect(() => {
|
|
updateDelay()
|
|
}, [updateDelay])
|
|
|
|
const onDelay = useLockFn(async () => {
|
|
setDelayState({ delay: -2, updatedAt: Date.now() })
|
|
setDelayState(
|
|
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
|
)
|
|
})
|
|
|
|
const delayValue = delayState.delay
|
|
|
|
return (
|
|
<ListItemButton
|
|
dense
|
|
selected={selected}
|
|
onClick={() => onClick?.(proxy.name)}
|
|
sx={[
|
|
{
|
|
height: 56,
|
|
borderRadius: 1.5,
|
|
pl: 1.5,
|
|
pr: 1,
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
({ palette: { mode, primary } }) => {
|
|
const bgcolor = mode === 'light' ? '#ffffff' : '#24252f'
|
|
const showDelay = delayValue > 0
|
|
const selectColor = mode === 'light' ? primary.main : primary.light
|
|
|
|
return {
|
|
'&:hover .the-check': { display: !showDelay ? 'block' : 'none' },
|
|
'&:hover .the-delay': { display: showDelay ? 'block' : 'none' },
|
|
'&:hover .the-icon': { display: 'none' },
|
|
'& .the-pin, & .the-unpin': {
|
|
position: 'absolute',
|
|
fontSize: '12px',
|
|
top: '-5px',
|
|
right: '-5px',
|
|
},
|
|
'& .the-unpin': { filter: 'grayscale(1)' },
|
|
'&.Mui-selected': {
|
|
width: `calc(100% + 3px)`,
|
|
marginLeft: `-3px`,
|
|
borderLeft: `3px solid ${selectColor}`,
|
|
bgcolor:
|
|
mode === 'light'
|
|
? alpha(primary.main, 0.15)
|
|
: alpha(primary.main, 0.35),
|
|
},
|
|
backgroundColor: bgcolor,
|
|
}
|
|
},
|
|
]}
|
|
>
|
|
<Box
|
|
title={`${proxy.name}\n${proxy.now ?? ''}`}
|
|
sx={{ overflow: 'hidden' }}
|
|
>
|
|
<Typography
|
|
variant="body2"
|
|
component="div"
|
|
color="text.primary"
|
|
sx={{
|
|
display: 'block',
|
|
textOverflow: 'ellipsis',
|
|
wordBreak: 'break-all',
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{proxy.name}
|
|
</Typography>
|
|
|
|
{showType && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexWrap: 'nowrap',
|
|
flex: 'none',
|
|
marginTop: '4px',
|
|
}}
|
|
>
|
|
{proxy.now && (
|
|
<Typography
|
|
variant="body2"
|
|
component="div"
|
|
color="text.secondary"
|
|
sx={{
|
|
display: 'block',
|
|
textOverflow: 'ellipsis',
|
|
wordBreak: 'break-all',
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
marginRight: '8px',
|
|
}}
|
|
>
|
|
{proxy.now}
|
|
</Typography>
|
|
)}
|
|
{!!proxy.provider && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
{proxy.provider}
|
|
</TypeBox>
|
|
)}
|
|
<TypeBox color="text.secondary" component="span">
|
|
{proxy.type}
|
|
</TypeBox>
|
|
{proxy.udp && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
UDP
|
|
</TypeBox>
|
|
)}
|
|
{proxy.xudp && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
XUDP
|
|
</TypeBox>
|
|
)}
|
|
{proxy.tfo && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
TFO
|
|
</TypeBox>
|
|
)}
|
|
{proxy.mptcp && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
MPTCP
|
|
</TypeBox>
|
|
)}
|
|
{proxy.smux && (
|
|
<TypeBox color="text.secondary" component="span">
|
|
SMUX
|
|
</TypeBox>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
<Box
|
|
sx={{ ml: 0.5, color: 'primary.main', display: isPreset ? 'none' : '' }}
|
|
>
|
|
{delayValue === -2 && (
|
|
<Widget>
|
|
<BaseLoading />
|
|
</Widget>
|
|
)}
|
|
{!proxy.provider && delayValue !== -2 && (
|
|
// provider 的节点不支持检测
|
|
<Widget
|
|
className="the-check"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
onDelay()
|
|
}}
|
|
sx={({ palette }) => ({
|
|
display: 'none', // hover 时显示
|
|
':hover': { bgcolor: alpha(palette.primary.main, 0.15) },
|
|
})}
|
|
>
|
|
Check
|
|
</Widget>
|
|
)}
|
|
|
|
{delayValue >= 0 && (
|
|
// 显示延迟
|
|
<Widget
|
|
className="the-delay"
|
|
onClick={(e) => {
|
|
if (proxy.provider) return
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
onDelay()
|
|
}}
|
|
sx={({ palette }) => ({
|
|
color: delayManager.formatDelayColor(delayValue, timeout),
|
|
...(!proxy.provider
|
|
? { ':hover': { bgcolor: alpha(palette.primary.main, 0.15) } }
|
|
: {}),
|
|
})}
|
|
>
|
|
{delayManager.formatDelay(delayValue, timeout)}
|
|
</Widget>
|
|
)}
|
|
{proxy.type !== 'Direct' &&
|
|
delayValue !== -2 &&
|
|
delayValue < 0 &&
|
|
selected && (
|
|
// 展示已选择的 icon
|
|
<CheckCircleOutlineRounded
|
|
className="the-icon"
|
|
sx={{ fontSize: 16, mr: 0.5, display: 'block' }}
|
|
/>
|
|
)}
|
|
</Box>
|
|
{group.fixed && group.fixed === proxy.name && (
|
|
// 展示 fixed 状态
|
|
<span
|
|
className={proxy.name === group.now ? 'the-pin' : 'the-unpin'}
|
|
title={
|
|
group.type === 'URLTest'
|
|
? t('proxies.page.labels.delayCheckReset')
|
|
: ''
|
|
}
|
|
>
|
|
📌
|
|
</span>
|
|
)}
|
|
</ListItemButton>
|
|
)
|
|
}
|
|
|
|
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
|
padding: '2px 4px',
|
|
fontSize: 14,
|
|
fontFamily: typography.fontFamily,
|
|
borderRadius: '4px',
|
|
}))
|
|
|
|
const TypeBox = styled(Box, {
|
|
shouldForwardProp: (prop) => prop !== 'component',
|
|
})<{ component?: React.ElementType }>(({ theme: { typography } }) => ({
|
|
display: 'inline-block',
|
|
border: '1px solid #ccc',
|
|
borderColor: 'text.secondary',
|
|
color: 'text.secondary',
|
|
borderRadius: 4,
|
|
fontSize: 10,
|
|
fontFamily: typography.fontFamily,
|
|
marginRight: '4px',
|
|
marginTop: 'auto',
|
|
padding: '0 4px',
|
|
lineHeight: 1.5,
|
|
}))
|