Files
clash-verge-rev/src/components/setting/setting-clash.tsx
T
Tunglies 6e08a77a84 refactor(backend): specta ipc bindings (#8195)
* refactor(ipc): generate bindings with Specta

* fix(profile): project IPC inputs to consumed fields

* fix(events): remove unused refresh payloads

* fix(webdav): return only consumed backup metadata

* fix(network): trim unused interface metadata

* fix(sysinfo): return only displayed system fields

* chore(knip): check generated runtime exports

* fix(proxy): confirm runtime chain updates before UI changes

* fix(clipboard): propagate proxy environment copy failures

* refactor(dns): remove ignored runtime patch

* refactor(core): trim unused upgrade response fields

* refactor(ipc): avoid redundant response copies

* refactor(proxy): clarify automatic proxy query naming

* chore(tests): remove added IPC regression tests

* refactor(ipc): enforce typed command boundaries

* chore(contract): check generated bindings before push

* refactor(ipc): remove unused proxy liveness payloads
2026-10-12 06:16:05 +00:00

357 lines
11 KiB
TypeScript

import { LanRounded, SettingsRounded } from '@mui/icons-material'
import { MenuItem, Select, TextField, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { updateGeo, type LogLevel } from 'tauri-plugin-mihomo-api'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useClash } from '@/hooks/use-clash'
import { useClashLog } from '@/hooks/use-clash-log'
import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
import { useProfiles } from '@/hooks/use-profiles'
import { useVerge } from '@/hooks/use-verge'
import { invoke_uwp_tool, setDnsOverride } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system'
import { ClashCoreViewer } from './mods/clash-core-viewer'
import { ClashPortViewer } from './mods/clash-port-viewer'
import { ControllerViewer } from './mods/controller-viewer'
import { DnsViewer } from './mods/dns-viewer'
import { HeaderConfiguration } from './mods/external-controller-cors'
import { GuardState } from './mods/guard-state'
import { NetworkInterfaceViewer } from './mods/network-interface-viewer'
import { SettingItem, SettingList } from './mods/setting-comp'
import { TunnelsViewer } from './mods/tunnels-viewer'
import { WebUIViewer } from './mods/web-ui-viewer'
const isWIN = getSystem() === 'windows'
interface Props {
onError: (err: Error) => void
}
const SettingClash = ({ onError }: Props) => {
const { t } = useTranslation()
const { clash, version, mutateClash, patchClash } = useClash()
const { verge, mutateVerge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
const displayedMixedPort = useDisplayedMixedPort()
const [, setClashLog] = useClashLog()
const {
ipv6,
'allow-lan': allowLan,
'log-level': logLevel,
'unified-delay': unifiedDelay,
} = clash ?? {}
const [dnsConfirmation, setDnsConfirmation] = useState<{
profileUid: string
source: string
} | null>(null)
const [dnsUpdating, setDnsUpdating] = useState(false)
const webRef = useRef<DialogRef>(null)
const portRef = useRef<DialogRef>(null)
const ctrlRef = useRef<DialogRef>(null)
const coreRef = useRef<DialogRef>(null)
const networkRef = useRef<DialogRef>(null)
const dnsRef = useRef<DialogRef>(null)
const corsRef = useRef<DialogRef>(null)
const tunnelRef = useRef<DialogRef>(null)
const onSwitchFormat = (_e: any, value: boolean) => value
const onChangeData = (patch: Partial<IConfigData>) => {
mutateClash((old) => ({ ...old!, ...patch }), false)
}
const onUpdateGeo = async () => {
try {
await mutate(() => updateGeo(), {
id: 'update-geo',
errorNotice: false,
})
showNotice.success('settings.feedback.notifications.clash.geoDataUpdated')
} catch (err: any) {
showNotice.error(err)
}
}
const handleDnsToggle = useLockFn(
async (
enable: boolean,
confirmation?: string,
profileUid = currentProfile?.uid,
) => {
if (!profileUid) return
setDnsUpdating(true)
try {
const result = await mutate(
() => setDnsOverride(profileUid, enable, confirmation),
{
id: 'set-dns-override',
errorNotice: false,
},
)
if (!result.ok) return
const outcome = result.value
if (outcome.status === 'confirmation_required') {
setDnsConfirmation({ profileUid, source: outcome.source })
} else {
setDnsConfirmation(null)
}
mutateVerge()
mutateClash()
} catch (err: any) {
showNotice.error(err)
} finally {
setDnsUpdating(false)
}
},
)
const closeDnsConfirmation = () => {
if (!dnsUpdating) setDnsConfirmation(null)
}
return (
<SettingList title={t('settings.sections.clash.title')}>
<WebUIViewer ref={webRef} />
<ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<BaseDialog
open={dnsConfirmation !== null}
title={t('settings.modals.dns.protection.title')}
okBtn={t('settings.modals.dns.protection.enableAnyway')}
cancelBtn={t('settings.modals.dns.protection.keepDisabled')}
contentSx={{ width: { xs: 320, sm: 420 } }}
loading={dnsUpdating}
disableCancel={dnsUpdating}
onCancel={closeDnsConfirmation}
onClose={closeDnsConfirmation}
onOk={() => {
if (dnsConfirmation)
void handleDnsToggle(
true,
dnsConfirmation.source,
dnsConfirmation.profileUid,
)
}}
>
<Typography variant="body2">
{t('settings.modals.dns.protection.message')}
</Typography>
</BaseDialog>
<HeaderConfiguration ref={corsRef} />
<TunnelsViewer ref={tunnelRef} />
<SettingItem
label={t('settings.sections.clash.form.fields.allowLan')}
extra={
<TooltipIcon
title={t('settings.sections.clash.form.tooltips.networkInterface')}
color={'inherit'}
icon={LanRounded}
onClick={() => {
networkRef.current?.open()
}}
/>
}
>
<GuardState
value={allowLan ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ 'allow-lan': e })}
onGuard={(e) => patchClash({ 'allow-lan': e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t('settings.sections.clash.form.fields.dnsOverwrite')}
extra={
<>
<TooltipIcon
icon={SettingsRounded}
onClick={() => dnsRef.current?.open()}
/>
<TooltipIcon
title={t('settings.modals.dns.dialog.profileScope')}
sx={{ opacity: '0.7' }}
/>
</>
}
>
<Switch
edge="end"
checked={dnsEnabled}
disabled={!currentProfile || dnsUpdating || dnsConfirmation !== null}
onChange={(_, checked) => handleDnsToggle(checked)}
/>
</SettingItem>
<SettingItem label={t('settings.sections.clash.form.fields.ipv6')}>
<GuardState
value={ipv6 ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ ipv6: e })}
onGuard={(e) => patchClash({ ipv6: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t('settings.sections.clash.form.fields.unifiedDelay')}
extra={
<TooltipIcon
title={t('settings.sections.clash.form.tooltips.unifiedDelay')}
sx={{ opacity: '0.7' }}
/>
}
>
<GuardState
value={unifiedDelay ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ 'unified-delay': e })}
onGuard={(e) => patchClash({ 'unified-delay': e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t('settings.sections.clash.form.fields.logLevel')}
extra={
<TooltipIcon
title={t('settings.sections.clash.form.tooltips.logLevel')}
sx={{ opacity: '0.7' }}
/>
}
>
<GuardState
value={logLevel === 'warn' ? 'warning' : (logLevel ?? 'info')}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ 'log-level': e })}
onGuard={(e) => {
setClashLog((pre) => ({
...pre!,
logLevel: e.toUpperCase() as LogLevel,
}))
return patchClash({ 'log-level': e })
}}
>
<Select size="small" sx={{ width: 100, '> div': { py: '7.5px' } }}>
<MenuItem value="debug">
{t('settings.sections.clash.form.options.logLevel.debug')}
</MenuItem>
<MenuItem value="info">
{t('settings.sections.clash.form.options.logLevel.info')}
</MenuItem>
<MenuItem value="warning">
{t('settings.sections.clash.form.options.logLevel.warning')}
</MenuItem>
<MenuItem value="error">
{t('settings.sections.clash.form.options.logLevel.error')}
</MenuItem>
<MenuItem value="silent">
{t('settings.sections.clash.form.options.logLevel.silent')}
</MenuItem>
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t('settings.sections.clash.form.fields.portConfig')}>
<TextField
autoComplete="new-password"
disabled={false}
size="small"
value={displayedMixedPort}
sx={{ width: 100, input: { py: '7.5px', cursor: 'pointer' } }}
onClick={(e) => {
portRef.current?.open()
;(e.target as HTMLElement).blur()
}}
/>
</SettingItem>
<SettingItem
label={t('settings.sections.clash.form.fields.external')}
extra={
<TooltipIcon
title={t('settings.sections.externalCors.tooltips.open')}
icon={SettingsRounded}
onClick={(e) => {
e.stopPropagation()
corsRef.current?.open()
}}
/>
}
onClick={() => {
ctrlRef.current?.open()
}}
/>
<SettingItem
onClick={() => webRef.current?.open()}
label={t('settings.sections.clash.form.fields.webUI')}
/>
<SettingItem
label={t('settings.sections.clash.form.fields.clashCore')}
extra={
<TooltipIcon
icon={SettingsRounded}
onClick={() => coreRef.current?.open()}
/>
}
>
<Typography sx={{ py: '7px', pr: 1 }}>{version}</Typography>
</SettingItem>
{isWIN && (
<SettingItem
onClick={invoke_uwp_tool}
label={t('settings.sections.clash.form.fields.openUwpTool')}
extra={
<TooltipIcon
title={t('settings.sections.clash.form.tooltips.openUwpTool')}
sx={{ opacity: '0.7' }}
/>
}
/>
)}
<SettingItem
onClick={onUpdateGeo}
label={t('settings.sections.clash.form.fields.updateGeoData')}
/>
<SettingItem
label={t('settings.sections.clash.form.fields.tunnels.title')}
onClick={() => tunnelRef.current?.open()}
/>
</SettingList>
)
}
export default SettingClash