import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd'; import { BarChartOutlined, ClockCircleOutlined, FileTextOutlined, ReloadOutlined, SettingOutlined, } from '@ant-design/icons'; import { OutboundDomainStrategies } from '@/schemas/primitives'; import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt'; import { SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { catTabLabel } from '@/pages/settings/catTabLabel'; import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting'; import './BasicsTab.css'; import { ACCESS_LOG, ERROR_LOG, LOG_LEVELS, MASK_ADDRESS, ROUTING_DOMAIN_STRATEGIES, } from './constants'; interface BasicsTabProps { templateSettings: XraySettingsValue | null; setTemplateSettings: SetTemplate; outboundTestUrl: string; onChangeOutboundTestUrl: (v: string) => void; onResetDefault: () => void; } export default function BasicsTab({ templateSettings, setTemplateSettings, outboundTestUrl, onChangeOutboundTestUrl, onResetDefault, }: BasicsTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const [modal, modalContextHolder] = Modal.useModal(); const mutate = useCallback( (mutator: (next: XraySettingsValue) => void) => { setTemplateSettings((prev) => { if (!prev) return prev; const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue; mutator(clone); return clone; }); }, [setTemplateSettings], ); const setLevel0 = useCallback( (field: string, value: number | null) => mutate((tt) => { if (!tt.policy) tt.policy = {}; if (!tt.policy.levels) tt.policy.levels = {}; if (!tt.policy.levels['0']) tt.policy.levels['0'] = {}; if (value === null || value === undefined) { delete tt.policy.levels['0'][field]; } else { tt.policy.levels['0'][field] = value; } }), [mutate], ); const metricsCfg = (templateSettings as { metrics?: { tag?: string; listen?: string } } | null)?.metrics; const setMetrics = useCallback( (field: 'tag' | 'listen', value: string) => mutate((tt) => { const node = tt as { metrics?: { tag?: string; listen?: string }; stats?: Record }; const m: { tag?: string; listen?: string } = { ...(node.metrics ?? {}) }; if (value.trim() === '') { delete m[field]; } else { m[field] = value.trim(); } if (!m.listen && !m.tag) { delete node.metrics; } else { node.metrics = m; // xray-core's metrics handler needs a stats object to populate. if (!node.stats) node.stats = {}; } }), [mutate], ); function confirmResetDefault() { modal.confirm({ title: t('pages.settings.resetDefaultConfig'), okText: t('reset'), okType: 'danger', cancelText: t('cancel'), onOk: () => onResetDefault(), }); } const freedomStrategy = (templateSettings?.outbounds?.find((o) => o?.protocol === 'freedom' && o?.tag === 'direct')?.settings as | { domainStrategy?: string } | undefined)?.domainStrategy ?? 'AsIs'; const directFreedomOutbound = templateSettings?.outbounds?.find( (o) => o?.protocol === 'freedom' && o?.tag === 'direct', ); const directHappyEyeballs = (() => { const sockopt = (directFreedomOutbound?.streamSettings as { sockopt?: { happyEyeballs?: unknown } } | undefined) ?.sockopt; const raw = sockopt?.happyEyeballs; if (raw == null || typeof raw !== 'object') return null; return HappyEyeballsSchema.parse(raw); })(); const setDirectHappyEyeballs = useCallback( (next: ReturnType | null) => { mutate((tt) => { if (!tt.outbounds) tt.outbounds = []; let idx = tt.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct'); if (idx < 0) { tt.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: {} }); idx = tt.outbounds.length - 1; } const ob = tt.outbounds[idx]; const stream = (ob.streamSettings ?? {}) as Record; const sockopt = (stream.sockopt ?? {}) as Record; if (next == null) { delete sockopt.happyEyeballs; } else { sockopt.happyEyeballs = next; } if (Object.keys(sockopt).length === 0) { delete stream.sockopt; } else { stream.sockopt = sockopt; } if (Object.keys(stream).length === 0) { delete ob.streamSettings; } else { ob.streamSettings = stream; } }); }, [mutate], ); const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs'; const log = (templateSettings?.log || {}) as Record; const policy = (templateSettings?.policy?.system || {}) as Record; const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record; const items = [ { key: '1', label: catTabLabel(, t('pages.xray.generalConfigs'), isMobile), children: ( <> ({ value: s, label: s }))} onChange={(next) => mutate((tt) => { if (!tt.outbounds) tt.outbounds = []; const idx = tt.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct'); if (idx < 0) { tt.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: { domainStrategy: next } }); } else { const ob = tt.outbounds[idx]; ob.settings = (ob.settings || {}) as Record; (ob.settings as Record).domainStrategy = next; } })} /> } /> { setDirectHappyEyeballs(checked ? HappyEyeballsSchema.parse({}) : null); }} /> } /> {directHappyEyeballs != null && ( <> setDirectHappyEyeballs({ ...directHappyEyeballs, tryDelayMs: typeof v === 'number' ? v : 0, })} /> } /> setDirectHappyEyeballs({ ...directHappyEyeballs, prioritizeIPv6: checked, })} /> } /> )} ({ value: s, label: s }))} onChange={(next) => mutate((tt) => { if (tt.routing) tt.routing.domainStrategy = next; })} /> } /> onChangeOutboundTestUrl(e.target.value)} placeholder="https://www.google.com/generate_204" /> } /> ), }, { key: '2', label: catTabLabel(, t('pages.xray.statistics'), isMobile), children: ( <> {[ ['statsInboundUplink', t('pages.xray.statsInboundUplink')], ['statsInboundDownlink', t('pages.xray.statsInboundDownlink')], ['statsOutboundUplink', t('pages.xray.statsOutboundUplink')], ['statsOutboundDownlink', t('pages.xray.statsOutboundDownlink')], ].map(([field, label]) => ( mutate((tt) => { if (!tt.policy) tt.policy = {}; if (!tt.policy.system) tt.policy.system = {}; tt.policy.system[field] = checked; })} /> } /> ))} setMetrics('listen', e.target.value)} placeholder="127.0.0.1:11111" /> } /> setMetrics('tag', e.target.value)} placeholder="metrics_out" /> } /> ), }, { key: 'connection', label: catTabLabel(, t('pages.xray.connectionLimits'), isMobile), children: ( <> setLevel0('connIdle', v as number | null)} /> } /> setLevel0('bufferSize', v as number | null)} /> } /> ), }, { key: '3', label: catTabLabel(, t('pages.xray.logConfigs'), isMobile), children: ( <> ({ value: s, label: s }))} onChange={(v) => mutate((tt) => { if (tt.log) tt.log.loglevel = v; })} /> } /> ({ value: s, label: s }))} onChange={(v) => mutate((tt) => { if (tt.log) tt.log.access = v; })} /> } /> ({ value: s, label: s }))]} onChange={(v) => mutate((tt) => { if (tt.log) tt.log.error = v; })} /> } /> ({ value: s, label: s }))]} onChange={(v) => mutate((tt) => { if (tt.log) tt.log.maskAddress = v; })} /> } /> mutate((tt) => { if (tt.log) tt.log.dnsLog = v; })} /> } /> ), }, { key: 'reset', label: catTabLabel(, t('pages.settings.resetDefaultConfig'), isMobile), children: ( ), }, ]; return ( <> {modalContextHolder} ); }