mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
Compare commits
3 Commits
a7de9b2588
...
3aff005f81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aff005f81 | ||
|
|
7bdebcf298 | ||
|
|
a7e769f402 |
@ -1,11 +1,13 @@
|
|||||||
## 1.8.8
|
## 1.8.8
|
||||||
|
|
||||||
### 新功能 (Feat)
|
### 新功能 (Feat)
|
||||||
|
- 升级内核版本
|
||||||
- 增加内核版本选择
|
- 增加内核版本选择
|
||||||
- 记住日志页面的筛选关键字
|
- 记住日志页面的筛选关键字
|
||||||
- Webdav增加Cron定时备份
|
- Webdav增加Cron定时备份
|
||||||
- 连接卡片纯数字显示样式
|
- 连接卡片纯数字显示样式
|
||||||
- 支持修改窗口触发行为
|
- 支持修改点击任务栏的窗口触发行为
|
||||||
|
- 在设置里增加 WebUI 快捷打开方式
|
||||||
|
|
||||||
### 修复 (Fix)
|
### 修复 (Fix)
|
||||||
- MacOS 首次启动时的 ENOENT: no such file or directory(config.yaml)
|
- MacOS 首次启动时的 ENOENT: no such file or directory(config.yaml)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ReactNode } from 'react'
|
|||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,7 +55,7 @@ interface Props {
|
|||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseErrorBoundary = (props: Props): JSX.Element => {
|
const BaseErrorBoundary = (props: Props): React.ReactElement => {
|
||||||
return <ErrorBoundary FallbackComponent={ErrorFallback}>{props.children}</ErrorBoundary>
|
return <ErrorBoundary FallbackComponent={ErrorFallback}>{props.children}</ErrorBoundary>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { GenIcon } from 'react-icons'
|
import { GenIcon, IconBaseProps } from 'react-icons'
|
||||||
function SubStoreIcon(props): JSX.Element {
|
import React from 'react'
|
||||||
|
|
||||||
|
function SubStoreIcon(props: IconBaseProps): React.ReactElement {
|
||||||
return GenIcon({
|
return GenIcon({
|
||||||
tag: 'svg',
|
tag: 'svg',
|
||||||
attr: { viewBox: '0 0 192 192' },
|
attr: { viewBox: '0 0 192 192' },
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Button, Card, CardBody } from '@heroui/react'
|
import { Button, Card, CardBody } from '@heroui/react'
|
||||||
import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
|
import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
|
||||||
import React from 'react'
|
import React, { useMemo, useState, useCallback } from 'react'
|
||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { FaMapPin } from 'react-icons/fa6'
|
import { FaMapPin } from 'react-icons/fa6'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -16,7 +15,14 @@ interface Props {
|
|||||||
isGroupTesting?: boolean
|
isGroupTesting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProxyItem: React.FC<Props> = (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<Props> = React.memo((props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
|
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
|
||||||
|
|
||||||
@ -25,34 +31,27 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
return proxy.history[proxy.history.length - 1].delay
|
return proxy.history[proxy.history.length - 1].delay
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}, [proxy])
|
}, [proxy.history])
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const isLoading = loading || isGroupTesting
|
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 === -1) return t('proxies.delay.test')
|
||||||
if (delay === 0) return t('proxies.delay.timeout')
|
if (delay === 0) return t('proxies.delay.timeout')
|
||||||
return delay.toString()
|
return delay.toString()
|
||||||
}
|
}, [delay, t])
|
||||||
|
|
||||||
const onDelay = (): void => {
|
const onDelay = useCallback((): void => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
onProxyDelay(proxy.name, group.testUrl).finally(() => {
|
onProxyDelay(proxy.name, group.testUrl).finally(() => {
|
||||||
mutateProxies()
|
mutateProxies()
|
||||||
setLoading(false)
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -60,7 +59,7 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
onPress={() => onSelect(group.name, proxy.name)}
|
onPress={() => onSelect(group.name, proxy.name)}
|
||||||
isPressable
|
isPressable
|
||||||
fullWidth
|
fullWidth
|
||||||
shadow="xs"
|
shadow="sm"
|
||||||
className={`${
|
className={`${
|
||||||
fixed
|
fixed
|
||||||
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'
|
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'
|
||||||
@ -118,7 +117,7 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap"
|
className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-end">
|
<div className="w-full h-full flex items-center justify-end">
|
||||||
{delayText(delay)}
|
{delayText}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -156,7 +155,7 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-end">
|
<div className="w-full h-full flex items-center justify-end">
|
||||||
{delayText(delay)}
|
{delayText}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -165,6 +164,18 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}, (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
|
export default ProxyItem
|
||||||
@ -61,7 +61,7 @@ const DNS: React.FC = () => {
|
|||||||
proxyServerNameserver,
|
proxyServerNameserver,
|
||||||
directNameserver,
|
directNameserver,
|
||||||
fallback,
|
fallback,
|
||||||
fallbackGeoip: fallbackFilter?.geoip || true,
|
fallbackGeoip: (fallbackFilter?.geoip || true) as string | true | string[],
|
||||||
fallbackGeoipCode: fallbackFilter?.['geoip-code'] || 'CN',
|
fallbackGeoipCode: fallbackFilter?.['geoip-code'] || 'CN',
|
||||||
fallbackIpcidr: fallbackFilter?.ipcidr || ['240.0.0.0/4', '0.0.0.0/32'],
|
fallbackIpcidr: fallbackFilter?.ipcidr || ['240.0.0.0/4', '0.0.0.0/32'],
|
||||||
fallbackDomain: fallbackFilter?.domain || ['+.google.com', '+.facebook.com', '+.youtube.com'],
|
fallbackDomain: fallbackFilter?.domain || ['+.google.com', '+.facebook.com', '+.youtube.com'],
|
||||||
@ -174,7 +174,7 @@ const DNS: React.FC = () => {
|
|||||||
'direct-nameserver': values.directNameserver,
|
'direct-nameserver': values.directNameserver,
|
||||||
fallback: values.fallback,
|
fallback: values.fallback,
|
||||||
'fallback-filter': {
|
'fallback-filter': {
|
||||||
geoip: values.fallbackGeoip,
|
...(values.fallbackGeoip ? { geoip: values.fallbackGeoip } : {}),
|
||||||
'geoip-code': values.fallbackGeoipCode,
|
'geoip-code': values.fallbackGeoipCode,
|
||||||
ipcidr: values.fallbackIpcidr,
|
ipcidr: values.fallbackIpcidr,
|
||||||
domain: values.fallbackDomain
|
domain: values.fallbackDomain
|
||||||
@ -415,9 +415,9 @@ const DNS: React.FC = () => {
|
|||||||
<SettingItem title={t('dns.fallbackFilter.geoip')} divider>
|
<SettingItem title={t('dns.fallbackFilter.geoip')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={values.fallbackGeoip}
|
isSelected={!!values.fallbackGeoip}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, fallbackGeoip: v })
|
setValues({ ...values, fallbackGeoip: v as string | true | string[] })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
@ -425,7 +425,7 @@ const DNS: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[100px]"
|
className="w-[100px]"
|
||||||
value={values.fallbackGeoipCode}
|
value={typeof values.fallbackGeoipCode === 'string' ? values.fallbackGeoipCode : ''}
|
||||||
placeholder="CN"
|
placeholder="CN"
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, fallbackGeoipCode: v })
|
setValues({ ...values, fallbackGeoipCode: v })
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const Logs: React.FC = () => {
|
|||||||
localStorage.setItem(LOGS_FILTER_KEY, filter)
|
localStorage.setItem(LOGS_FILTER_KEY, filter)
|
||||||
}, [filter])
|
}, [filter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect((): void | (() => void) => {
|
||||||
if (!trace) {
|
if (!trace) {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (container) {
|
if (container) {
|
||||||
|
|||||||
@ -92,7 +92,7 @@ const Override: React.FC = () => {
|
|||||||
if (event.dataTransfer?.files) {
|
if (event.dataTransfer?.files) {
|
||||||
const file = event.dataTransfer.files[0]
|
const file = event.dataTransfer.files[0]
|
||||||
if (file.name.endsWith('.js') || file.name.endsWith('.yaml')) {
|
if (file.name.endsWith('.js') || file.name.endsWith('.yaml')) {
|
||||||
const content = await readTextFile(file.path)
|
const content = await readTextFile((file as File & { path: string }).path)
|
||||||
try {
|
try {
|
||||||
await addOverrideItem({
|
await addOverrideItem({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
|||||||
@ -23,14 +23,39 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
|
const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
|
||||||
|
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
|
||||||
|
|
||||||
// 自定义 hook 用于管理展开状态
|
// 自定义 hook 用于管理展开状态
|
||||||
const useProxyState = (groups: IMihomoMixedGroup[]): {
|
const useProxyState = (groups: IMihomoMixedGroup[]): {
|
||||||
virtuosoRef: React.RefObject<GroupedVirtuosoHandle>;
|
virtuosoRef: React.RefObject<GroupedVirtuosoHandle | null>;
|
||||||
isOpen: boolean[];
|
isOpen: boolean[];
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
|
||||||
|
initialTopMostItemIndex: number;
|
||||||
|
handleRangeChanged: (range: { startIndex: number }) => void;
|
||||||
} => {
|
} => {
|
||||||
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
|
const virtuosoRef = useRef<GroupedVirtuosoHandle | null>(null)
|
||||||
|
|
||||||
|
// 记住滚动位置
|
||||||
|
const [initialTopMostItemIndex] = useState<number>(() => {
|
||||||
|
try {
|
||||||
|
const savedPosition = sessionStorage.getItem(SCROLL_POSITION_KEY)
|
||||||
|
if (savedPosition) {
|
||||||
|
sessionStorage.removeItem(SCROLL_POSITION_KEY)
|
||||||
|
return parseInt(savedPosition, 10) || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore scroll position:', error)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRangeChanged = useCallback((range: { startIndex: number }) => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(SCROLL_POSITION_KEY, range.startIndex.toString())
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save scroll position:', error)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 初始化展开状态
|
// 初始化展开状态
|
||||||
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
||||||
@ -55,7 +80,9 @@ const useProxyState = (groups: IMihomoMixedGroup[]): {
|
|||||||
return {
|
return {
|
||||||
virtuosoRef,
|
virtuosoRef,
|
||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen
|
setIsOpen,
|
||||||
|
initialTopMostItemIndex,
|
||||||
|
handleRangeChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,41 +101,57 @@ const Proxies: React.FC = () => {
|
|||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
|
|
||||||
const [cols, setCols] = useState(1)
|
const [cols, setCols] = useState(1)
|
||||||
const { virtuosoRef, isOpen, setIsOpen } = useProxyState(groups)
|
const { virtuosoRef, isOpen, setIsOpen, initialTopMostItemIndex, handleRangeChanged } = useProxyState(groups)
|
||||||
const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
|
const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
|
||||||
const [proxyDelaying, setProxyDelaying] = useState<Record<string, boolean>>({})
|
const [proxyDelaying, setProxyDelaying] = useState<Set<string>>(new Set())
|
||||||
const [searchValue, setSearchValue] = useState(Array(groups.length).fill(''))
|
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, allProxies } = useMemo(() => {
|
||||||
const groupCounts: number[] = []
|
const groupCounts: number[] = []
|
||||||
const allProxies: (IMihomoProxy | IMihomoGroup)[][] = []
|
const allProxies: (IMihomoProxy | IMihomoGroup)[][] = []
|
||||||
if (groups.length !== searchValue.length) setSearchValue(Array(groups.length).fill(''))
|
|
||||||
groups.forEach((group, index) => {
|
groups.forEach((group, index) => {
|
||||||
if (isOpen[index]) {
|
if (isOpen[index]) {
|
||||||
let groupProxies = group.all.filter(
|
const filtered = group.all.filter(
|
||||||
(proxy) => proxy && includesIgnoreCase(proxy.name, searchValue[index])
|
(proxy) => proxy && includesIgnoreCase(proxy.name, searchValue[index])
|
||||||
)
|
)
|
||||||
const count = Math.floor(groupProxies.length / cols)
|
const sorted = sortProxies(filtered, proxyDisplayOrder)
|
||||||
groupCounts.push(groupProxies.length % cols === 0 ? count : count + 1)
|
const count = Math.ceil(sorted.length / cols)
|
||||||
if (proxyDisplayOrder === 'delay') {
|
groupCounts.push(count)
|
||||||
groupProxies = groupProxies.sort((a, b) => {
|
allProxies.push(sorted)
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
groupCounts.push(0)
|
groupCounts.push(0)
|
||||||
allProxies.push([])
|
allProxies.push([])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return { groupCounts, allProxies }
|
return { groupCounts, allProxies }
|
||||||
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue])
|
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue, sortProxies])
|
||||||
|
|
||||||
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
|
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
|
||||||
await mihomoChangeProxy(group, proxy)
|
await mihomoChangeProxy(group, proxy)
|
||||||
@ -136,14 +179,12 @@ const Proxies: React.FC = () => {
|
|||||||
return newDelaying
|
return newDelaying
|
||||||
})
|
})
|
||||||
|
|
||||||
// 本组测试状态
|
// 管理测试状态
|
||||||
const groupProxies = allProxies[index]
|
const groupProxies = allProxies[index]
|
||||||
setProxyDelaying((prev) => {
|
setProxyDelaying((prev) => {
|
||||||
const newProxyDelaying = { ...prev }
|
const newSet = new Set(prev)
|
||||||
groupProxies.forEach(proxy => {
|
groupProxies.forEach(proxy => newSet.add(proxy.name))
|
||||||
newProxyDelaying[proxy.name] = true
|
return newSet
|
||||||
})
|
|
||||||
return newProxyDelaying
|
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -157,11 +198,11 @@ const Proxies: React.FC = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
// 立即更新状态
|
// 更新状态
|
||||||
setProxyDelaying((prev) => {
|
setProxyDelaying((prev) => {
|
||||||
const newProxyDelaying = { ...prev }
|
const newSet = new Set(prev)
|
||||||
delete newProxyDelaying[proxy.name]
|
newSet.delete(proxy.name)
|
||||||
return newProxyDelaying
|
return newSet
|
||||||
})
|
})
|
||||||
mutate()
|
mutate()
|
||||||
}
|
}
|
||||||
@ -184,14 +225,12 @@ const Proxies: React.FC = () => {
|
|||||||
})
|
})
|
||||||
// 状态清理
|
// 状态清理
|
||||||
setProxyDelaying((prev) => {
|
setProxyDelaying((prev) => {
|
||||||
const newProxyDelaying = { ...prev }
|
const newSet = new Set(prev)
|
||||||
groupProxies.forEach(proxy => {
|
groupProxies.forEach(proxy => newSet.delete(proxy.name))
|
||||||
delete newProxyDelaying[proxy.name]
|
return newSet
|
||||||
})
|
|
||||||
return newProxyDelaying
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [allProxies, groups, delayTestConcurrency, mutate])
|
}, [allProxies, groups, delayTestConcurrency, mutate, setIsOpen])
|
||||||
|
|
||||||
const calcCols = useCallback((): number => {
|
const calcCols = useCallback((): number => {
|
||||||
if (proxyCols !== 'auto') {
|
if (proxyCols !== 'auto') {
|
||||||
@ -210,12 +249,203 @@ const Proxies: React.FC = () => {
|
|||||||
|
|
||||||
handleResize() // 初始化
|
handleResize() // 初始化
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
}
|
}
|
||||||
}, [calcCols])
|
}, [calcCols])
|
||||||
|
|
||||||
|
// 预加载图片
|
||||||
|
useEffect(() => {
|
||||||
|
const loadImages = async (): Promise<void> => {
|
||||||
|
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] ? (
|
||||||
|
<div
|
||||||
|
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
as="div"
|
||||||
|
isPressable
|
||||||
|
fullWidth
|
||||||
|
onPress={() => {
|
||||||
|
setIsOpen((prev) => {
|
||||||
|
const newOpen = [...prev]
|
||||||
|
newOpen[index] = !prev[index]
|
||||||
|
return newOpen
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardBody className="w-full">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
{groups[index].icon ? (
|
||||||
|
<Avatar
|
||||||
|
className="bg-transparent mr-2"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
src={
|
||||||
|
groups[index].icon.startsWith('<svg')
|
||||||
|
? `data:image/svg+xml;utf8,${groups[index].icon}`
|
||||||
|
: localStorage.getItem(groups[index].icon) || groups[index].icon
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
<div
|
||||||
|
title={groups[index].name}
|
||||||
|
className="inline flag-emoji h-[32px] text-md leading-[32px]"
|
||||||
|
>
|
||||||
|
{groups[index].name}
|
||||||
|
</div>
|
||||||
|
{proxyDisplayMode === 'full' && (
|
||||||
|
<div
|
||||||
|
title={groups[index].type}
|
||||||
|
className="inline ml-2 text-sm text-foreground-500"
|
||||||
|
>
|
||||||
|
{groups[index].type}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{proxyDisplayMode === 'full' && (
|
||||||
|
<div className="inline flag-emoji ml-2 text-sm text-foreground-500">
|
||||||
|
{groups[index].now}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{proxyDisplayMode === 'full' && (
|
||||||
|
<Chip size="sm" className="my-1 mr-2">
|
||||||
|
{groups[index].all.length}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
<CollapseInput
|
||||||
|
title={t('proxies.search.placeholder')}
|
||||||
|
value={searchValue[index]}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setSearchValue((prev) => {
|
||||||
|
const newSearchValue = [...prev]
|
||||||
|
newSearchValue[index] = v
|
||||||
|
return newSearchValue
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={t('proxies.locate')}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
onPress={() => {
|
||||||
|
if (!isOpen[index]) {
|
||||||
|
setIsOpen((prev) => {
|
||||||
|
const newOpen = [...prev]
|
||||||
|
newOpen[index] = true
|
||||||
|
return newOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let i = 0
|
||||||
|
for (let j = 0; j < index; j++) {
|
||||||
|
i += groupCounts[j]
|
||||||
|
}
|
||||||
|
i += Math.floor(
|
||||||
|
allProxies[index].findIndex(
|
||||||
|
(proxy) => proxy.name === groups[index].now
|
||||||
|
) / cols
|
||||||
|
)
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index: Math.floor(i),
|
||||||
|
align: 'start'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaLocationCrosshairs className="text-lg text-foreground-500" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title={t('proxies.delay.test')}
|
||||||
|
variant="light"
|
||||||
|
isLoading={delaying[index]}
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
onPress={() => {
|
||||||
|
onGroupDelay(index)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdOutlineSpeed className="text-lg text-foreground-500" />
|
||||||
|
</Button>
|
||||||
|
<IoIosArrowBack
|
||||||
|
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>Never See This</div>
|
||||||
|
)
|
||||||
|
}, [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] ? (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
proxyCols !== 'auto'
|
||||||
|
? { gridTemplateColumns: `repeat(${proxyCols}, minmax(0, 1fr))` }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
className={`grid ${proxyCols === 'auto' ? 'sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' : ''} ${groupIndex === groupCounts.length - 1 && innerIndex === groupCounts[groupIndex] - 1 ? 'pb-2' : ''} gap-2 pt-2 mx-2`}
|
||||||
|
>
|
||||||
|
{Array.from({ length: cols }).map((_, i) => {
|
||||||
|
if (!allProxies[groupIndex][innerIndex * cols + i]) return null
|
||||||
|
return (
|
||||||
|
<ProxyItem
|
||||||
|
key={allProxies[groupIndex][innerIndex * cols + i].name}
|
||||||
|
mutateProxies={mutate}
|
||||||
|
onProxyDelay={onProxyDelay}
|
||||||
|
onSelect={onChangeProxy}
|
||||||
|
proxy={allProxies[groupIndex][innerIndex * cols + i]}
|
||||||
|
group={groups[groupIndex]}
|
||||||
|
proxyDisplayMode={proxyDisplayMode}
|
||||||
|
selected={
|
||||||
|
allProxies[groupIndex][innerIndex * cols + i]?.name ===
|
||||||
|
groups[groupIndex].now
|
||||||
|
}
|
||||||
|
isGroupTesting={proxyDelaying.has(allProxies[groupIndex][innerIndex * cols + i].name)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>Never See This</div>
|
||||||
|
)
|
||||||
|
}, [groupCounts, allProxies, proxyCols, cols, groups, proxyDisplayMode, proxyDelaying, mutate, onProxyDelay, onChangeProxy])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t('proxies.title')}
|
title={t('proxies.title')}
|
||||||
@ -278,191 +508,13 @@ const Proxies: React.FC = () => {
|
|||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
groupCounts={groupCounts}
|
groupCounts={groupCounts}
|
||||||
defaultItemHeight={80}
|
defaultItemHeight={80}
|
||||||
increaseViewportBy={{ top: 300, bottom: 300 }}
|
increaseViewportBy={{ top: 150, bottom: 150 }}
|
||||||
overscan={500}
|
overscan={200}
|
||||||
computeItemKey={(index, groupIndex) => {
|
initialTopMostItemIndex={initialTopMostItemIndex}
|
||||||
let innerIndex = index
|
rangeChanged={handleRangeChanged}
|
||||||
groupCounts.slice(0, groupIndex).forEach((count) => {
|
computeItemKey={(index, groupIndex) => `${groupIndex}-${index}`}
|
||||||
innerIndex -= count
|
groupContent={renderGroupContent}
|
||||||
})
|
itemContent={renderItemContent}
|
||||||
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] ? (
|
|
||||||
<div
|
|
||||||
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
as="div"
|
|
||||||
isPressable
|
|
||||||
fullWidth
|
|
||||||
onPress={() => {
|
|
||||||
setIsOpen((prev) => {
|
|
||||||
const newOpen = [...prev]
|
|
||||||
newOpen[index] = !prev[index]
|
|
||||||
return newOpen
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardBody className="w-full">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
|
|
||||||
{groups[index].icon ? (
|
|
||||||
<Avatar
|
|
||||||
className="bg-transparent mr-2"
|
|
||||||
size="sm"
|
|
||||||
radius="sm"
|
|
||||||
src={
|
|
||||||
groups[index].icon.startsWith('<svg')
|
|
||||||
? `data:image/svg+xml;utf8,${groups[index].icon}`
|
|
||||||
: localStorage.getItem(groups[index].icon) || groups[index].icon
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
|
||||||
<div
|
|
||||||
title={groups[index].name}
|
|
||||||
className="inline flag-emoji h-[32px] text-md leading-[32px]"
|
|
||||||
>
|
|
||||||
{groups[index].name}
|
|
||||||
</div>
|
|
||||||
{proxyDisplayMode === 'full' && (
|
|
||||||
<div
|
|
||||||
title={groups[index].type}
|
|
||||||
className="inline ml-2 text-sm text-foreground-500"
|
|
||||||
>
|
|
||||||
{groups[index].type}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{proxyDisplayMode === 'full' && (
|
|
||||||
<div className="inline flag-emoji ml-2 text-sm text-foreground-500">
|
|
||||||
{groups[index].now}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
{proxyDisplayMode === 'full' && (
|
|
||||||
<Chip size="sm" className="my-1 mr-2">
|
|
||||||
{groups[index].all.length}
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
<CollapseInput
|
|
||||||
title={t('proxies.search.placeholder')}
|
|
||||||
value={searchValue[index]}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setSearchValue((prev) => {
|
|
||||||
const newSearchValue = [...prev]
|
|
||||||
newSearchValue[index] = v
|
|
||||||
return newSearchValue
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title={t('proxies.locate')}
|
|
||||||
variant="light"
|
|
||||||
size="sm"
|
|
||||||
isIconOnly
|
|
||||||
onPress={() => {
|
|
||||||
if (!isOpen[index]) {
|
|
||||||
setIsOpen((prev) => {
|
|
||||||
const newOpen = [...prev]
|
|
||||||
newOpen[index] = true
|
|
||||||
return newOpen
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let i = 0
|
|
||||||
for (let j = 0; j < index; j++) {
|
|
||||||
i += groupCounts[j]
|
|
||||||
}
|
|
||||||
i += Math.floor(
|
|
||||||
allProxies[index].findIndex(
|
|
||||||
(proxy) => proxy.name === groups[index].now
|
|
||||||
) / cols
|
|
||||||
)
|
|
||||||
virtuosoRef.current?.scrollToIndex({
|
|
||||||
index: Math.floor(i),
|
|
||||||
align: 'start'
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaLocationCrosshairs className="text-lg text-foreground-500" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
title={t('proxies.delay.test')}
|
|
||||||
variant="light"
|
|
||||||
isLoading={delaying[index]}
|
|
||||||
size="sm"
|
|
||||||
isIconOnly
|
|
||||||
onPress={() => {
|
|
||||||
onGroupDelay(index)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MdOutlineSpeed className="text-lg text-foreground-500" />
|
|
||||||
</Button>
|
|
||||||
<IoIosArrowBack
|
|
||||||
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>Never See This</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
itemContent={(index, groupIndex) => {
|
|
||||||
let innerIndex = index
|
|
||||||
groupCounts.slice(0, groupIndex).forEach((count) => {
|
|
||||||
innerIndex -= count
|
|
||||||
})
|
|
||||||
return allProxies[groupIndex] ? (
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
proxyCols !== 'auto'
|
|
||||||
? { gridTemplateColumns: `repeat(${proxyCols}, minmax(0, 1fr))` }
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
className={`grid ${proxyCols === 'auto' ? 'sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' : ''} ${groupIndex === groupCounts.length - 1 && innerIndex === groupCounts[groupIndex] - 1 ? 'pb-2' : ''} gap-2 pt-2 mx-2`}
|
|
||||||
>
|
|
||||||
{Array.from({ length: cols }).map((_, i) => {
|
|
||||||
if (!allProxies[groupIndex][innerIndex * cols + i]) return null
|
|
||||||
return (
|
|
||||||
<ProxyItem
|
|
||||||
key={allProxies[groupIndex][innerIndex * cols + i].name}
|
|
||||||
mutateProxies={mutate}
|
|
||||||
onProxyDelay={onProxyDelay}
|
|
||||||
onSelect={onChangeProxy}
|
|
||||||
proxy={allProxies[groupIndex][innerIndex * cols + i]}
|
|
||||||
group={groups[groupIndex]}
|
|
||||||
proxyDisplayMode={proxyDisplayMode}
|
|
||||||
selected={
|
|
||||||
allProxies[groupIndex][innerIndex * cols + i]?.name ===
|
|
||||||
groups[groupIndex].now
|
|
||||||
}
|
|
||||||
isGroupTesting={!!proxyDelaying[allProxies[groupIndex][innerIndex * cols + i].name]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>Never See This</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user