Files
clash-verge-rev/src/components/proxy/proxy-group-tools.tsx
T
wonfen 5e6a696087 revert(tray): revert per-group latency testing (#8173)
This reverts commit cb751052a4.

Remove the tray latency test and its dashboard URL persistence and cache
changes because the two entry points have diverging behavior.

Preserve the subsequent IPC binding migration and remove generated fields,
input projections, and locale entries that depended on the reverted feature.
2026-10-21 05:37:18 +08:00

283 lines
8.6 KiB
TypeScript

import AccessTimeRounded from '@mui/icons-material/AccessTimeRounded'
import MyLocationRounded from '@mui/icons-material/MyLocationRounded'
import NetworkCheckRounded from '@mui/icons-material/NetworkCheckRounded'
import SearchOffRounded from '@mui/icons-material/SearchOffRounded'
import SearchRounded from '@mui/icons-material/SearchRounded'
import SortByAlphaRounded from '@mui/icons-material/SortByAlphaRounded'
import SortRounded from '@mui/icons-material/SortRounded'
import VisibilityOffRounded from '@mui/icons-material/VisibilityOffRounded'
import VisibilityRounded from '@mui/icons-material/VisibilityRounded'
import WifiTetheringOffRounded from '@mui/icons-material/WifiTetheringOffRounded'
import WifiTetheringRounded from '@mui/icons-material/WifiTetheringRounded'
import { Box, IconButton, type SxProps, TextField } from '@mui/material'
import { useDebounceFn } from 'ahooks'
import { memo, useEffect, useRef } from 'react'
import { flushSync } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { useVerge } from '@/hooks/use-verge'
import delayManager from '@/services/delay'
import { showNotice } from '@/services/notice-service'
import { isValidUrl } from '@/utils/network'
import { BaseSearchBox, type SearchState } from '../base'
import type { ProxySortType } from './use-filter-sort'
import type { HeadState } from './use-head-state'
interface Props {
sx?: SxProps
side?: 'left' | 'right'
url?: string
groupName: string
headState: HeadState
onLocation: () => void
onCheckDelay: () => void
onHeadState: (val: Partial<HeadState>) => void
}
export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
const {
sx,
side = 'right',
url,
groupName,
headState,
onCheckDelay,
onHeadState,
onLocation,
} = props
const {
showType,
sortType,
filterText,
textState,
testUrl,
filterMatchCase,
filterMatchWholeWord,
filterUseRegularExpression,
} = headState
const { t } = useTranslation()
const { verge } = useVerge()
const defaultLatencyUrl =
verge?.default_latency_test?.trim() ||
'http://cp.cloudflare.com/generate_204'
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
}, [groupName, testUrl, defaultLatencyUrl, url])
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
// 因此对写入 headState 的动作做防抖,避免每输入一个字符就过滤一次。
const { run: applyFilter, flush: flushFilter } = useDebounceFn(
(state: SearchState) => {
onHeadState({
filterText: state.text,
filterMatchCase: state.matchCase,
filterMatchWholeWord: state.matchWholeWord,
filterUseRegularExpression: state.useRegularExpression,
})
},
{ wait: 600 },
)
// 关闭过滤框或卸载时立即应用最后一次输入,避免丢失未生效的过滤条件。
useEffect(() => {
if (textState !== 'filter') flushFilter()
}, [textState, flushFilter])
useEffect(() => () => flushFilter(), [flushFilter])
const textInput = (
<>
{textState === 'filter' && (
<Box sx={{ flex: '1 1 auto' }}>
<BaseSearchBox
inputRef={inputRef}
defaultValue={filterText}
matchCase={filterMatchCase}
matchWholeWord={filterMatchWholeWord}
useRegularExpression={filterUseRegularExpression}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSearch={(_, state) => applyFilter(state)}
/>
</Box>
)}
{textState === 'url' && (
<TextField
inputRef={inputRef}
autoComplete="new-password"
hiddenLabel
autoSave="off"
value={testUrl}
size="small"
variant="outlined"
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onChange={(e) => onHeadState({ testUrl: e.target.value })}
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
/>
)}
</>
)
return (
<Box
sx={{
display: 'flex',
justifyContent: side === 'left' ? 'start' : 'end',
alignItems: 'center',
gap: 0.5,
height: 36,
flex: 1,
ml: side === 'left' ? 0 : 2,
...sx,
}}
>
{side === 'right' && textInput}
<IconButton
size="small"
color="inherit"
title={t('proxies.page.tooltips.locate')}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (!headState.open)
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => onHeadState({ open: true }))
onLocation()
}}
>
<MyLocationRounded fontSize="inherit" />
</IconButton>
<IconButton
size="small"
color="inherit"
title={t('proxies.page.tooltips.delayCheck')}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (!headState.open)
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => onHeadState({ open: true }))
// Remind the user that it is custom test url
if (testUrl?.trim() && textState !== 'filter') {
onHeadState({ textState: 'url' })
}
if (testUrl?.trim() && !isValidUrl(testUrl)) {
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
return
}
onCheckDelay()
}}
>
<NetworkCheckRounded fontSize="inherit" />
</IconButton>
<IconButton
size="small"
color="inherit"
title={
[
t('proxies.page.tooltips.sortDefault'),
t('proxies.page.tooltips.sortDelay'),
t('proxies.page.tooltips.sortName'),
][sortType]
}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (!headState.open)
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => onHeadState({ open: true }))
onHeadState({
sortType: ((sortType + 1) % 3) as ProxySortType,
})
}}
>
{sortType !== 1 && sortType !== 2 && <SortRounded fontSize="inherit" />}
{sortType === 1 && <AccessTimeRounded fontSize="inherit" />}
{sortType === 2 && <SortByAlphaRounded fontSize="inherit" />}
</IconButton>
<IconButton
size="small"
color="inherit"
title={t('proxies.page.tooltips.delayCheckUrl')}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onHeadState({
textState: textState === 'url' ? null : 'url',
})
setTimeout(() => inputRef.current?.focus())
}}
>
{textState === 'url' ? (
<WifiTetheringRounded fontSize="inherit" />
) : (
<WifiTetheringOffRounded fontSize="inherit" />
)}
</IconButton>
<IconButton
size="small"
color="inherit"
title={
showType
? t('proxies.page.tooltips.showBasic')
: t('proxies.page.tooltips.showDetail')
}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (!headState.open)
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => onHeadState({ open: true }))
onHeadState({ showType: !showType })
}}
>
{showType ? (
<VisibilityRounded fontSize="inherit" />
) : (
<VisibilityOffRounded fontSize="inherit" />
)}
</IconButton>
<IconButton
size="small"
color="inherit"
title={t('proxies.page.tooltips.filter')}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (!headState.open && textState !== 'filter')
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => onHeadState({ open: true }))
onHeadState({ textState: textState === 'filter' ? null : 'filter' })
setTimeout(() => inputRef.current?.focus())
}}
>
{textState === 'filter' ? (
<SearchOffRounded fontSize="inherit" />
) : (
<SearchRounded fontSize="inherit" />
)}
</IconButton>
{side === 'left' && textInput}
</Box>
)
})