Files
clash-verge-rev/src/components/setting/mods/dns-viewer.tsx
T
wonfen ff1ce92fac feat(dns): remember override state per profile
Save the DNS override switch and session confirmation per profile so automatic protection only disables the affected profile. Preserve the legacy switch as the initial preference and show the profile scope beside the settings gear and in the DNS editor.

Refs #7993
2026-09-22 10:54:55 +08:00

1096 lines
34 KiB
TypeScript

import { RestartAltRounded } from '@mui/icons-material'
import {
Alert,
Box,
Button,
FormControl,
List,
ListItem,
ListItemText,
MenuItem,
Select,
styled,
TextField,
Typography,
} from '@mui/material'
import { invoke } from '@tauri-apps/api/core'
import { useLockFn } from 'ahooks'
import * as yaml from 'js-yaml'
import type { Ref } from 'react'
import {
useCallback,
useEffect,
useImperativeHandle,
useReducer,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
BaseDialog,
type DialogRef,
MonacoEditor,
Switch,
} from '@/components/base'
import { useClash } from '@/hooks/use-clash'
import { useProfiles } from '@/hooks/use-profiles'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { useThemeMode } from '@/services/states'
import type { MonacoEditorInstance } from '@/types/monaco'
import { MONACO_FONT_FAMILY } from '@/utils/font-family'
const Item = styled(ListItem)(() => ({
padding: '5px 2px',
'& textarea': {
lineHeight: 1.5,
fontSize: 14,
resize: 'vertical',
},
}))
type NameserverPolicy = Record<string, any>
function parseNameserverPolicy(str: string): NameserverPolicy {
const result: NameserverPolicy = {}
if (!str) return result
const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g
const matchs = [...str.matchAll(ruleRegex)]
matchs.forEach((match) => {
const [, domainsPart, serversPart] = match
const domains = [domainsPart.trim()]
const servers = serversPart.split(';').map((s) => s.trim())
domains.forEach((domain) => {
result[domain] = servers
})
})
return result
}
function formatNameserverPolicy(policy: unknown): string {
if (!policy || typeof policy !== 'object') return ''
return Object.entries(policy as Record<string, unknown>)
.map(([domain, servers]) => {
const serversStr = Array.isArray(servers) ? servers.join(';') : servers
return `${domain}=${serversStr}`
})
.join(', ')
}
function formatHosts(hosts: unknown): string {
if (!hosts || typeof hosts !== 'object') return ''
const result: string[] = []
Object.entries(hosts as Record<string, unknown>).forEach(
([domain, value]) => {
if (Array.isArray(value)) {
const ipsStr = value.join(';')
result.push(`${domain}=${ipsStr}`)
} else {
result.push(`${domain}=${value}`)
}
},
)
return result.join(', ')
}
function parseHosts(str: string): NameserverPolicy {
const result: NameserverPolicy = {}
if (!str) return result
str.split(',').forEach((item) => {
const parts = item.trim().split('=')
if (parts.length < 2) return
const domain = parts[0].trim()
const valueStr = parts.slice(1).join('=').trim()
if (valueStr.includes(';')) {
result[domain] = valueStr
.split(';')
.map((s) => s.trim())
.filter(Boolean)
} else {
result[domain] = valueStr
}
})
return result
}
function parseList(str: string): string[] {
if (!str?.trim()) return []
return str
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
const DEFAULT_DNS_CONFIG = {
enable: true,
listen: ':53',
'enhanced-mode': 'fake-ip' as 'fake-ip' | 'redir-host',
'fake-ip-range': '198.18.0.1/16',
'fake-ip-range6': 'fdfe:dcba:9876::1/64',
'fake-ip-filter-mode': 'blacklist' as 'blacklist' | 'whitelist',
'prefer-h3': false,
'respect-rules': false,
'use-hosts': false,
'use-system-hosts': false,
ipv6: true,
'fake-ip-filter': [
'*.lan',
'*.local',
'*.arpa',
'time.*.com',
'ntp.*.com',
'time.*.com',
'+.market.xiaomi.com',
'localhost.ptlogin2.qq.com',
'*.msftncsi.com',
'www.msftconnecttest.com',
],
'default-nameserver': [
'system',
'223.6.6.6',
'8.8.8.8',
'2400:3200::1',
'2001:4860:4860::8888',
],
nameserver: [
'8.8.8.8',
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
],
fallback: [],
'nameserver-policy': {},
'proxy-server-nameserver': [],
'direct-nameserver': [],
'direct-nameserver-follow-policy': false,
'fallback-filter': {
geoip: true,
'geoip-code': 'CN',
ipcidr: ['240.0.0.0/4', '0.0.0.0/32'],
domain: ['+.google.com', '+.facebook.com', '+.youtube.com'],
},
}
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { mutateClash } = useClash()
const { verge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<{
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpRange6: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}>({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
// Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer(
(_: string, next: string) => next,
'{ isInitValue: true }',
)
const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return
const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {}
const enhancedMode =
dnsConfig['enhanced-mode'] || DEFAULT_DNS_CONFIG['enhanced-mode']
const validEnhancedMode =
enhancedMode === 'fake-ip' || enhancedMode === 'redir-host'
? enhancedMode
: DEFAULT_DNS_CONFIG['enhanced-mode']
const fakeIpFilterMode =
dnsConfig['fake-ip-filter-mode'] ||
DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
const validFakeIpFilterMode =
fakeIpFilterMode === 'blacklist' || fakeIpFilterMode === 'whitelist'
? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
setValues({
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
fakeIpRange:
dnsConfig['fake-ip-range'] ?? DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6:
dnsConfig['fake-ip-range6'] ?? DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: validFakeIpFilterMode,
preferH3: dnsConfig['prefer-h3'] ?? DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules:
dnsConfig['respect-rules'] ?? DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: dnsConfig['use-hosts'] ?? DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts:
dnsConfig['use-system-hosts'] ?? DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter:
dnsConfig['fake-ip-filter']?.join(', ') ??
DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
nameserver:
dnsConfig.nameserver?.join(', ') ??
DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback:
dnsConfig.fallback?.join(', ') ??
DEFAULT_DNS_CONFIG.fallback.join(', '),
defaultNameserver:
dnsConfig['default-nameserver']?.join(', ') ??
DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
proxyServerNameserver:
dnsConfig['proxy-server-nameserver']?.join(', ') ??
(DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || ''),
directNameserver:
dnsConfig['direct-nameserver']?.join(', ') ??
(DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || ''),
directNameserverFollowPolicy:
dnsConfig['direct-nameserver-follow-policy'] ??
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'],
fallbackGeoip:
dnsConfig['fallback-filter']?.geoip ??
DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode:
dnsConfig['fallback-filter']?.['geoip-code'] ??
DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
dnsConfig['fallback-filter']?.ipcidr?.join(', ') ??
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr.join(', '),
fallbackDomain:
dnsConfig['fallback-filter']?.domain?.join(', ') ??
DEFAULT_DNS_CONFIG['fallback-filter'].domain.join(', '),
nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '',
})
}, [])
const generateDnsConfig = useCallback(() => {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}, [values])
const updateYamlFromValues = useCallback(() => {
const config: Record<string, any> = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
setYamlContent(yaml.dump(config, { forceQuotes: true }))
}, [generateDnsConfig, values.hosts])
const resetToDefaults = useCallback(() => {
setValues({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver:
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
updateYamlFromValues()
}, [updateYamlFromValues])
const updateValuesFromYaml = useCallback(() => {
try {
const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml)
} catch {
showNotice.error('settings.modals.dns.errors.invalidYaml')
}
}, [yamlContent, updateValuesFromConfig])
useEffect(() => {
if (skipYamlSyncRef.current) {
skipYamlSyncRef.current = false
return
}
updateYamlFromValues()
}, [updateYamlFromValues])
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
useEffect(() => {
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
}, [updateValuesFromYaml, updateYamlFromValues])
useEffect(() => {
if (visualization) {
latestUpdateValuesFromYamlRef.current()
} else {
latestUpdateYamlFromValuesRef.current()
}
}, [visualization])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const initDnsConfig = useCallback(async () => {
try {
const dnsConfig = await invoke<string | null>(
'get_dns_config_content',
{},
)
if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any
updateValuesFromConfig(config)
setYamlContent(dnsConfig)
} else {
resetToDefaults()
}
} catch (err) {
console.error('Failed to initialize DNS config', err)
resetToDefaults()
}
}, [resetToDefaults, updateValuesFromConfig])
useImperativeHandle(
ref,
() => ({
open: () => {
setOpen(true)
void initDnsConfig()
},
close: () => setOpen(false),
}),
[initDnsConfig],
)
const onSave = useLockFn(async () => {
try {
let config: Record<string, any>
if (visualization) {
config = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
} else {
const parsedConfig = yaml.load(yamlContent)
if (typeof parsedConfig !== 'object' || parsedConfig === null) {
throw new Error(t('settings.modals.dns.errors.invalid'))
}
config = parsedConfig as Record<string, any>
}
await invoke('save_dns_config', { dnsConfig: config })
const validation = await invoke<ValidationOutcome>(
'validate_dns_config',
{},
)
if (validation.status !== 'valid') {
const errorMsg =
validation.status === 'invalid'
? validation.message
: 'Configuration validation skipped'
let cleanErrorMsg = errorMsg
if (errorMsg.includes('level=error')) {
const errorLines = errorMsg
.split('\n')
.filter(
(line) =>
line.includes('level=error') ||
line.includes('level=fatal') ||
line.includes('failed'),
)
if (errorLines.length > 0) {
cleanErrorMsg = errorLines
.map((line) => {
const msgMatch = line.match(/msg="([^"]+)"/)
return msgMatch ? msgMatch[1] : line
})
.join(', ')
}
}
showNotice.error(
'settings.modals.dns.messages.configError',
cleanErrorMsg,
)
return
}
if (dnsEnabled) {
await invoke('apply_dns_config', { apply: true })
mutateClash()
}
setOpen(false)
showNotice.success('settings.modals.dns.messages.saved')
} catch (err) {
showNotice.error('settings.modals.dns.messages.configError', err)
}
})
const handleYamlChange = (value?: string) => {
setYamlContent(value || '')
}
const handleChange = (field: string) => (event: any) => {
const value =
event.target.type === 'checkbox'
? event.target.checked
: event.target.value
setValues((prev) => {
const newValues = {
...prev,
[field]: value,
}
if (visualization) {
setTimeout(() => {
updateYamlFromValues()
}, 0)
}
return newValues
})
}
return (
<BaseDialog
open={open}
disableEnforceFocus={!visualization}
title={
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{t('settings.modals.dns.dialog.title')}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="outlined"
size="small"
color="warning"
startIcon={<RestartAltRounded />}
onClick={resetToDefaults}
>
{t('shared.actions.resetToDefault')}
</Button>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev)
}}
>
{visualization
? t('shared.editorModes.advanced')
: t('shared.editorModes.visualization')}
</Button>
</Box>
</Box>
}
contentSx={{
width: 550,
overflow: 'auto',
...(visualization
? {}
: { padding: '0 24px', display: 'flex', flexDirection: 'column' }),
}}
okBtn={t('shared.actions.save')}
cancelBtn={t('shared.actions.cancel')}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<Alert severity="info" sx={{ mb: 2 }}>
{t('settings.modals.dns.dialog.profileScope')}
</Alert>
<Typography
variant="body2"
color="warning.main"
sx={{ mb: 2, mt: 0, fontStyle: 'italic' }}
>
{t('settings.modals.dns.dialog.warning')}
</Typography>
{visualization ? (
<List>
<Typography
variant="subtitle1"
sx={{ mt: 1, mb: 1, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.general')}
</Typography>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.enable')} />
<Switch
edge="end"
checked={values.enable}
onChange={handleChange('enable')}
/>
</Item>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.listen')} />
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.listen}
onChange={handleChange('listen')}
placeholder=":53"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.enhancedMode')}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.enhancedMode}
onChange={handleChange('enhancedMode')}
>
<MenuItem value="fake-ip">fake-ip</MenuItem>
<MenuItem value="redir-host">redir-host</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange')}
/>
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.fakeIpRange}
onChange={handleChange('fakeIpRange')}
placeholder="198.18.0.1/16"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange6')}
/>
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.fakeIpRange6}
onChange={handleChange('fakeIpRange6')}
placeholder="fdfe:dcba:9876::1/64"
sx={{ width: 200 }}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilterMode')}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.fakeIpFilterMode}
onChange={handleChange('fakeIpFilterMode')}
>
<MenuItem value="blacklist">blacklist</MenuItem>
<MenuItem value="whitelist">whitelist</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.ipv6.label')}
secondary={t('settings.modals.dns.fields.ipv6.description')}
/>
<Switch
edge="end"
checked={values.ipv6}
onChange={handleChange('ipv6')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.preferH3.label')}
secondary={t('settings.modals.dns.fields.preferH3.description')}
/>
<Switch
edge="end"
checked={values.preferH3}
onChange={handleChange('preferH3')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.respectRules.label')}
secondary={t(
'settings.modals.dns.fields.respectRules.description',
)}
/>
<Switch
edge="end"
checked={values.respectRules}
onChange={handleChange('respectRules')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.useHosts.label')}
secondary={t('settings.modals.dns.fields.useHosts.description')}
/>
<Switch
edge="end"
checked={values.useHosts}
onChange={handleChange('useHosts')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.useSystemHosts.label')}
secondary={t(
'settings.modals.dns.fields.useSystemHosts.description',
)}
/>
<Switch
edge="end"
checked={values.useSystemHosts}
onChange={handleChange('useSystemHosts')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.directPolicy.label')}
secondary={t(
'settings.modals.dns.fields.directPolicy.description',
)}
/>
<Switch
edge="end"
checked={values.directNameserverFollowPolicy}
onChange={handleChange('directNameserverFollowPolicy')}
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.defaultNameserver.label')}
secondary={t(
'settings.modals.dns.fields.defaultNameserver.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.defaultNameserver}
onChange={handleChange('defaultNameserver')}
placeholder="system,223.6.6.6, 8.8.8.8, 2400:3200::1, 2001:4860:4860::8888"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.nameserver.label')}
secondary={t('settings.modals.dns.fields.nameserver.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.nameserver}
onChange={handleChange('nameserver')}
placeholder="8.8.8.8, https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallback.label')}
secondary={t('settings.modals.dns.fields.fallback.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.fallback}
onChange={handleChange('fallback')}
placeholder="https://dns.alidns.com/dns-query, https://dns.google/dns-query, https://cloudflare-dns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.proxy.label')}
secondary={t('settings.modals.dns.fields.proxy.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.proxyServerNameserver}
onChange={handleChange('proxyServerNameserver')}
placeholder="https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.directNameserver.label')}
secondary={t(
'settings.modals.dns.fields.directNameserver.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.directNameserver}
onChange={handleChange('directNameserver')}
placeholder="system, 223.6.6.6"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilter.label')}
secondary={t(
'settings.modals.dns.fields.fakeIpFilter.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.fakeIpFilter}
onChange={handleChange('fakeIpFilter')}
placeholder="*.lan, *.local, localhost.ptlogin2.qq.com"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.nameserverPolicy.label')}
secondary={t(
'settings.modals.dns.fields.nameserverPolicy.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.nameserverPolicy}
onChange={handleChange('nameserverPolicy')}
placeholder="+.arpa=10.0.0.1, rule-set:cn=https://doh.pub/dns-query;https://dns.alidns.com/dns-query"
/>
</Item>
<Typography
variant="subtitle2"
sx={{ mt: 2, mb: 1, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.fallbackFilter')}
</Typography>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.geoipFiltering.label')}
secondary={t(
'settings.modals.dns.fields.geoipFiltering.description',
)}
/>
<Switch
edge="end"
checked={values.fallbackGeoip}
onChange={handleChange('fallbackGeoip')}
/>
</Item>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.geoipCode')} />
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.fallbackGeoipCode}
onChange={handleChange('fallbackGeoipCode')}
placeholder="CN"
sx={{ width: 100 }}
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackIpCidr.label')}
secondary={t(
'settings.modals.dns.fields.fallbackIpCidr.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.fallbackIpcidr}
onChange={handleChange('fallbackIpcidr')}
placeholder="240.0.0.0/4, 127.0.0.1/8"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackDomain.label')}
secondary={t(
'settings.modals.dns.fields.fallbackDomain.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.fallbackDomain}
onChange={handleChange('fallbackDomain')}
placeholder="+.google.com, +.facebook.com, +.youtube.com"
/>
</Item>
<Typography
variant="subtitle1"
sx={{ mt: 3, mb: 0, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.hosts')}
</Typography>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.hosts.label')}
secondary={t('settings.modals.dns.fields.hosts.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.hosts}
onChange={handleChange('hosts')}
placeholder="*.clash.dev=127.0.0.1, alpha.clash.dev=::1, test.com=1.1.1.1;2.2.2.2, baidu.com=google.com"
/>
</Item>
</List>
) : (
<MonacoEditor
height="100vh"
language="yaml"
value={yamlContent}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
className="flex-grow"
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2,
minimap: {
enabled: document.documentElement.clientWidth >= 1500,
},
mouseWheelZoom: true,
quickSuggestions: {
strings: true,
comments: true,
other: true,
},
padding: {
top: 33,
},
fontFamily: MONACO_FONT_FAMILY,
fontLigatures: false,
smoothScrolling: true,
}}
onChange={handleYamlChange}
/>
)}
</BaseDialog>
)
}