mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 21:22:20 +08:00
Hostnames such as mi13.local are resolved by mDNS on the local network, which the proxy core cannot do, so they failed with 'couldn't find ip' while the system proxy was on. The macOS default already bypasses *.local. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
614 lines
19 KiB
TypeScript
614 lines
19 KiB
TypeScript
import { EditRounded } from '@mui/icons-material'
|
|
import {
|
|
Autocomplete,
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
InputAdornment,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
styled,
|
|
TextField,
|
|
Typography,
|
|
} from '@mui/material'
|
|
import { useLockFn } from 'ahooks'
|
|
import {
|
|
forwardRef,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useMemo,
|
|
useState,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import {
|
|
BaseDialog,
|
|
BaseFieldset,
|
|
BaseSplitChipEditor,
|
|
type DialogRef,
|
|
Switch,
|
|
TooltipIcon,
|
|
} from '@/components/base'
|
|
import { EditorViewer } from '@/components/profile/editor-viewer'
|
|
import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
|
|
import { useSystemProxyState } from '@/hooks/use-system-proxy-state'
|
|
import { useVerge } from '@/hooks/use-verge'
|
|
import { useSystemData } from '@/providers/app-data-context'
|
|
import {
|
|
getEmbeddedServerPort,
|
|
getNetworkInterfacesInfo,
|
|
getSystemHostname,
|
|
} from '@/services/cmds'
|
|
import { showNotice } from '@/services/notice-service'
|
|
import { debugLog } from '@/utils/debug'
|
|
import getSystem from '@/utils/get-system'
|
|
|
|
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
|
|
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
|
|
}`
|
|
|
|
/** NO_PROXY validation */
|
|
|
|
// *., cdn*., *, etc.
|
|
const domain_subdomain_part = String.raw`(?:[a-z0-9\-\*]+\.|\*)*`
|
|
// .*, .cn, .moe, .co*, *
|
|
const domain_tld_part = String.raw`(?:\w{2,64}\*?|\*)`
|
|
// *epicgames*, *skk.moe, *.skk.moe, skk.*, sponsor.cdn.skk.moe, *.*, etc.
|
|
// also matches 192.168.*, 10.*, 127.0.0.*, etc. (partial ipv4)
|
|
const rDomainSimple = domain_subdomain_part + domain_tld_part
|
|
|
|
const ipv4_part = String.raw`\d{1,3}`
|
|
|
|
const ipv6_part = '(?:[a-fA-F0-9:])+'
|
|
|
|
const rLocal = `localhost|<local>|localdomain`
|
|
|
|
const getValidReg = (isWindows: boolean) => {
|
|
// 127.0.0.1 (full ipv4)
|
|
const rIPv4Unix = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}(?:\/\d{1,2})?`
|
|
const rIPv4Windows = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`
|
|
|
|
const rIPv6Unix = String.raw`(?:${ipv6_part}:+)+${ipv6_part}(?:\/\d{1,3})?`
|
|
const rIPv6Windows = String.raw`(?:${ipv6_part}:+)+${ipv6_part}`
|
|
|
|
const rValidPart = `${rDomainSimple}|${
|
|
isWindows ? rIPv4Windows : rIPv4Unix
|
|
}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`
|
|
const separator = isWindows ? ';' : ','
|
|
const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`
|
|
|
|
return new RegExp(rValid)
|
|
}
|
|
|
|
const splitBypass = (value?: string) =>
|
|
(value ?? '')
|
|
.split(/[,\n;\r]+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
|
|
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|
const { t } = useTranslation()
|
|
const systemName = getSystem()
|
|
const isWindows = systemName === 'windows'
|
|
const validReg = useMemo(() => getValidReg(isWindows), [isWindows])
|
|
|
|
const [open, setOpen] = useState(false)
|
|
const [editorOpen, setEditorOpen] = useState(false)
|
|
const [pacEditorValue, setPacEditorValue] = useState(DEFAULT_PAC)
|
|
const [pacEditorSavedValue, setPacEditorSavedValue] = useState(DEFAULT_PAC)
|
|
const [saving, setSaving] = useState(false)
|
|
const { verge, patchVerge, mutateVerge } = useVerge()
|
|
const [hostOptions, setHostOptions] = useState<string[]>([])
|
|
|
|
const { indicator: isProxyReallyEnabled, invalidateProxyState } =
|
|
useSystemProxyState()
|
|
|
|
const {
|
|
enable_system_proxy: enabled,
|
|
proxy_auto_config,
|
|
pac_file_content,
|
|
enable_proxy_guard,
|
|
enable_bypass_check,
|
|
use_default_bypass,
|
|
system_proxy_bypass,
|
|
proxy_guard_duration,
|
|
proxy_host,
|
|
} = verge ?? {}
|
|
|
|
const [value, setValue] = useState({
|
|
guard: enable_proxy_guard,
|
|
enable_bypass_check: enable_bypass_check ?? true,
|
|
bypass: system_proxy_bypass,
|
|
duration: proxy_guard_duration ?? 10,
|
|
use_default: use_default_bypass ?? true,
|
|
pac: proxy_auto_config,
|
|
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
proxy_host: proxy_host ?? '127.0.0.1',
|
|
})
|
|
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
|
null,
|
|
)
|
|
|
|
useEffect(() => {
|
|
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
|
|
}, [])
|
|
|
|
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows])
|
|
|
|
const defaultBypass = () => {
|
|
if (isWindows) {
|
|
return 'localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;*.local;<local>'
|
|
}
|
|
if (systemName === 'linux') {
|
|
return 'localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1'
|
|
}
|
|
return '127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>'
|
|
}
|
|
|
|
const { systemProxyAddress } = useSystemData()
|
|
const displayedMixedPort = useDisplayedMixedPort()
|
|
|
|
const getSystemProxyAddress = useMemo(() => {
|
|
const isPacMode = value.pac ?? false
|
|
|
|
if (isPacMode) {
|
|
const host = value.proxy_host || '127.0.0.1'
|
|
return `${host}:${displayedMixedPort}`
|
|
} else {
|
|
return systemProxyAddress
|
|
}
|
|
}, [value.pac, value.proxy_host, displayedMixedPort, systemProxyAddress])
|
|
const getCurrentPacUrl = useMemo(() => {
|
|
const host = value.proxy_host || '127.0.0.1'
|
|
return embeddedServerPort
|
|
? `http://${host}:${embeddedServerPort}/commands/pac`
|
|
: '-'
|
|
}, [embeddedServerPort, value.proxy_host])
|
|
|
|
const bypassError =
|
|
value.enable_bypass_check &&
|
|
!value.pac &&
|
|
!value.use_default &&
|
|
value.bypass
|
|
? !validReg.test(value.bypass)
|
|
: false
|
|
|
|
const openPacEditor = () => {
|
|
const nextPac = value.pac_content ?? DEFAULT_PAC
|
|
setPacEditorValue(nextPac)
|
|
setPacEditorSavedValue(nextPac)
|
|
setEditorOpen(true)
|
|
}
|
|
|
|
const handleSavePac = useLockFn(async () => {
|
|
const nextPac =
|
|
pacEditorValue.trim().length > 0 ? pacEditorValue : DEFAULT_PAC
|
|
|
|
setValue((current) => ({ ...current, pac_content: nextPac }))
|
|
setPacEditorSavedValue(nextPac)
|
|
})
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
open: () => {
|
|
setOpen(true)
|
|
setValue({
|
|
guard: enable_proxy_guard,
|
|
enable_bypass_check: enable_bypass_check ?? true,
|
|
bypass: system_proxy_bypass,
|
|
duration: proxy_guard_duration ?? 10,
|
|
use_default: use_default_bypass ?? true,
|
|
pac: proxy_auto_config,
|
|
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
proxy_host: proxy_host ?? '127.0.0.1',
|
|
})
|
|
fetchNetworkInterfaces()
|
|
},
|
|
close: () => setOpen(false),
|
|
}))
|
|
|
|
const fetchNetworkInterfaces = async () => {
|
|
try {
|
|
const interfaces = await getNetworkInterfacesInfo()
|
|
const ipAddresses: string[] = []
|
|
|
|
interfaces.forEach((iface) => {
|
|
iface.addr.forEach((address) => {
|
|
if (address.V4?.ip) {
|
|
ipAddresses.push(address.V4.ip)
|
|
}
|
|
if (address.V6?.ip) {
|
|
ipAddresses.push(address.V6.ip)
|
|
}
|
|
})
|
|
})
|
|
|
|
let hostname = ''
|
|
try {
|
|
hostname = await getSystemHostname()
|
|
debugLog('获取到主机名:', hostname)
|
|
} catch (err) {
|
|
console.error('获取主机名失败:', err)
|
|
}
|
|
|
|
const options = ['127.0.0.1', 'localhost']
|
|
|
|
if (hostname) {
|
|
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
|
hostname = `${hostname}.local`
|
|
options.push(hostname)
|
|
debugLog('主机名已添加到选项中:', hostname)
|
|
} else {
|
|
debugLog('主机名与已有选项重复:', hostname)
|
|
}
|
|
} else {
|
|
debugLog('主机名为空')
|
|
}
|
|
|
|
options.push(...ipAddresses)
|
|
|
|
const uniqueOptions = Array.from(new Set(options))
|
|
debugLog('最终选项列表:', uniqueOptions)
|
|
setHostOptions(uniqueOptions)
|
|
} catch (error) {
|
|
console.error('获取网络接口失败:', error)
|
|
setHostOptions(['127.0.0.1', 'localhost'])
|
|
}
|
|
}
|
|
|
|
const onSave = useLockFn(async () => {
|
|
if (value.duration < 1) {
|
|
showNotice.error('settings.modals.sysproxy.messages.durationTooShort')
|
|
return
|
|
}
|
|
if (
|
|
value.enable_bypass_check &&
|
|
!value.pac &&
|
|
!value.use_default &&
|
|
value.bypass &&
|
|
!validReg.test(value.bypass)
|
|
) {
|
|
showNotice.error('settings.modals.sysproxy.messages.invalidBypass')
|
|
return
|
|
}
|
|
|
|
const ipv4Regex =
|
|
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
const ipv6Regex =
|
|
/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/
|
|
const hostnameRegex =
|
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
|
|
|
|
if (
|
|
!ipv4Regex.test(value.proxy_host) &&
|
|
!ipv6Regex.test(value.proxy_host) &&
|
|
!hostnameRegex.test(value.proxy_host)
|
|
) {
|
|
showNotice.error('settings.modals.sysproxy.messages.invalidProxyHost')
|
|
return
|
|
}
|
|
|
|
setSaving(true)
|
|
setOpen(false)
|
|
setSaving(false)
|
|
const patch: Partial<IVergeConfig> = {}
|
|
|
|
if (value.guard !== enable_proxy_guard) {
|
|
patch.enable_proxy_guard = value.guard
|
|
}
|
|
if (value.enable_bypass_check !== enable_bypass_check) {
|
|
patch.enable_bypass_check = value.enable_bypass_check
|
|
}
|
|
if (value.duration !== proxy_guard_duration) {
|
|
patch.proxy_guard_duration = value.duration
|
|
}
|
|
if (value.bypass !== system_proxy_bypass) {
|
|
patch.system_proxy_bypass = value.bypass
|
|
}
|
|
if (value.pac !== proxy_auto_config) {
|
|
patch.proxy_auto_config = value.pac
|
|
}
|
|
if (value.use_default !== use_default_bypass) {
|
|
patch.use_default_bypass = value.use_default
|
|
}
|
|
|
|
let pacContent = value.pac_content
|
|
if (pacContent) {
|
|
pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host)
|
|
const mixedPortStr = displayedMixedPort.toString()
|
|
pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr)
|
|
}
|
|
|
|
if (pacContent !== pac_file_content) {
|
|
patch.pac_file_content = pacContent
|
|
}
|
|
|
|
let proxyHost = value.proxy_host
|
|
if (
|
|
ipv6Regex.test(proxyHost) &&
|
|
!proxyHost.startsWith('[') &&
|
|
!proxyHost.endsWith(']')
|
|
) {
|
|
proxyHost = `[${proxyHost}]`
|
|
}
|
|
|
|
if (proxyHost !== proxy_host) {
|
|
patch.proxy_host = proxyHost
|
|
}
|
|
|
|
Promise.resolve().then(async () => {
|
|
try {
|
|
if (Object.keys(patch).length > 0) {
|
|
mutateVerge({ ...verge, ...patch }, false)
|
|
}
|
|
if (Object.keys(patch).length > 0) {
|
|
await patchVerge(patch)
|
|
}
|
|
try {
|
|
await invalidateProxyState()
|
|
} catch (err) {
|
|
console.warn('代理状态更新失败:', err)
|
|
}
|
|
} catch (err) {
|
|
console.error('配置保存失败:', err)
|
|
mutateVerge()
|
|
showNotice.error(err)
|
|
}
|
|
})
|
|
})
|
|
|
|
return (
|
|
<BaseDialog
|
|
open={open}
|
|
title={t('settings.modals.sysproxy.title')}
|
|
contentSx={{ width: 450, maxHeight: 565 }}
|
|
okBtn={t('shared.actions.save')}
|
|
cancelBtn={t('shared.actions.cancel')}
|
|
onClose={() => setOpen(false)}
|
|
onCancel={() => setOpen(false)}
|
|
onOk={onSave}
|
|
loading={saving}
|
|
disableOk={saving}
|
|
>
|
|
<List>
|
|
<BaseFieldset
|
|
label={t('settings.modals.sysproxy.fieldsets.currentStatus')}
|
|
padding="15px 10px"
|
|
>
|
|
<FlexBox>
|
|
<Typography className="label">
|
|
{t('settings.modals.sysproxy.fields.enableStatus')}
|
|
</Typography>
|
|
<Typography className="value">
|
|
{isProxyReallyEnabled
|
|
? t('shared.statuses.enabled')
|
|
: t('shared.statuses.disabled')}
|
|
</Typography>
|
|
</FlexBox>
|
|
{!value.pac && (
|
|
<FlexBox>
|
|
<Typography className="label">
|
|
{t('settings.modals.sysproxy.fields.serverAddr')}
|
|
</Typography>
|
|
<Typography className="value">{getSystemProxyAddress}</Typography>
|
|
</FlexBox>
|
|
)}
|
|
{value.pac && (
|
|
<FlexBox>
|
|
<Typography className="label">
|
|
{t('settings.modals.sysproxy.fields.pacUrl')}
|
|
</Typography>
|
|
<Typography className="value">
|
|
{getCurrentPacUrl || '-'}
|
|
</Typography>
|
|
</FlexBox>
|
|
)}
|
|
</BaseFieldset>
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.proxyHost')}
|
|
/>
|
|
<Autocomplete
|
|
size="small"
|
|
sx={{ width: 150 }}
|
|
options={hostOptions}
|
|
value={value.proxy_host}
|
|
freeSolo
|
|
renderInput={(params) => (
|
|
<TextField {...params} placeholder="127.0.0.1" size="small" />
|
|
)}
|
|
onChange={(_, newValue) => {
|
|
setValue((v) => ({
|
|
...v,
|
|
proxy_host: newValue || '127.0.0.1',
|
|
}))
|
|
}}
|
|
onInputChange={(_, newInputValue) => {
|
|
setValue((v) => ({
|
|
...v,
|
|
proxy_host: newInputValue || '127.0.0.1',
|
|
}))
|
|
}}
|
|
/>
|
|
</ListItem>
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.usePacMode')}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
disabled={!enabled}
|
|
checked={value.pac}
|
|
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.proxyGuard')}
|
|
sx={{ maxWidth: 'fit-content' }}
|
|
/>
|
|
<TooltipIcon
|
|
title={t('settings.modals.sysproxy.tooltips.proxyGuard')}
|
|
sx={{ opacity: '0.7' }}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
disabled={!enabled}
|
|
checked={value.guard}
|
|
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
|
|
sx={{ marginLeft: 'auto' }}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.guardDuration')}
|
|
/>
|
|
<TextField
|
|
disabled={!enabled}
|
|
size="small"
|
|
value={value.duration}
|
|
sx={{ width: 100 }}
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: <InputAdornment position="end">s</InputAdornment>,
|
|
},
|
|
}}
|
|
onChange={(e) => {
|
|
setValue((v) => ({
|
|
...v,
|
|
duration: +e.target.value.replace(/\D/, ''),
|
|
}))
|
|
}}
|
|
/>
|
|
</ListItem>
|
|
{!value.pac && (
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t(
|
|
'settings.modals.sysproxy.fields.alwaysUseDefaultBypass',
|
|
)}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
disabled={!enabled}
|
|
checked={value.use_default}
|
|
onChange={(_, e) => {
|
|
if (!e && !value.bypass) {
|
|
const nextBypass = defaultBypass()
|
|
setValue((v) => ({
|
|
...v,
|
|
use_default: e,
|
|
bypass: nextBypass,
|
|
}))
|
|
return
|
|
}
|
|
setValue((v) => ({ ...v, use_default: e }))
|
|
}}
|
|
/>
|
|
</ListItem>
|
|
)}
|
|
|
|
{!value.pac && (
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.enableBypassCheck')}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
disabled={!enabled}
|
|
checked={value.enable_bypass_check}
|
|
onChange={(_, e) =>
|
|
setValue((v) => ({ ...v, enable_bypass_check: e }))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
)}
|
|
|
|
{!value.pac && !value.use_default && (
|
|
<BaseSplitChipEditor
|
|
value={value.bypass ?? ''}
|
|
separator={separator}
|
|
disabled={!enabled}
|
|
error={bypassError}
|
|
helperText={
|
|
bypassError
|
|
? t('settings.modals.sysproxy.messages.invalidBypass')
|
|
: undefined
|
|
}
|
|
placeholder="localhost"
|
|
ariaLabel={t('settings.modals.sysproxy.fields.proxyBypass')}
|
|
onChange={(nextValue) => {
|
|
setValue((v) => ({ ...v, bypass: nextValue }))
|
|
}}
|
|
renderHeader={(modeToggle) => (
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.proxyBypass')}
|
|
/>
|
|
{modeToggle ? (
|
|
<Box sx={{ marginLeft: 'auto' }}>{modeToggle}</Box>
|
|
) : null}
|
|
</ListItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{!value.pac && value.use_default && (
|
|
<>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.bypass')}
|
|
/>
|
|
<Box sx={{ padding: '0 2px 5px' }}>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|
{splitBypass(defaultBypass()).map((item) => (
|
|
<Chip key={item} label={item} size="small" />
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
</>
|
|
)}
|
|
|
|
{value.pac && (
|
|
<ListItem sx={{ padding: '5px 2px', alignItems: 'start' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.sysproxy.fields.pacScriptContent')}
|
|
sx={{ padding: '3px 0' }}
|
|
/>
|
|
<Button
|
|
startIcon={<EditRounded />}
|
|
variant="outlined"
|
|
onClick={openPacEditor}
|
|
>
|
|
{t('settings.modals.sysproxy.actions.editPac')}
|
|
</Button>
|
|
{editorOpen && (
|
|
<EditorViewer
|
|
open={true}
|
|
title={t('settings.modals.sysproxy.actions.editPac')}
|
|
value={pacEditorValue}
|
|
language="javascript"
|
|
path="sysproxy-pac.js"
|
|
dirty={pacEditorValue !== pacEditorSavedValue}
|
|
onChange={setPacEditorValue}
|
|
onSave={handleSavePac}
|
|
onClose={() => setEditorOpen(false)}
|
|
/>
|
|
)}
|
|
</ListItem>
|
|
)}
|
|
</List>
|
|
</BaseDialog>
|
|
)
|
|
})
|
|
|
|
const FlexBox = styled('div')`
|
|
display: flex;
|
|
margin-top: 4px;
|
|
|
|
.label {
|
|
flex: none;
|
|
}
|
|
`
|