import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, Input, InputNumber, Select, Switch, Tabs } from 'antd'; import { FileTextOutlined, NodeIndexOutlined, PartitionOutlined, RocketOutlined, SendOutlined, SettingOutlined, StopOutlined, } from '@ant-design/icons'; import type { AllSetting } from '@/models/setting'; import { onNumber } from '@/utils/onNumber'; import { SettingListItem } from '@/components/ui'; import { GoRegexInput } from '@/components/form'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { catTabLabel } from './catTabLabel'; import { sanitizePath, normalizePath } from './uriPath'; import { remoteSourceBadge } from './subscriptionShared'; import SubJsonFinalMaskForm from './SubJsonFinalMaskForm'; import './SubscriptionFormatsTab.css'; interface SubscriptionFormatsTabProps { allSetting: AllSetting; updateSetting: (patch: Partial) => void; } const DEFAULT_MUX = { enabled: true, concurrency: 8, xudpConcurrency: 16, xudpProxyUDP443: 'reject', }; type SubJsonRule = { type: string; outboundTag: string; domain?: string[]; ip?: string[] }; const DEFAULT_DIRECT_RULES: SubJsonRule[] = [ { type: 'field', outboundTag: 'direct', domain: ['geosite:category-ir'] }, { type: 'field', outboundTag: 'direct', ip: ['geoip:private', 'geoip:ir'] }, ]; const DEFAULT_BLOCK_RULES: SubJsonRule[] = [ { type: 'field', outboundTag: 'block', domain: ['geosite:category-ads-all'] }, ]; const BLOCK_IP_RULE: SubJsonRule = { type: 'field', outboundTag: 'block', ip: [] }; const directIPsOptions = [ { label: 'Private IP', value: 'geoip:private' }, { label: '🇮🇷 Iran', value: 'geoip:ir' }, { label: '🇨🇳 China', value: 'geoip:cn' }, { label: '🇷🇺 Russia', value: 'geoip:ru' }, { label: '🇻🇳 Vietnam', value: 'geoip:vn' }, { label: '🇪🇸 Spain', value: 'geoip:es' }, { label: '🇮🇩 Indonesia', value: 'geoip:id' }, { label: '🇺🇦 Ukraine', value: 'geoip:ua' }, { label: '🇹🇷 Türkiye', value: 'geoip:tr' }, { label: '🇧🇷 Brazil', value: 'geoip:br' }, ]; const directDomainsOptions = [ { label: 'Private DNS', value: 'geosite:private' }, { label: '🇮🇷 Iran', value: 'geosite:category-ir' }, { label: '🇨🇳 China', value: 'geosite:cn' }, { label: '🇷🇺 Russia', value: 'geosite:category-ru' }, { label: 'Apple', value: 'geosite:apple' }, { label: 'Meta', value: 'geosite:meta' }, { label: 'Google', value: 'geosite:google' }, ]; const blockDomainsOptions = [ { label: 'Ads All', value: 'geosite:category-ads-all' }, { label: 'Adult +18', value: 'geosite:category-porn' }, ]; function readJson(raw: string, fallback: T): T { try { if (!raw) return fallback; return JSON.parse(raw) as T; } catch { return fallback; } } function readRules(raw: string): SubJsonRule[] { const parsed = readJson(raw, null); return Array.isArray(parsed) ? (parsed as SubJsonRule[]) : []; } export default function SubscriptionFormatsTab({ allSetting, updateSetting, }: SubscriptionFormatsTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const muxEnabled = allSetting.subJsonMux !== ''; const muxObj = useMemo( () => muxEnabled ? readJson(allSetting.subJsonMux, DEFAULT_MUX) : DEFAULT_MUX, [allSetting.subJsonMux, muxEnabled], ); function setMuxEnabled(v: boolean) { updateSetting({ subJsonMux: v ? JSON.stringify(DEFAULT_MUX) : '' }); } function setMuxField(key: K, value: (typeof DEFAULT_MUX)[K]) { const next = { ...muxObj, [key]: value }; updateSetting({ subJsonMux: JSON.stringify(next) }); } const ruleArray = useMemo(() => readRules(allSetting.subJsonRules), [allSetting.subJsonRules]); const directEnabled = ruleArray.some((r) => r.outboundTag === 'direct'); const blockEnabled = ruleArray.some((r) => r.outboundTag === 'block'); const ruleValues = (tag: string, key: 'ip' | 'domain') => ruleArray.find((r) => r.outboundTag === tag && r[key])?.[key] ?? []; function writeRules(rules: SubJsonRule[]) { updateSetting({ subJsonRules: rules.length > 0 ? JSON.stringify(rules) : '' }); } function setTagEnabled(tag: string, defaults: SubJsonRule[], enabled: boolean) { const rest = ruleArray.filter((r) => r.outboundTag !== tag); if (!enabled) { // Turning off the last managed tag also drops foreign-tag leftovers so // the panel still has a path back to an empty subJsonRules. const hasManaged = rest.some((r) => r.outboundTag === 'direct' || r.outboundTag === 'block'); writeRules(hasManaged ? rest : []); return; } // Prepend block defaults so ads match before direct; never re-sort the rest. writeRules(tag === 'block' ? [...defaults, ...rest] : [...rest, ...defaults]); } function setRuleValues( tag: string, key: 'ip' | 'domain', template: SubJsonRule, value: string[], ) { let rules = [...ruleArray]; if (value.length === 0) { rules = rules.filter((r) => !(r.outboundTag === tag && r[key])); } else { let index = rules.findIndex((r) => r.outboundTag === tag && r[key]); if (index === -1) { rules.push({ ...template }); index = rules.length - 1; } rules[index] = { ...rules[index], [key]: [...value] }; } writeRules(rules); } return ( , t('pages.settings.panelSettings'), isMobile, ), children: (
{allSetting.subJsonEnable && ( {t('pages.settings.subJsonEnableTitle')} } > JSON {t('pages.settings.subPath')}} description={t('pages.settings.subPathDesc')} > updateSetting({ subJsonPath: sanitizePath(e.target.value) })} onBlur={() => updateSetting({ subJsonPath: normalizePath(allSetting.subJsonPath) }) } /> JSON {t('pages.settings.subURI')}} description={t('pages.settings.subURIDesc')} > updateSetting({ subJsonURI: e.target.value })} /> updateSetting({ subJsonAlwaysArray: value })} /> updateSetting({ subJsonAutoDetect: v })} /> updateSetting({ subJsonUserAgentRegex: value })} /> updateSetting({ subJsonRoutingRules: e.target.value })} autoSize={{ minRows: 2, maxRows: 6 }} /> updateSetting({ subJsonDns: e.target.value })} autoSize={{ minRows: 2, maxRows: 6 }} /> )} {allSetting.subClashEnable && ( {t('pages.settings.subClashEnableTitle')} } > Clash {t('pages.settings.subPath')}} description={t('pages.settings.subPathDesc')} > updateSetting({ subClashPath: sanitizePath(e.target.value) }) } onBlur={() => updateSetting({ subClashPath: normalizePath(allSetting.subClashPath) }) } /> Clash {t('pages.settings.subURI')}} description={t('pages.settings.subURIDesc')} > updateSetting({ subClashURI: e.target.value })} /> updateSetting({ subClashAutoDetect: v })} /> updateSetting({ subClashUserAgentRegex: value })} /> )}
), }, { key: '2', label: catTabLabel( , t('pages.settings.subFormats.finalMask'), isMobile, ), children: ( <> updateSetting({ subJsonFinalMask: v })} /> ), }, { key: '3', label: catTabLabel(, t('pages.settings.mux'), isMobile), children: ( <> {muxEnabled && (
setMuxField('concurrency', v))} /> setMuxField('xudpConcurrency', v))} /> setRuleValues('direct', 'ip', DEFAULT_DIRECT_RULES[1], v)} options={directIPsOptions} /> {t('pages.settings.direct')} {t('domainName')} } > setRuleValues('block', 'domain', DEFAULT_BLOCK_RULES[0], v)} options={blockDomainsOptions} />