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) => 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(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' && ( { e.preventDefault() e.stopPropagation() }} onSearch={(_, state) => applyFilter(state)} /> )} {textState === 'url' && ( { e.preventDefault() e.stopPropagation() }} onChange={(e) => onHeadState({ testUrl: e.target.value })} sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }} /> )} ) return ( {side === 'right' && textInput} { e.preventDefault() e.stopPropagation() if (!headState.open) // eslint-disable-next-line @eslint-react/dom-no-flush-sync flushSync(() => onHeadState({ open: true })) onLocation() }} > { 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() }} > { 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 && } {sortType === 1 && } {sortType === 2 && } { e.preventDefault() e.stopPropagation() onHeadState({ textState: textState === 'url' ? null : 'url', }) setTimeout(() => inputRef.current?.focus()) }} > {textState === 'url' ? ( ) : ( )} { 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 ? ( ) : ( )} { 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' ? ( ) : ( )} {side === 'left' && textInput} ) })