SubscriptionFormatsTab.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { useMemo } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Input,
  5. InputNumber,
  6. Select,
  7. Switch,
  8. Tabs,
  9. } from 'antd';
  10. import {
  11. PartitionOutlined,
  12. RocketOutlined,
  13. SendOutlined,
  14. SettingOutlined,
  15. } from '@ant-design/icons';
  16. import type { AllSetting } from '@/models/setting';
  17. import { SettingListItem } from '@/components/ui';
  18. import { useMediaQuery } from '@/hooks/useMediaQuery';
  19. import { catTabLabel } from './catTabLabel';
  20. import { sanitizePath, normalizePath } from './uriPath';
  21. import SubJsonFinalMaskForm from './SubJsonFinalMaskForm';
  22. import './SubscriptionFormatsTab.css';
  23. interface SubscriptionFormatsTabProps {
  24. allSetting: AllSetting;
  25. updateSetting: (patch: Partial<AllSetting>) => void;
  26. }
  27. const DEFAULT_MUX = {
  28. enabled: true,
  29. concurrency: 8,
  30. xudpConcurrency: 16,
  31. xudpProxyUDP443: 'reject',
  32. };
  33. const DEFAULT_RULES: { type: string; outboundTag: string; domain?: string[]; ip?: string[] }[] = [
  34. { type: 'field', outboundTag: 'direct', domain: ['geosite:category-ir'] },
  35. { type: 'field', outboundTag: 'direct', ip: ['geoip:private', 'geoip:ir'] },
  36. ];
  37. const directIPsOptions = [
  38. { label: 'Private IP', value: 'geoip:private' },
  39. { label: '🇮🇷 Iran', value: 'geoip:ir' },
  40. { label: '🇨🇳 China', value: 'geoip:cn' },
  41. { label: '🇷🇺 Russia', value: 'geoip:ru' },
  42. { label: '🇻🇳 Vietnam', value: 'geoip:vn' },
  43. { label: '🇪🇸 Spain', value: 'geoip:es' },
  44. { label: '🇮🇩 Indonesia', value: 'geoip:id' },
  45. { label: '🇺🇦 Ukraine', value: 'geoip:ua' },
  46. { label: '🇹🇷 Türkiye', value: 'geoip:tr' },
  47. { label: '🇧🇷 Brazil', value: 'geoip:br' },
  48. ];
  49. const directDomainsOptions = [
  50. { label: 'Private DNS', value: 'geosite:private' },
  51. { label: '🇮🇷 Iran', value: 'geosite:category-ir' },
  52. { label: '🇨🇳 China', value: 'geosite:cn' },
  53. { label: '🇷🇺 Russia', value: 'geosite:category-ru' },
  54. { label: 'Apple', value: 'geosite:apple' },
  55. { label: 'Meta', value: 'geosite:meta' },
  56. { label: 'Google', value: 'geosite:google' },
  57. ];
  58. function readJson<T>(raw: string, fallback: T): T {
  59. try {
  60. if (!raw) return fallback;
  61. return JSON.parse(raw) as T;
  62. } catch {
  63. return fallback;
  64. }
  65. }
  66. export default function SubscriptionFormatsTab({ allSetting, updateSetting }: SubscriptionFormatsTabProps) {
  67. const { t } = useTranslation();
  68. const { isMobile } = useMediaQuery();
  69. const muxEnabled = allSetting.subJsonMux !== '';
  70. const directEnabled = allSetting.subJsonRules !== '';
  71. const muxObj = useMemo(
  72. () => (muxEnabled ? readJson<typeof DEFAULT_MUX>(allSetting.subJsonMux, DEFAULT_MUX) : DEFAULT_MUX),
  73. [allSetting.subJsonMux, muxEnabled],
  74. );
  75. function setMuxEnabled(v: boolean) {
  76. updateSetting({ subJsonMux: v ? JSON.stringify(DEFAULT_MUX) : '' });
  77. }
  78. function setMuxField<K extends keyof typeof DEFAULT_MUX>(key: K, value: typeof DEFAULT_MUX[K]) {
  79. const next = { ...muxObj, [key]: value };
  80. updateSetting({ subJsonMux: JSON.stringify(next) });
  81. }
  82. const ruleArray = useMemo(() => {
  83. if (!directEnabled) return null;
  84. return readJson<typeof DEFAULT_RULES | null>(allSetting.subJsonRules, null);
  85. }, [allSetting.subJsonRules, directEnabled]);
  86. const directIPs = useMemo(() => {
  87. if (!ruleArray) return [];
  88. const ipRule = ruleArray.find((r) => r.ip);
  89. return ipRule?.ip ?? [];
  90. }, [ruleArray]);
  91. const directDomains = useMemo(() => {
  92. if (!ruleArray) return [];
  93. const dRule = ruleArray.find((r) => r.domain);
  94. return dRule?.domain ?? [];
  95. }, [ruleArray]);
  96. function setDirectEnabled(v: boolean) {
  97. updateSetting({ subJsonRules: v ? JSON.stringify(DEFAULT_RULES) : '' });
  98. }
  99. function setDirectIPs(value: string[]) {
  100. if (!ruleArray) return;
  101. let rules = [...ruleArray];
  102. if (value.length === 0) {
  103. rules = rules.filter((r) => !r.ip);
  104. } else {
  105. let idx = rules.findIndex((r) => r.ip);
  106. if (idx === -1) {
  107. rules.push({ ...DEFAULT_RULES[1] });
  108. idx = rules.length - 1;
  109. }
  110. rules[idx] = { ...rules[idx], ip: [...value] };
  111. }
  112. updateSetting({ subJsonRules: JSON.stringify(rules) });
  113. }
  114. function setDirectDomains(value: string[]) {
  115. if (!ruleArray) return;
  116. let rules = [...ruleArray];
  117. if (value.length === 0) {
  118. rules = rules.filter((r) => !r.domain);
  119. } else {
  120. let idx = rules.findIndex((r) => r.domain);
  121. if (idx === -1) {
  122. rules.push({ ...DEFAULT_RULES[0] });
  123. idx = rules.length - 1;
  124. }
  125. rules[idx] = { ...rules[idx], domain: [...value] };
  126. }
  127. updateSetting({ subJsonRules: JSON.stringify(rules) });
  128. }
  129. return (
  130. <Tabs defaultActiveKey="1" items={[
  131. {
  132. key: '1',
  133. label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
  134. children: (
  135. <>
  136. {allSetting.subJsonEnable && (
  137. <>
  138. <SettingListItem paddings="small" title={<>JSON {t('pages.settings.subPath')}</>} description={t('pages.settings.subPathDesc')}>
  139. <Input
  140. value={allSetting.subJsonPath}
  141. placeholder="/json/"
  142. onChange={(e) => updateSetting({ subJsonPath: sanitizePath(e.target.value) })}
  143. onBlur={() => updateSetting({ subJsonPath: normalizePath(allSetting.subJsonPath) })}
  144. />
  145. </SettingListItem>
  146. <SettingListItem paddings="small" title={<>JSON {t('pages.settings.subURI')}</>} description={t('pages.settings.subURIDesc')}>
  147. <Input
  148. value={allSetting.subJsonURI}
  149. placeholder="(http|https)://domain[:port]/path/"
  150. onChange={(e) => updateSetting({ subJsonURI: e.target.value })}
  151. />
  152. </SettingListItem>
  153. </>
  154. )}
  155. {allSetting.subClashEnable && (
  156. <>
  157. <SettingListItem paddings="small" title={<>Clash {t('pages.settings.subPath')}</>} description={t('pages.settings.subPathDesc')}>
  158. <Input
  159. value={allSetting.subClashPath}
  160. placeholder="/clash/"
  161. onChange={(e) => updateSetting({ subClashPath: sanitizePath(e.target.value) })}
  162. onBlur={() => updateSetting({ subClashPath: normalizePath(allSetting.subClashPath) })}
  163. />
  164. </SettingListItem>
  165. <SettingListItem paddings="small" title={<>Clash {t('pages.settings.subURI')}</>} description={t('pages.settings.subURIDesc')}>
  166. <Input
  167. value={allSetting.subClashURI}
  168. placeholder="(http|https)://domain[:port]/path/"
  169. onChange={(e) => updateSetting({ subClashURI: e.target.value })}
  170. />
  171. </SettingListItem>
  172. </>
  173. )}
  174. </>
  175. ),
  176. },
  177. {
  178. key: '2',
  179. label: catTabLabel(<RocketOutlined />, t('pages.settings.subFormats.finalMask'), isMobile),
  180. children: (
  181. <>
  182. <SettingListItem paddings="small" title={t('pages.settings.subFormats.finalMask')} description={t('pages.settings.subFormats.finalMaskDesc')} />
  183. <SubJsonFinalMaskForm
  184. value={allSetting.subJsonFinalMask}
  185. onChange={(v) => updateSetting({ subJsonFinalMask: v })}
  186. />
  187. </>
  188. ),
  189. },
  190. {
  191. key: '3',
  192. label: catTabLabel(<PartitionOutlined />, t('pages.settings.mux'), isMobile),
  193. children: (
  194. <>
  195. <SettingListItem paddings="small" title={t('pages.settings.mux')} description={t('pages.settings.muxDesc')}>
  196. <Switch checked={muxEnabled} onChange={setMuxEnabled} />
  197. </SettingListItem>
  198. {muxEnabled && (
  199. <div className="format-settings">
  200. <SettingListItem paddings="small" title={t('pages.settings.subFormats.concurrency')}>
  201. <InputNumber value={muxObj.concurrency} min={-1} max={1024} style={{ width: '100%' }}
  202. onChange={(v) => setMuxField('concurrency', Number(v) || 0)} />
  203. </SettingListItem>
  204. <SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpConcurrency')}>
  205. <InputNumber value={muxObj.xudpConcurrency} min={-1} max={1024} style={{ width: '100%' }}
  206. onChange={(v) => setMuxField('xudpConcurrency', Number(v) || 0)} />
  207. </SettingListItem>
  208. <SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpUdp443')}>
  209. <Select
  210. value={muxObj.xudpProxyUDP443}
  211. style={{ width: '100%' }}
  212. onChange={(v) => setMuxField('xudpProxyUDP443', v)}
  213. options={['reject', 'allow', 'skip'].map((p) => ({ value: p, label: p }))}
  214. />
  215. </SettingListItem>
  216. </div>
  217. )}
  218. </>
  219. ),
  220. },
  221. {
  222. key: '4',
  223. label: catTabLabel(<SendOutlined />, t('pages.settings.direct'), isMobile),
  224. children: (
  225. <>
  226. <SettingListItem paddings="small" title={t('pages.settings.direct')} description={t('pages.settings.directDesc')}>
  227. <Switch checked={directEnabled} onChange={setDirectEnabled} />
  228. </SettingListItem>
  229. {directEnabled && (
  230. <div className="format-settings">
  231. <SettingListItem paddings="small" title={<>{t('pages.settings.direct')} IPs</>}>
  232. <Select
  233. mode="tags"
  234. value={directIPs}
  235. style={{ width: '100%' }}
  236. onChange={setDirectIPs}
  237. options={directIPsOptions}
  238. />
  239. </SettingListItem>
  240. <SettingListItem paddings="small" title={<>{t('pages.settings.direct')} {t('domainName')}</>}>
  241. <Select
  242. mode="tags"
  243. value={directDomains}
  244. style={{ width: '100%' }}
  245. onChange={setDirectDomains}
  246. options={directDomainsOptions}
  247. />
  248. </SettingListItem>
  249. </div>
  250. )}
  251. </>
  252. ),
  253. },
  254. ]} />
  255. );
  256. }