import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { Alert, Button, Card, Col, ConfigProvider, FloatButton, Layout, message, Modal, Popover, Radio, Result, Row, Space, Spin, } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useXraySetting } from '@/hooks/useXraySetting'; import type { XraySettingsValue } from '@/hooks/useXraySetting'; import AppSidebar from '@/layouts/AppSidebar'; import { JsonEditor } from '@/components/form'; import { setMessageInstance } from '@/utils/messageBus'; import { BasicsTab } from './basics'; import { propagateOutboundTagRename } from './basics/helpers'; import { RoutingTab } from './routing'; import { OutboundsTab } from './outbounds'; import { BalancersTab } from './balancers'; import { DnsTab } from './dns'; import { WarpModal, NordModal } from './overrides'; import './XrayPage.css'; const SECTION_SLUGS = ['basic', 'routing', 'outbound', 'balancer', 'dns', 'advanced']; type AdvKey = 'xraySetting' | 'inboundSettings' | 'outboundSettings' | 'routingRuleSettings'; export default function XrayPage() { const { t } = useTranslation(); const { isDark, isUltra, antdThemeConfig } = useTheme(); const { isMobile } = useMediaQuery(); const [messageApi, messageContextHolder] = message.useMessage(); useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); const xs = useXraySetting(); const { fetched, spinning, saveDisabled, fetchError, xraySetting, setXraySetting, templateSettings, setTemplateSettings, outboundTestUrl, setOutboundTestUrl, inboundTags, clientReverseTags, subscriptionOutbounds, subscriptionOutboundTags, restartResult, outboundsTraffic, outboundTestStates, subscriptionTestStates, testingAll, fetchAll, resetOutboundsTraffic, testOutbound, testSubscriptionOutbound, testAllOutbounds, saveAll, resetToDefault, restartXray, } = xs; const [modal, modalContextHolder] = Modal.useModal(); const [warpOpen, setWarpOpen] = useState(false); const [nordOpen, setNordOpen] = useState(false); const [advSettings, setAdvSettings] = useState('xraySetting'); const location = useLocation(); const navigate = useNavigate(); const sectionSlug = location.hash.replace(/^#/, ''); const activeSection = SECTION_SLUGS.includes(sectionSlug) ? sectionSlug : 'basic'; 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], ); async function onTestOutbound(idx: number, mode: string) { const outbound = templateSettings?.outbounds?.[idx]; if (outbound) await testOutbound(idx, outbound, mode); } async function onTestSubscription(outbound: Record, mode: string) { const tag = typeof outbound?.tag === 'string' ? outbound.tag : ''; if (tag) await testSubscriptionOutbound(tag, outbound, mode); } function onAddOutbound(outbound: Record) { mutate((tt) => { if (!Array.isArray(tt.outbounds)) tt.outbounds = []; tt.outbounds.push(outbound as never); }); } function onResetOutbound(payload: { index: number; outbound: Record; oldTag?: string; newTag?: string }) { mutate((tt) => { if (!tt.outbounds || payload.index < 0) return; tt.outbounds[payload.index] = payload.outbound as never; if (payload.oldTag && payload.newTag) { propagateOutboundTagRename(tt, payload.oldTag, payload.newTag); } }); } function onRemoveOutboundByTag(tag: string) { mutate((tt) => { if (!tt.outbounds) return; const idx = tt.outbounds.findIndex((o) => o?.tag === tag); if (idx >= 0) tt.outbounds.splice(idx, 1); }); } function onRemoveOutboundByIndex(index: number) { mutate((tt) => { if (tt.outbounds && index >= 0) tt.outbounds.splice(index, 1); }); } function onRemoveRoutingRules(payload: { prefix: string }) { mutate((tt) => { const rules = tt.routing?.rules; if (!Array.isArray(rules)) return; tt.routing!.rules = rules.filter((r) => !r?.outboundTag?.startsWith?.(payload.prefix)); }); } const advancedText = useMemo(() => { if (advSettings === 'xraySetting') return xraySetting; const tpl = templateSettings; if (!tpl) return ''; try { switch (advSettings) { case 'inboundSettings': return JSON.stringify(tpl.inbounds || [], null, 2); case 'outboundSettings': return JSON.stringify(tpl.outbounds || [], null, 2); case 'routingRuleSettings': return JSON.stringify(tpl.routing?.rules || [], null, 2); default: return ''; } } catch { return ''; } }, [advSettings, xraySetting, templateSettings]); function onAdvancedTextChange(next: string) { if (advSettings === 'xraySetting') { setXraySetting(next); return; } let parsed; try { parsed = JSON.parse(next); } catch { return; } mutate((tt) => { switch (advSettings) { case 'inboundSettings': tt.inbounds = parsed; break; case 'outboundSettings': tt.outbounds = parsed; break; case 'routingRuleSettings': if (!tt.routing) tt.routing = {}; tt.routing.rules = parsed; break; } }); } function confirmRestart() { modal.confirm({ title: t('pages.xray.restartConfirmTitle'), content: t('pages.xray.restartConfirmContent'), okText: t('pages.xray.restart'), cancelText: t('cancel'), onOk: () => restartXray(), }); } function onSaveAll() { try { JSON.parse(xraySetting); } catch (e) { messageApi.error(`Advanced JSON: ${(e as Error).message}`); navigate('/xray#advanced'); return; } saveAll(); } const scrollTarget = () => document.getElementById('content-layout') || window; const pageClass = `xray-page ${isDark ? 'is-dark' : ''} ${isUltra ? 'is-ultra' : ''}`.trim(); const sectionBody = (() => { switch (activeSection) { case 'routing': return ( ); case 'outbound': return ( setWarpOpen(true)} onShowNord={() => setNordOpen(true)} onRefreshXrayData={fetchAll} /> ); case 'balancer': return ( ); case 'dns': return ( ); case 'advanced': return ( <>

{t('pages.xray.Template')}

{t('pages.xray.TemplateDesc')}

setAdvSettings(e.target.value)} > {t('pages.xray.completeTemplate')} {t('pages.xray.Inbounds')} {t('pages.xray.Outbounds')} {t('pages.xray.Routings')} ); default: return ( ); } })(); return ( {messageContextHolder} {modalContextHolder} {!fetched ? (
) : fetchError ? ( {t('check')}} /> ) : ( {restartResult && ( {restartResult}} > )} {sectionBody} )} setWarpOpen(false)} onAddOutbound={onAddOutbound} onResetOutbound={onResetOutbound} onRemoveOutbound={onRemoveOutboundByTag} /> setNordOpen(false)} onAddOutbound={onAddOutbound} onResetOutbound={onResetOutbound} onRemoveOutbound={onRemoveOutboundByIndex} onRemoveRoutingRules={onRemoveRoutingRules} /> ); }