mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
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.
This commit is contained in:
@@ -9,7 +9,7 @@ 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 } from '@mui/material'
|
||||
import { Box, IconButton, type SxProps, TextField } from '@mui/material'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
@@ -18,11 +18,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import { BaseSearchBox, type SearchState } from '../base'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -70,10 +69,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
|
||||
@@ -116,8 +112,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
autoComplete="new-password"
|
||||
hiddenLabel
|
||||
@@ -130,7 +125,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
@@ -181,7 +176,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
if (testUrl?.trim() && textState !== 'filter') {
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
||||
import { syncTrayProxySelection } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import {
|
||||
isInteractableMember,
|
||||
resolveMember,
|
||||
@@ -145,12 +143,6 @@ function useProxyRenderState(
|
||||
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
||||
} finally {
|
||||
onProxies()
|
||||
mutate(() => syncTrayProxySelection(), {
|
||||
id: 'sync-tray-selection',
|
||||
errorNotice: false,
|
||||
}).catch((error) => {
|
||||
console.error('[ProxyGroups] Failed to sync tray latency:', error)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SortByAlphaRounded,
|
||||
SortRounded,
|
||||
} from '@mui/icons-material'
|
||||
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||
import { Box, IconButton, TextField, type SxProps } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -20,9 +20,8 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -73,10 +72,7 @@ export const ProxyHead = ({
|
||||
'http://cp.cloudflare.com/generate_204'
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
return (
|
||||
@@ -101,7 +97,7 @@ export const ProxyHead = ({
|
||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
@@ -192,8 +188,7 @@ export const ProxyHead = ({
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
autoFocus={autoFocus}
|
||||
hiddenLabel
|
||||
@@ -202,7 +197,7 @@ export const ProxyHead = ({
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { TextField, type TextFieldProps } from '@mui/material'
|
||||
import { useEffect, useEffectEvent, useRef, useState } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
|
||||
type Props = Omit<TextFieldProps, 'value' | 'onChange'> & {
|
||||
value: string
|
||||
onCommit: (url: string) => void
|
||||
}
|
||||
|
||||
export const ProxyTestUrlInput = ({ value, name, ...props }: Props) => {
|
||||
const { profiles } = useProfiles()
|
||||
return (
|
||||
<DraftTestUrlInput
|
||||
key={JSON.stringify([profiles?.current, name])}
|
||||
name={name}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DraftTestUrlInput = ({ value, onCommit, ...props }: Props) => {
|
||||
const [draft, setDraft] = useState<string | null>(null)
|
||||
const pendingRef = useRef<string | null>(null)
|
||||
const commitOnUnmount = useEffectEvent(() => {
|
||||
if (pendingRef.current !== null && pendingRef.current !== value)
|
||||
onCommit(pendingRef.current)
|
||||
})
|
||||
useEffect(() => () => commitOnUnmount(), [])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
value={draft ?? value}
|
||||
onChange={(event) => {
|
||||
pendingRef.current = event.target.value
|
||||
setDraft(event.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const next = pendingRef.current
|
||||
pendingRef.current = null
|
||||
setDraft(null)
|
||||
if (next !== null && next !== value) onCommit(next)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
const input = event.target as HTMLInputElement
|
||||
input.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -13,17 +13,11 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name, recordId: `${memberIndex}` },
|
||||
node: {
|
||||
recordId: `${memberIndex}`,
|
||||
history: [{ delay }],
|
||||
extra: {
|
||||
'http://cp.cloudflare.com/generate_204': {
|
||||
history: [{ time: new Date(0).toISOString(), delay }],
|
||||
},
|
||||
},
|
||||
source: { kind: 'provider', providerName: `${memberIndex}` },
|
||||
},
|
||||
},
|
||||
}) as unknown as ResolvedMemberOccurrence
|
||||
}) as ResolvedMemberOccurrence
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
@@ -42,9 +36,7 @@ test('matches the previous comparator for cached, fallback and sentinel delays',
|
||||
for (const cached of [false, true]) {
|
||||
const group = `sort-${cached}`
|
||||
if (cached) {
|
||||
values.forEach((delay, i) =>
|
||||
delayManager.setDelay(list[i].member, group, delay),
|
||||
)
|
||||
values.forEach((delay, i) => delayManager.setDelay(`${i}`, group, delay))
|
||||
}
|
||||
for (const timeout of [10000, 20, 0, NaN]) {
|
||||
const expected = list
|
||||
@@ -74,7 +66,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
node(3, 25),
|
||||
]
|
||||
const cachedOrder = [list[0], list[2], list[1], list[3]]
|
||||
delayManager.setDelay(list[0].member, 'expiry', 1)
|
||||
delayManager.setDelay('0', 'expiry', 1)
|
||||
const get = vi.spyOn(delayManager, 'getDelayFix')
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
expect(get).toHaveBeenCalledTimes(list.length)
|
||||
@@ -88,7 +80,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
list[3],
|
||||
list[0],
|
||||
])
|
||||
delayManager.setDelay(list[0].member, 'expiry', 2)
|
||||
delayManager.setDelay('0', 'expiry', 2)
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
get.mockClear()
|
||||
filterSort([], 'expiry', '', 1)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import {
|
||||
profileTestUrls,
|
||||
commitProfileTestUrl,
|
||||
} from '@/services/proxy-test-url'
|
||||
|
||||
import { ProxySortType } from './use-filter-sort'
|
||||
|
||||
@@ -36,45 +32,22 @@ export const DEFAULT_STATE: HeadState = {
|
||||
}
|
||||
|
||||
type HeadStateAction =
|
||||
| { type: 'reset'; profile: string }
|
||||
| { type: 'replace'; profile: string; payload: Record<string, HeadState> }
|
||||
| { type: 'reset' }
|
||||
| { type: 'replace'; payload: Record<string, HeadState> }
|
||||
| { type: 'update'; groupName: string; patch: Partial<HeadState> }
|
||||
| { type: 'urls'; urls: Record<string, string> }
|
||||
|
||||
interface LoadedHeadState {
|
||||
profile: string | null
|
||||
heads: Record<string, HeadState>
|
||||
}
|
||||
|
||||
function headStateReducer(
|
||||
state: LoadedHeadState,
|
||||
state: Record<string, HeadState>,
|
||||
action: HeadStateAction,
|
||||
): LoadedHeadState {
|
||||
): Record<string, HeadState> {
|
||||
switch (action.type) {
|
||||
case 'reset':
|
||||
return { profile: action.profile, heads: {} }
|
||||
return {}
|
||||
case 'replace':
|
||||
return { profile: action.profile, heads: action.payload }
|
||||
case 'urls': {
|
||||
let heads = state.heads
|
||||
for (const [name, testUrl] of Object.entries(action.urls)) {
|
||||
if (heads[name]?.testUrl === testUrl) continue
|
||||
heads = {
|
||||
...heads,
|
||||
[name]: { ...DEFAULT_STATE, ...heads[name], testUrl },
|
||||
}
|
||||
}
|
||||
return heads === state.heads ? state : { ...state, heads }
|
||||
}
|
||||
return action.payload
|
||||
case 'update': {
|
||||
const prev = state.heads[action.groupName] || DEFAULT_STATE
|
||||
return {
|
||||
...state,
|
||||
heads: {
|
||||
...state.heads,
|
||||
[action.groupName]: { ...prev, ...action.patch },
|
||||
},
|
||||
}
|
||||
const prev = state[action.groupName] || DEFAULT_STATE
|
||||
return { ...state, [action.groupName]: { ...prev, ...action.patch } }
|
||||
}
|
||||
default:
|
||||
return state
|
||||
@@ -82,42 +55,30 @@ function headStateReducer(
|
||||
}
|
||||
|
||||
export function useHeadStateNew() {
|
||||
const { profiles, current: profile } = useProfiles()
|
||||
const { profiles } = useProfiles()
|
||||
const current = profiles?.current || ''
|
||||
|
||||
const [{ profile: loadedProfile, heads: state }, dispatch] = useReducer(
|
||||
headStateReducer,
|
||||
{ profile: null, heads: {} },
|
||||
)
|
||||
const [state, dispatch] = useReducer(headStateReducer, {})
|
||||
|
||||
useEffect(() => {
|
||||
const urls = profile ? profileTestUrls(profile) : {}
|
||||
if (loadedProfile === current) {
|
||||
dispatch({ type: 'urls', urls })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(
|
||||
localStorage.getItem(HEAD_STATE_KEY) || '{}',
|
||||
localStorage.getItem(HEAD_STATE_KEY)!,
|
||||
) as HeadStateStorage
|
||||
|
||||
const value = data?.[current] || {}
|
||||
for (const [name, testUrl] of Object.entries(urls)) {
|
||||
value[name] = { ...DEFAULT_STATE, ...value[name], testUrl }
|
||||
}
|
||||
const value = data[current] || {}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
dispatch({ type: 'replace', profile: current, payload: value })
|
||||
dispatch({ type: 'replace', payload: value })
|
||||
} else {
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
dispatch({ type: 'reset' })
|
||||
}
|
||||
} catch {
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
dispatch({ type: 'reset' })
|
||||
}
|
||||
}, [current, loadedProfile, profile])
|
||||
}, [current])
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedProfile !== current) return
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const item = localStorage.getItem(HEAD_STATE_KEY)
|
||||
@@ -133,16 +94,13 @@ export function useHeadStateNew() {
|
||||
})
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [state, current, loadedProfile])
|
||||
}, [state, current])
|
||||
|
||||
const setHeadState = useCallback(
|
||||
(groupName: string, obj: Partial<HeadState>) => {
|
||||
dispatch({ type: 'update', groupName, patch: obj })
|
||||
if (current && obj.testUrl !== undefined) {
|
||||
void commitProfileTestUrl(current, groupName, obj.testUrl)
|
||||
}
|
||||
},
|
||||
[current],
|
||||
[],
|
||||
)
|
||||
|
||||
return [state, setHeadState] as const
|
||||
|
||||
@@ -116,7 +116,6 @@ const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
extra: {},
|
||||
members,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user