import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, InputNumber, message, Modal, Radio, Select, Space, Switch, Tabs, Checkbox, } from 'antd'; import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined } from '@ant-design/icons'; import { Wireguard } from '@/utils'; import InputAddon from '@/components/InputAddon'; import { Outbound, Protocols, SSMethods, TLS_FLOW_CONTROL, UTLS_FINGERPRINT, ALPN_OPTION, SNIFFING_OPTION, USERS_SECURITY, OutboundDomainStrategies, WireguardDomainStrategy, Address_Port_Strategy, MODE_OPTION, DNSRuleActions, } from '@/models/outbound'; import FinalMaskForm from '@/components/FinalMaskForm'; import JsonEditor from '@/components/JsonEditor'; import './OutboundFormModal.css'; interface OutboundFormModalProps { open: boolean; outbound: Record | null; existingTags: string[]; onClose: () => void; onConfirm: (outbound: Record) => void; } const PROTOCOL_OPTIONS = Object.values(Protocols) as string[]; const SECURITY_OPTIONS = Object.values(USERS_SECURITY) as string[]; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL) as string[]; const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT) as string[]; const ALPN_OPTIONS = Object.values(ALPN_OPTION) as string[]; const NETWORKS = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']; const NETWORK_LABELS: Record = { tcp: 'TCP (RAW)', kcp: 'mKCP', ws: 'WebSocket', grpc: 'gRPC', httpupgrade: 'HTTPUpgrade', xhttp: 'XHTTP', }; export default function OutboundFormModal({ open, outbound: outboundProp, existingTags, onClose, onConfirm, }: OutboundFormModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const outboundRef = useRef(null); const [, setTick] = useState(0); const [activeKey, setActiveKey] = useState('1'); const [linkInput, setLinkInput] = useState(''); const [advancedJson, setAdvancedJson] = useState(''); const revertingTabRef = useRef(false); const isEdit = outboundProp != null; const refresh = useCallback(() => setTick((n) => n + 1), []); const primeAdvancedJson = useCallback(() => { const ob = outboundRef.current; if (!ob) { setAdvancedJson(''); return; } try { setAdvancedJson(JSON.stringify(ob.toJson(), null, 2)); } catch { setAdvancedJson(''); } }, []); useEffect(() => { if (!open) return; outboundRef.current = outboundProp ? Outbound.fromJson(outboundProp) : new Outbound(); setActiveKey('1'); setLinkInput(''); primeAdvancedJson(); refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, outboundProp]); function applyAdvancedJsonToForm(): boolean { const raw = advancedJson.trim(); if (!raw) return true; const ob = outboundRef.current; let currentJson = ''; try { currentJson = JSON.stringify(ob?.toJson() ?? {}, null, 2); } catch { /* ignore */ } if (raw === currentJson.trim()) return true; let parsed; try { parsed = JSON.parse(raw); } catch (e) { messageApi.error(`JSON: ${(e as Error).message}`); return false; } try { const fallbackTag = ob?.tag; const next = Outbound.fromJson(parsed); if (!next.tag && fallbackTag) next.tag = fallbackTag; outboundRef.current = next; refresh(); return true; } catch (e) { messageApi.error(`JSON: ${(e as Error).message}`); return false; } } function onTabChange(key: string) { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } if (revertingTabRef.current) { revertingTabRef.current = false; setActiveKey(key); return; } const prev = activeKey; if (key === '2') { primeAdvancedJson(); setActiveKey(key); } else if (key === '1' && prev === '2') { if (!applyAdvancedJsonToForm()) { revertingTabRef.current = true; setActiveKey('2'); } else { setActiveKey(key); } } else { setActiveKey(key); } } const ob = outboundRef.current; const proto = ob?.protocol; const isVMess = proto === Protocols.VMess; const isVLESS = proto === Protocols.VLESS; const isVMessOrVLess = isVMess || isVLESS; const isTrojan = proto === Protocols.Trojan; const isShadowsocks = proto === Protocols.Shadowsocks; const isFreedom = proto === Protocols.Freedom; const isBlackhole = proto === Protocols.Blackhole; const isDNS = proto === Protocols.DNS; const isWireguard = proto === Protocols.Wireguard; const isHysteria = proto === Protocols.Hysteria; const isLoopback = proto === Protocols.Loopback; function onProtocolChange(next: string) { if (!ob) return; ob.protocol = next; refresh(); } function streamNetworkChange(next: string) { if (!ob?.stream) return; ob.stream.network = next; if (!ob.canEnableTls()) ob.stream.security = 'none'; refresh(); } function regenerateWgKeys() { if (!ob?.settings) return; const pair = Wireguard.generateKeypair(); ob.settings.secretKey = pair.privateKey; ob.settings.pubKey = pair.publicKey; refresh(); } const duplicateTag = useMemo(() => { if (!ob?.tag) return false; const myTag = ob.tag.trim(); if (!myTag) return false; if (isEdit && (outboundProp?.tag as string | undefined) === myTag) return false; return (existingTags || []).includes(myTag); }, [ob?.tag, existingTags, isEdit, outboundProp]); const tagEmpty = !ob?.tag?.trim(); const tagValidateStatus: 'error' | 'warning' | 'success' = tagEmpty ? 'error' : duplicateTag ? 'warning' : 'success'; const tagHelp = tagEmpty ? 'Tag is required' : duplicateTag ? 'Tag already used by another outbound' : ''; function onOk() { if (!ob) return; if (activeKey === '2' && !applyAdvancedJsonToForm()) return; if (!ob.tag?.trim()) { messageApi.error('Tag is required'); return; } if (duplicateTag) { messageApi.error('Tag already used by another outbound'); return; } onConfirm(ob.toJson()); } function convertLink() { const link = linkInput.trim(); if (!link) return; try { const next = Outbound.fromLink(link); if (!next) { messageApi.error('Wrong Link!'); return; } outboundRef.current = next; primeAdvancedJson(); setLinkInput(''); messageApi.success('Link imported successfully...'); setActiveKey('1'); refresh(); } catch (e) { messageApi.error(`Link parse: ${(e as Error).message}`); } } const title = isEdit ? `${t('edit')} ${t('pages.xray.Outbounds')}` : `+ ${t('pages.xray.Outbounds')}`; const okText = isEdit ? t('pages.clients.submitEdit') : t('create'); if (!ob) { return ( <> {messageContextHolder} ); } return ( <> {messageContextHolder}
{ ob.tag = e.target.value; refresh(); }} /> { ob.sendThrough = e.target.value; refresh(); }} /> {isFreedom && } {isBlackhole && ( { ob.settings.inboundTag = e.target.value; refresh(); }} /> )} {isDNS && } {isWireguard && } {ob.hasAddressPort() && ( <> { ob.settings.address = e.target.value; refresh(); }} /> { ob.settings.port = Number(v) || 0; refresh(); }} /> )} {isVMessOrVLess && ( )} {(isTrojan || isShadowsocks) && ( { ob.settings.password = e.target.value; refresh(); }} /> )} {isShadowsocks && ( <> { ob.settings.user = e.target.value; refresh(); }} /> { ob.settings.pass = e.target.value; refresh(); }} /> )} {isHysteria && ( )} {ob.canEnableStream() && ( )} {ob.canEnableTls() && } {ob.stream && } {ob.canEnableMux() && } {ob.stream && ob.canEnableStream() && ( )} ), }, { key: '2', label: 'JSON', children: ( setLinkInput(e.target.value)} onSearch={convertLink} /> ), }, ]} />
); } type OB = Outbound; interface FieldProps { ob: OB; refresh: () => void; } interface TFieldProps extends FieldProps { t: (k: string) => string; } function FreedomFields({ ob, refresh }: FieldProps) { const fragment = (ob.settings.fragment || {}) as Record; const noises = (ob.settings.noises || []) as Array<{ type: string; packet: string; delay: string; applyTo: string }>; const finalRules = (ob.settings.finalRules || []) as Array<{ action: string; network?: string; port?: string; ip?: string[]; blockDelay?: string }>; return ( <> { ob.settings.redirect = e.target.value; refresh(); }} /> 0} onChange={(checked) => { ob.settings.fragment = checked ? { packets: 'tlshello', length: '100-200', interval: '10-20', maxSplit: '300-400' } : {}; refresh(); }} /> {ob.settings.fragment && Object.keys(ob.settings.fragment).length > 0 && ( <> { (ob.settings.fragment as Record)[field] = e.target.value; refresh(); }} /> ))} )} 0} onChange={(checked) => { ob.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []; refresh(); }} /> {noises.length > 0 && (