diff --git a/src/renderer/src/components/proxies/proxy-item.tsx b/src/renderer/src/components/proxies/proxy-item.tsx index 5ff4bb2..1caf98c 100644 --- a/src/renderer/src/components/proxies/proxy-item.tsx +++ b/src/renderer/src/components/proxies/proxy-item.tsx @@ -1,7 +1,6 @@ import { Button, Card, CardBody } from '@heroui/react' import { mihomoUnfixedProxy } from '@renderer/utils/ipc' -import React from 'react' -import { useMemo, useState } from 'react' +import React, { useMemo, useState, useCallback } from 'react' import { FaMapPin } from 'react-icons/fa6' import { useTranslation } from 'react-i18next' @@ -16,7 +15,14 @@ interface Props { isGroupTesting?: boolean } -const ProxyItem: React.FC = (props) => { +function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' { + if (delay === -1) return 'primary' + if (delay === 0) return 'danger' + if (delay < 500) return 'success' + return 'warning' +} + +const ProxyItem: React.FC = React.memo((props) => { const { t } = useTranslation() const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props @@ -25,34 +31,27 @@ const ProxyItem: React.FC = (props) => { return proxy.history[proxy.history.length - 1].delay } return -1 - }, [proxy]) + }, [proxy.history]) const [loading, setLoading] = useState(false) const isLoading = loading || isGroupTesting - - function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' { - if (delay === -1) return 'primary' - if (delay === 0) return 'danger' - if (delay < 500) return 'success' - return 'warning' - } - function delayText(delay: number): string { + const delayText = useMemo(() => { if (delay === -1) return t('proxies.delay.test') if (delay === 0) return t('proxies.delay.timeout') return delay.toString() - } + }, [delay, t]) - const onDelay = (): void => { + const onDelay = useCallback((): void => { setLoading(true) onProxyDelay(proxy.name, group.testUrl).finally(() => { mutateProxies() setLoading(false) }) - } + }, [proxy.name, group.testUrl, onProxyDelay, mutateProxies]) - const fixed = group.fixed && group.fixed === proxy.name + const fixed = useMemo(() => group.fixed && group.fixed === proxy.name, [group.fixed, proxy.name]) return ( = (props) => { className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap" >
- {delayText(delay)} + {delayText}
@@ -156,7 +155,7 @@ const ProxyItem: React.FC = (props) => { className="h-full text-sm px-2 relative w-min whitespace-nowrap" >
- {delayText(delay)} + {delayText}
@@ -165,6 +164,18 @@ const ProxyItem: React.FC = (props) => {
) -} +}, (prevProps, nextProps) => { + // 必要时重新渲染 + return ( + prevProps.proxy.name === nextProps.proxy.name && + prevProps.proxy.history === nextProps.proxy.history && + prevProps.selected === nextProps.selected && + prevProps.proxyDisplayMode === nextProps.proxyDisplayMode && + prevProps.group.fixed === nextProps.group.fixed && + prevProps.isGroupTesting === nextProps.isGroupTesting + ) +}) + +ProxyItem.displayName = 'ProxyItem' export default ProxyItem \ No newline at end of file diff --git a/src/renderer/src/pages/proxies.tsx b/src/renderer/src/pages/proxies.tsx index 27c934c..40c89bc 100644 --- a/src/renderer/src/pages/proxies.tsx +++ b/src/renderer/src/pages/proxies.tsx @@ -26,11 +26,11 @@ const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state' // 自定义 hook 用于管理展开状态 const useProxyState = (groups: IMihomoMixedGroup[]): { - virtuosoRef: React.RefObject; + virtuosoRef: React.RefObject; isOpen: boolean[]; setIsOpen: React.Dispatch>; } => { - const virtuosoRef = useRef(null) + const virtuosoRef = useRef(null) // 初始化展开状态 const [isOpen, setIsOpen] = useState(() => { @@ -76,39 +76,55 @@ const Proxies: React.FC = () => { const [cols, setCols] = useState(1) const { virtuosoRef, isOpen, setIsOpen } = useProxyState(groups) const [delaying, setDelaying] = useState(Array(groups.length).fill(false)) - const [proxyDelaying, setProxyDelaying] = useState>({}) + const [proxyDelaying, setProxyDelaying] = useState>(new Set()) const [searchValue, setSearchValue] = useState(Array(groups.length).fill('')) + + // searchValue 初始化 + useEffect(() => { + if (groups.length !== searchValue.length) { + setSearchValue(Array(groups.length).fill('')) + } + }, [groups.length]) + + // 代理列表排序 + const sortProxies = useCallback((proxies: (IMihomoProxy | IMihomoGroup)[], order: string) => { + if (order === 'delay') { + return [...proxies].sort((a, b) => { + if (a.history.length === 0) return 1 + if (b.history.length === 0) return -1 + const aDelay = a.history[a.history.length - 1].delay + const bDelay = b.history[b.history.length - 1].delay + if (aDelay === 0) return 1 + if (bDelay === 0) return -1 + return aDelay - bDelay + }) + } + if (order === 'name') { + return [...proxies].sort((a, b) => a.name.localeCompare(b.name)) + } + return proxies + }, []) + const { groupCounts, allProxies } = useMemo(() => { const groupCounts: number[] = [] const allProxies: (IMihomoProxy | IMihomoGroup)[][] = [] - if (groups.length !== searchValue.length) setSearchValue(Array(groups.length).fill('')) + groups.forEach((group, index) => { if (isOpen[index]) { - let groupProxies = group.all.filter( + const filtered = group.all.filter( (proxy) => proxy && includesIgnoreCase(proxy.name, searchValue[index]) ) - const count = Math.floor(groupProxies.length / cols) - groupCounts.push(groupProxies.length % cols === 0 ? count : count + 1) - if (proxyDisplayOrder === 'delay') { - groupProxies = groupProxies.sort((a, b) => { - if (a.history.length === 0) return -1 - if (b.history.length === 0) return 1 - if (a.history[a.history.length - 1].delay === 0) return 1 - if (b.history[b.history.length - 1].delay === 0) return -1 - return a.history[a.history.length - 1].delay - b.history[b.history.length - 1].delay - }) - } - if (proxyDisplayOrder === 'name') { - groupProxies = groupProxies.sort((a, b) => a.name.localeCompare(b.name)) - } - allProxies.push(groupProxies) + const sorted = sortProxies(filtered, proxyDisplayOrder) + const count = Math.ceil(sorted.length / cols) + groupCounts.push(count) + allProxies.push(sorted) } else { groupCounts.push(0) allProxies.push([]) } }) return { groupCounts, allProxies } - }, [groups, isOpen, proxyDisplayOrder, cols, searchValue]) + }, [groups, isOpen, proxyDisplayOrder, cols, searchValue, sortProxies]) const onChangeProxy = useCallback(async (group: string, proxy: string): Promise => { await mihomoChangeProxy(group, proxy) @@ -136,14 +152,12 @@ const Proxies: React.FC = () => { return newDelaying }) - // 本组测试状态 + // 管理测试状态 const groupProxies = allProxies[index] setProxyDelaying((prev) => { - const newProxyDelaying = { ...prev } - groupProxies.forEach(proxy => { - newProxyDelaying[proxy.name] = true - }) - return newProxyDelaying + const newSet = new Set(prev) + groupProxies.forEach(proxy => newSet.add(proxy.name)) + return newSet }) try { @@ -157,11 +171,11 @@ const Proxies: React.FC = () => { } catch { // ignore } finally { - // 立即更新状态 + // 更新状态 setProxyDelaying((prev) => { - const newProxyDelaying = { ...prev } - delete newProxyDelaying[proxy.name] - return newProxyDelaying + const newSet = new Set(prev) + newSet.delete(proxy.name) + return newSet }) mutate() } @@ -184,14 +198,12 @@ const Proxies: React.FC = () => { }) // 状态清理 setProxyDelaying((prev) => { - const newProxyDelaying = { ...prev } - groupProxies.forEach(proxy => { - delete newProxyDelaying[proxy.name] - }) - return newProxyDelaying + const newSet = new Set(prev) + groupProxies.forEach(proxy => newSet.delete(proxy.name)) + return newSet }) } - }, [allProxies, groups, delayTestConcurrency, mutate]) + }, [allProxies, groups, delayTestConcurrency, mutate, setIsOpen]) const calcCols = useCallback((): number => { if (proxyCols !== 'auto') { @@ -210,12 +222,203 @@ const Proxies: React.FC = () => { handleResize() // 初始化 window.addEventListener('resize', handleResize) - + return (): void => { window.removeEventListener('resize', handleResize) } }, [calcCols]) + // 预加载图片 + useEffect(() => { + const loadImages = async (): Promise => { + const imagesToLoad: string[] = [] + groups.forEach((group) => { + if (group.icon && group.icon.startsWith('http') && !localStorage.getItem(group.icon)) { + imagesToLoad.push(group.icon) + } + }) + + if (imagesToLoad.length > 0) { + const promises = imagesToLoad.map(async (url) => { + try { + const dataURL = await getImageDataURL(url) + localStorage.setItem(url, dataURL) + } catch (error) { + console.error('Failed to load image:', url, error) + } + }) + await Promise.all(promises) + mutate() + } + } + loadImages() + }, [groups, mutate]) + + const renderGroupContent = useCallback((index: number) => { + return groups[index] ? ( +
+ { + setIsOpen((prev) => { + const newOpen = [...prev] + newOpen[index] = !prev[index] + return newOpen + }) + }} + > + +
+
+ {groups[index].icon ? ( + + ) : null} +
+
+ {groups[index].name} +
+ {proxyDisplayMode === 'full' && ( +
+ {groups[index].type} +
+ )} + {proxyDisplayMode === 'full' && ( +
+ {groups[index].now} +
+ )} +
+
+
+ {proxyDisplayMode === 'full' && ( + + {groups[index].all.length} + + )} + { + setSearchValue((prev) => { + const newSearchValue = [...prev] + newSearchValue[index] = v + return newSearchValue + }) + }} + /> + + + +
+
+
+
+
+ ) : ( +
Never See This
+ ) + }, [groups, groupCounts, isOpen, proxyDisplayMode, searchValue, delaying, cols, allProxies, virtuosoRef, t, setIsOpen, onGroupDelay]) + + const renderItemContent = useCallback((index: number, groupIndex: number) => { + let innerIndex = index + groupCounts.slice(0, groupIndex).forEach((count) => { + innerIndex -= count + }) + return allProxies[groupIndex] ? ( +
+ {Array.from({ length: cols }).map((_, i) => { + if (!allProxies[groupIndex][innerIndex * cols + i]) return null + return ( + + ) + })} +
+ ) : ( +
Never See This
+ ) + }, [groupCounts, allProxies, proxyCols, cols, groups, proxyDisplayMode, proxyDelaying, mutate, onProxyDelay, onChangeProxy]) + return ( { ref={virtuosoRef} groupCounts={groupCounts} defaultItemHeight={80} - increaseViewportBy={{ top: 300, bottom: 300 }} - overscan={500} - computeItemKey={(index, groupIndex) => { - let innerIndex = index - groupCounts.slice(0, groupIndex).forEach((count) => { - innerIndex -= count - }) - const proxyIndex = innerIndex * cols - const proxy = allProxies[groupIndex]?.[proxyIndex] - return proxy ? `${groupIndex}-${proxy.name}` : `${groupIndex}-${index}` - }} - groupContent={(index) => { - if ( - groups[index] && - groups[index].icon && - groups[index].icon.startsWith('http') && - !localStorage.getItem(groups[index].icon) - ) { - getImageDataURL(groups[index].icon).then((dataURL) => { - localStorage.setItem(groups[index].icon, dataURL) - mutate() - }) - } - return groups[index] ? ( -
- { - setIsOpen((prev) => { - const newOpen = [...prev] - newOpen[index] = !prev[index] - return newOpen - }) - }} - > - -
-
- {groups[index].icon ? ( - - ) : null} -
-
- {groups[index].name} -
- {proxyDisplayMode === 'full' && ( -
- {groups[index].type} -
- )} - {proxyDisplayMode === 'full' && ( -
- {groups[index].now} -
- )} -
-
-
- {proxyDisplayMode === 'full' && ( - - {groups[index].all.length} - - )} - { - setSearchValue((prev) => { - const newSearchValue = [...prev] - newSearchValue[index] = v - return newSearchValue - }) - }} - /> - - - -
-
-
-
-
- ) : ( -
Never See This
- ) - }} - itemContent={(index, groupIndex) => { - let innerIndex = index - groupCounts.slice(0, groupIndex).forEach((count) => { - innerIndex -= count - }) - return allProxies[groupIndex] ? ( -
- {Array.from({ length: cols }).map((_, i) => { - if (!allProxies[groupIndex][innerIndex * cols + i]) return null - return ( - - ) - })} -
- ) : ( -
Never See This
- ) - }} + increaseViewportBy={{ top: 150, bottom: 150 }} + overscan={200} + computeItemKey={(index, groupIndex) => `${groupIndex}-${index}`} + groupContent={renderGroupContent} + itemContent={renderItemContent} /> )}