remember scroll position and expansion state

This commit is contained in:
pompurin404 2025-02-01 13:39:16 +08:00
parent 26a95bdb89
commit e746ab4241

View File

@ -11,7 +11,7 @@ import { CgDetailsLess, CgDetailsMore } from 'react-icons/cg'
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 { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { GroupedVirtuoso, GroupedVirtuosoHandle } from 'react-virtuoso'
import ProxyItem from '@renderer/components/proxies/proxy-item'
import { IoIosArrowBack } from 'react-icons/io'
@ -21,6 +21,96 @@ import CollapseInput from '@renderer/components/base/collapse-input'
import { includesIgnoreCase } from '@renderer/utils/includes'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
const SCROLL_DEBOUNCE_TIME = 200
const RENDER_DELAY = 100
// 自定义 hook 用于管理滚动位置和展开状态
const useProxyState = (groups: IMihomoMixedGroup[]) => {
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
const [scrollPosition, setScrollPosition] = useState<number>(0)
const scrollTimerRef = useRef<NodeJS.Timeout>()
// 初始化展开状态
const [isOpen, setIsOpen] = useState(() => {
try {
const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY)
return savedState ? JSON.parse(savedState) : Array(groups.length).fill(false)
} catch (error) {
console.error('Failed to load group expand state:', error)
return Array(groups.length).fill(false)
}
})
// 保存展开状态
useEffect(() => {
try {
localStorage.setItem(GROUP_EXPAND_STATE_KEY, JSON.stringify(isOpen))
} catch (error) {
console.error('Failed to save group expand state:', error)
}
}, [isOpen])
// 清理定时器
useEffect(() => {
return () => {
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current)
}
}
}, [])
// 恢复滚动位置
useEffect(() => {
if (groups.length > 0) {
try {
const savedPosition = localStorage.getItem(SCROLL_POSITION_KEY)
if (savedPosition) {
const position = parseInt(savedPosition)
if (!isNaN(position) && position >= 0) {
const timer = setTimeout(() => {
virtuosoRef.current?.scrollTo({ top: position })
}, RENDER_DELAY)
return () => clearTimeout(timer)
}
}
} catch (error) {
console.error('Failed to restore scroll position:', error)
}
}
}, [groups])
const saveScrollPosition = useCallback((position: number) => {
try {
localStorage.setItem(SCROLL_POSITION_KEY, position.toString())
} catch (error) {
console.error('Failed to save scroll position:', error)
}
}, [])
return {
virtuosoRef,
isOpen,
setIsOpen,
scrollPosition,
onScroll: useCallback((e: React.UIEvent<HTMLElement>) => {
const position = (e.target as HTMLElement).scrollTop
setScrollPosition(position)
// 清理之前的定时器
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current)
}
// 使用防抖来减少存储频率
scrollTimerRef.current = setTimeout(() => {
saveScrollPosition(position)
}, SCROLL_DEBOUNCE_TIME)
}, [saveScrollPosition])
}
}
const Proxies: React.FC = () => {
const { controledMihomoConfig } = useControledMihomoConfig()
const { mode = 'rule' } = controledMihomoConfig || {}
@ -33,11 +123,11 @@ const Proxies: React.FC = () => {
proxyCols = 'auto',
delayTestConcurrency = 50
} = appConfig || {}
const [cols, setCols] = useState(1)
const [isOpen, setIsOpen] = useState(Array(groups.length).fill(false))
const { virtuosoRef, isOpen, setIsOpen, onScroll, scrollPosition } = useProxyState(groups)
const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
const [searchValue, setSearchValue] = useState(Array(groups.length).fill(''))
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
const { groupCounts, allProxies } = useMemo(() => {
const groupCounts: number[] = []
const allProxies: (IMihomoProxy | IMihomoGroup)[][] = []
@ -70,19 +160,25 @@ const Proxies: React.FC = () => {
return { groupCounts, allProxies }
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue])
const onChangeProxy = async (group: string, proxy: string): Promise<void> => {
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
await mihomoChangeProxy(group, proxy)
if (autoCloseConnection) {
await mihomoCloseAllConnections()
}
mutate()
}
// 等待 DOM 更新完成后再恢复滚动位置
requestAnimationFrame(() => {
requestAnimationFrame(() => {
virtuosoRef.current?.scrollTo({ top: scrollPosition })
})
})
}, [autoCloseConnection, mutate, virtuosoRef, scrollPosition])
const onProxyDelay = async (proxy: string, url?: string): Promise<IMihomoDelay> => {
const onProxyDelay = useCallback(async (proxy: string, url?: string): Promise<IMihomoDelay> => {
return await mihomoProxyDelay(proxy, url)
}
}, [])
const onGroupDelay = async (index: number): Promise<void> => {
const onGroupDelay = useCallback(async (index: number): Promise<void> => {
if (allProxies[index].length === 0) {
setIsOpen((prev) => {
const newOpen = [...prev]
@ -95,6 +191,8 @@ const Proxies: React.FC = () => {
newDelaying[index] = true
return newDelaying
})
try {
// 限制并发数量
const result: Promise<void>[] = []
const runningList: Promise<void>[] = []
@ -118,38 +216,37 @@ const Proxies: React.FC = () => {
}
}
await Promise.all(result)
} finally {
setDelaying((prev) => {
const newDelaying = [...prev]
newDelaying[index] = false
return newDelaying
})
}
}, [allProxies, groups, delayTestConcurrency, mutate])
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 {
const calcCols = useCallback((): number => {
if (proxyCols !== 'auto') {
return parseInt(proxyCols)
}
if (window.matchMedia('(min-width: 1536px)').matches) return 5
if (window.matchMedia('(min-width: 1280px)').matches) return 4
if (window.matchMedia('(min-width: 1024px)').matches) return 3
return 2
}
}
}, [proxyCols])
useEffect(() => {
if (proxyCols !== 'auto') {
setCols(parseInt(proxyCols))
return
}
setCols(calcCols())
window.onresize = (): void => {
const handleResize = (): void => {
setCols(calcCols())
}
return (): void => {
window.onresize = null
handleResize() // 初始化
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
}, [calcCols])
return (
<BasePage
@ -212,6 +309,7 @@ const Proxies: React.FC = () => {
<GroupedVirtuoso
ref={virtuosoRef}
groupCounts={groupCounts}
onScroll={onScroll}
groupContent={(index) => {
if (
groups[index] &&