mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
* 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
357 lines
11 KiB
TypeScript
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
|