mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
Show the speed warning with explicit enable/keep-disabled actions, and add a close button to the empty chain panel.
230 lines
6.7 KiB
TypeScript
230 lines
6.7 KiB
TypeScript
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
|
|
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
|
|
import { useLockFn } from 'ahooks'
|
|
import { useCallback, useEffect, useReducer, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
|
|
import { ProviderButton } from '@/components/proxy/provider-button'
|
|
import { ProxyGroups } from '@/components/proxy/proxy-groups'
|
|
import {
|
|
useAppRefreshers,
|
|
useClashConfigData,
|
|
} from '@/providers/app-data-context'
|
|
import {
|
|
getRuntimeProxyChainConfig,
|
|
patchClashMode,
|
|
updateProxyChainConfigInRuntime,
|
|
} from '@/services/cmds'
|
|
import { mutate } from '@/services/mutate'
|
|
import { showNotice } from '@/services/notice-service'
|
|
import { debugLog } from '@/utils/debug'
|
|
|
|
const MODES = ['rule', 'global', 'direct'] as const
|
|
type Mode = (typeof MODES)[number]
|
|
const MODE_SET = new Set<string>(MODES)
|
|
const isMode = (value: unknown): value is Mode =>
|
|
typeof value === 'string' && MODE_SET.has(value)
|
|
|
|
const ProxyPage = () => {
|
|
const { t } = useTranslation()
|
|
|
|
// 从 localStorage 恢复链式代理按钮状态
|
|
const [isChainMode, setIsChainMode] = useState(() => {
|
|
try {
|
|
const saved = localStorage.getItem('proxy-chain-mode-enabled')
|
|
return saved === 'true'
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
|
|
|
|
const [chainConfigData, dispatchChainConfigData] = useReducer(
|
|
(_: string | null, action: string | null) => action,
|
|
null as string | null,
|
|
)
|
|
|
|
const { clashConfig } = useClashConfigData()
|
|
const { refreshClashConfig } = useAppRefreshers()
|
|
|
|
const updateChainConfigData = useCallback((value: string | null) => {
|
|
dispatchChainConfigData(value)
|
|
}, [])
|
|
|
|
const normalizedMode = clashConfig?.mode?.toLowerCase()
|
|
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
|
|
const chainWarning = t('proxies.page.chain.warning')
|
|
|
|
const onChangeMode = useLockFn(async (mode: Mode) => {
|
|
try {
|
|
// patchClashMode 在后端 PATCH 失败时会 reject,需提示用户而非静默失败
|
|
await mutate(() => patchClashMode(mode), {
|
|
id: 'patch-clash-mode',
|
|
errorNotice: false,
|
|
})
|
|
await refreshClashConfig()
|
|
} catch (error) {
|
|
showNotice.error(error)
|
|
}
|
|
})
|
|
|
|
const applyChainMode = (enabled: boolean) => {
|
|
setIsChainMode(enabled)
|
|
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
|
|
}
|
|
|
|
const onExitChainMode = useLockFn(async () => {
|
|
// 退出链式代理模式时,清除链式代理配置
|
|
try {
|
|
debugLog('Exiting chain mode, clearing chain configuration')
|
|
const result = await mutate(() => updateProxyChainConfigInRuntime(null), {
|
|
id: 'update-proxy-chain-runtime',
|
|
errorNotice: false,
|
|
})
|
|
if (!result.ok) return
|
|
debugLog('Chain configuration cleared successfully')
|
|
} catch (error) {
|
|
console.error('Failed to clear chain configuration:', error)
|
|
showNotice.error(error)
|
|
return
|
|
}
|
|
applyChainMode(false)
|
|
})
|
|
|
|
const onToggleChainMode = () => {
|
|
if (isChainMode) onExitChainMode().catch(() => {})
|
|
else setChainConfirmOpen(true)
|
|
}
|
|
|
|
// 当开启链式代理模式时,获取配置数据
|
|
useEffect(() => {
|
|
if (!isChainMode) {
|
|
updateChainConfigData(null)
|
|
return
|
|
}
|
|
|
|
let cancelled = false
|
|
|
|
const fetchChainConfig = async () => {
|
|
try {
|
|
const exitNode = localStorage.getItem('proxy-chain-exit-node')
|
|
|
|
if (!exitNode) {
|
|
console.error('No proxy chain exit node found in localStorage')
|
|
if (!cancelled) {
|
|
updateChainConfigData('')
|
|
}
|
|
return
|
|
}
|
|
|
|
const configData = await getRuntimeProxyChainConfig(exitNode)
|
|
if (!cancelled) {
|
|
updateChainConfigData(configData || '')
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get runtime proxy chain config:', error)
|
|
if (!cancelled) {
|
|
updateChainConfigData('')
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchChainConfig().catch(() => {})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [isChainMode, updateChainConfigData])
|
|
|
|
useEffect(() => {
|
|
if (normalizedMode && !isMode(normalizedMode)) {
|
|
onChangeMode('rule').catch(() => {})
|
|
}
|
|
}, [normalizedMode, onChangeMode])
|
|
|
|
return (
|
|
<BasePage
|
|
full
|
|
contentStyle={{ height: '100%' }}
|
|
title={
|
|
isChainMode ? (
|
|
<Box
|
|
component="span"
|
|
data-tauri-drag-region="true"
|
|
sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}
|
|
>
|
|
{t('proxies.page.title.chainMode')}
|
|
<TooltipIcon
|
|
title={chainWarning}
|
|
icon={WarningRounded}
|
|
color="warning"
|
|
sx={{ p: 0.25 }}
|
|
/>
|
|
</Box>
|
|
) : (
|
|
t('proxies.page.title.default')
|
|
)
|
|
}
|
|
header={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<ProviderButton />
|
|
|
|
<ButtonGroup size="small">
|
|
{MODES.map((mode) => (
|
|
<Button
|
|
key={mode}
|
|
variant={mode === curMode ? 'contained' : 'outlined'}
|
|
onClick={() => onChangeMode(mode)}
|
|
sx={{ textTransform: 'capitalize' }}
|
|
>
|
|
{t(`proxies.page.modes.${mode}`)}
|
|
</Button>
|
|
))}
|
|
</ButtonGroup>
|
|
|
|
<Button
|
|
size="small"
|
|
variant={isChainMode ? 'contained' : 'outlined'}
|
|
onClick={onToggleChainMode}
|
|
sx={{ ml: 1 }}
|
|
startIcon={
|
|
isChainMode ? (
|
|
<LanRounded fontSize="small" />
|
|
) : (
|
|
<LanOutlined fontSize="small" />
|
|
)
|
|
}
|
|
>
|
|
{t('proxies.page.actions.toggleChain')}
|
|
</Button>
|
|
</Box>
|
|
}
|
|
>
|
|
<ProxyGroups
|
|
mode={curMode ?? 'rule'}
|
|
isChainMode={isChainMode}
|
|
chainConfigData={chainConfigData}
|
|
onExitChainMode={onExitChainMode}
|
|
/>
|
|
<BaseDialog
|
|
open={chainConfirmOpen}
|
|
title={t('proxies.page.actions.toggleChain')}
|
|
okBtn={t('proxies.page.chain.confirmEnable')}
|
|
cancelBtn={t('proxies.page.chain.keepDisabled')}
|
|
contentSx={{ width: { xs: 320, sm: 420 } }}
|
|
onCancel={() => setChainConfirmOpen(false)}
|
|
onClose={() => setChainConfirmOpen(false)}
|
|
onOk={() => {
|
|
setChainConfirmOpen(false)
|
|
applyChainMode(true)
|
|
}}
|
|
>
|
|
<Typography variant="body2">{chainWarning}</Typography>
|
|
</BaseDialog>
|
|
</BasePage>
|
|
)
|
|
}
|
|
|
|
export default ProxyPage
|