SubscriptionGeneralTab.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import { Alert, Button, Input, InputNumber, Select, Switch, Tabs } from 'antd';
  2. import {
  3. BranchesOutlined,
  4. CompassOutlined,
  5. IdcardOutlined,
  6. InfoCircleOutlined,
  7. NodeIndexOutlined,
  8. SafetyCertificateOutlined,
  9. SettingOutlined,
  10. } from '@ant-design/icons';
  11. import { useTranslation } from 'react-i18next';
  12. import { useNavigate, useSearchParams } from 'react-router';
  13. import type { AllSetting } from '@/models/setting';
  14. import type { SubProfileMode } from '@/schemas/setting';
  15. import { onNumber } from '@/utils/onNumber';
  16. import { DefaultSettingTag, SettingListItem } from '@/components/ui';
  17. import { RemarkTemplateField } from '@/components/form';
  18. import { useMediaQuery } from '@/hooks/useMediaQuery';
  19. import { catTabLabel } from './catTabLabel';
  20. import { sanitizePath, normalizePath } from './uriPath';
  21. import HappSettingsContent from './HappSettingsContent';
  22. import IncySettingsContent from './IncySettingsContent';
  23. import { remoteSourceBadge } from './subscriptionShared';
  24. interface SubscriptionGeneralTabProps {
  25. allSetting: AllSetting;
  26. updateSetting: (patch: Partial<AllSetting>) => void;
  27. }
  28. const PANEL_SETTINGS_TAB = '1';
  29. const HAPP_SETTINGS_TAB = '5';
  30. export default function SubscriptionGeneralTab({
  31. allSetting,
  32. updateSetting,
  33. }: SubscriptionGeneralTabProps) {
  34. const { t } = useTranslation();
  35. const navigate = useNavigate();
  36. const [searchParams] = useSearchParams();
  37. const { isMobile } = useMediaQuery();
  38. // Keep the URL semantic while mapping to the legacy numeric key used by these inner tabs.
  39. const initialTab =
  40. searchParams.get('subscriptionTab') === 'happ' ? HAPP_SETTINGS_TAB : PANEL_SETTINGS_TAB;
  41. return (
  42. <Tabs
  43. defaultActiveKey={initialTab}
  44. items={[
  45. {
  46. key: '1',
  47. label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
  48. children: (
  49. <>
  50. <SettingListItem
  51. paddings="small"
  52. title={t('pages.settings.subEnable')}
  53. description={t('pages.settings.subEnableDesc')}
  54. >
  55. <Switch
  56. checked={allSetting.subEnable}
  57. onChange={(v) => updateSetting({ subEnable: v })}
  58. />
  59. </SettingListItem>
  60. <SettingListItem
  61. paddings="small"
  62. title={t('pages.settings.subJsonEnableTitle')}
  63. description={t('pages.settings.subJsonEnable')}
  64. >
  65. <Switch
  66. checked={allSetting.subJsonEnable}
  67. onChange={(v) => updateSetting({ subJsonEnable: v })}
  68. />
  69. </SettingListItem>
  70. <SettingListItem paddings="small" title={t('pages.settings.subClashEnableTitle')}>
  71. <Switch
  72. checked={allSetting.subClashEnable}
  73. onChange={(v) => updateSetting({ subClashEnable: v })}
  74. />
  75. </SettingListItem>
  76. {(allSetting.subJsonEnable || allSetting.subClashEnable) && (
  77. <Alert
  78. type="info"
  79. showIcon
  80. style={{ margin: '12px 20px' }}
  81. title={t('pages.settings.subFormatsTipTitle')}
  82. description={t('pages.settings.subFormatsTipDesc')}
  83. action={
  84. <Button size="small" onClick={() => navigate('/settings#subscription-formats')}>
  85. {t('pages.settings.subFormatsTipAction')}
  86. </Button>
  87. }
  88. />
  89. )}
  90. <SettingListItem
  91. paddings="small"
  92. title={t('pages.settings.subListen')}
  93. description={t('pages.settings.subListenDesc')}
  94. >
  95. <Input
  96. value={allSetting.subListen}
  97. onChange={(e) => updateSetting({ subListen: e.target.value })}
  98. />
  99. </SettingListItem>
  100. <SettingListItem
  101. paddings="small"
  102. title={t('pages.settings.subDomain')}
  103. description={t('pages.settings.subDomainDesc')}
  104. >
  105. <Input
  106. value={allSetting.subDomain}
  107. onChange={(e) => updateSetting({ subDomain: e.target.value })}
  108. />
  109. </SettingListItem>
  110. <SettingListItem
  111. paddings="small"
  112. title={t('pages.settings.subPort')}
  113. badge={<DefaultSettingTag settingKey="subPort" value={allSetting.subPort} />}
  114. description={t('pages.settings.subPortDesc')}
  115. >
  116. <InputNumber
  117. value={allSetting.subPort}
  118. min={1}
  119. max={65535}
  120. style={{ width: '100%' }}
  121. onChange={onNumber((v) => updateSetting({ subPort: v }))}
  122. />
  123. </SettingListItem>
  124. <SettingListItem
  125. paddings="small"
  126. title={t('pages.settings.subPath')}
  127. description={t('pages.settings.subPathDesc')}
  128. >
  129. <Input
  130. value={allSetting.subPath}
  131. placeholder="/sub/"
  132. onChange={(e) => updateSetting({ subPath: sanitizePath(e.target.value) })}
  133. onBlur={() => updateSetting({ subPath: normalizePath(allSetting.subPath) })}
  134. />
  135. </SettingListItem>
  136. <SettingListItem
  137. paddings="small"
  138. title={t('pages.settings.subURI')}
  139. description={t('pages.settings.subURIDesc')}
  140. >
  141. <Input
  142. value={allSetting.subURI}
  143. placeholder="(http|https)://domain[:port]/path/"
  144. onChange={(e) => updateSetting({ subURI: e.target.value })}
  145. />
  146. </SettingListItem>
  147. </>
  148. ),
  149. },
  150. {
  151. key: '2',
  152. label: catTabLabel(<InfoCircleOutlined />, t('pages.settings.information'), isMobile),
  153. children: (
  154. <>
  155. <SettingListItem
  156. paddings="small"
  157. title={t('pages.settings.subEncrypt')}
  158. description={t('pages.settings.subEncryptDesc')}
  159. >
  160. <Switch
  161. checked={allSetting.subEncrypt}
  162. onChange={(v) => updateSetting({ subEncrypt: v })}
  163. />
  164. </SettingListItem>
  165. <SettingListItem
  166. paddings="small"
  167. title={t('pages.settings.remarkTemplate')}
  168. description={t('pages.settings.remarkTemplateDesc')}
  169. >
  170. <RemarkTemplateField
  171. value={allSetting.remarkTemplate}
  172. onChange={(v) => updateSetting({ remarkTemplate: v })}
  173. maxLength={256}
  174. />
  175. </SettingListItem>
  176. <SettingListItem
  177. paddings="small"
  178. title={t('pages.settings.subShowIdentityOnAllLinks')}
  179. description={t('pages.settings.subShowIdentityOnAllLinksDesc')}
  180. >
  181. <Switch
  182. checked={allSetting.subShowIdentityOnAllLinks}
  183. onChange={(v) => updateSetting({ subShowIdentityOnAllLinks: v })}
  184. />
  185. </SettingListItem>
  186. <SettingListItem
  187. paddings="small"
  188. title={t('pages.settings.subInfoNodeEnable')}
  189. description={t('pages.settings.subInfoNodeEnableDesc')}
  190. >
  191. <Switch
  192. checked={allSetting.subInfoNodeEnable}
  193. onChange={(v) => updateSetting({ subInfoNodeEnable: v })}
  194. />
  195. </SettingListItem>
  196. <SettingListItem
  197. paddings="small"
  198. title={t('pages.settings.subCalendarExpireInclusive')}
  199. description={t('pages.settings.subCalendarExpireInclusiveDesc')}
  200. >
  201. <Switch
  202. checked={allSetting.subCalendarExpireInclusive}
  203. onChange={(v) => updateSetting({ subCalendarExpireInclusive: v })}
  204. />
  205. </SettingListItem>
  206. <SettingListItem
  207. paddings="small"
  208. title={t('pages.settings.subExpiredTemplate')}
  209. description={t('pages.settings.subExpiredTemplateDesc')}
  210. >
  211. <RemarkTemplateField
  212. value={allSetting.subExpiredTemplate}
  213. onChange={(v) => updateSetting({ subExpiredTemplate: v })}
  214. maxLength={256}
  215. />
  216. </SettingListItem>
  217. <SettingListItem
  218. paddings="small"
  219. title={t('pages.settings.subTrafficDepletedTemplate')}
  220. description={t('pages.settings.subTrafficDepletedTemplateDesc')}
  221. >
  222. <RemarkTemplateField
  223. value={allSetting.subTrafficDepletedTemplate}
  224. onChange={(v) => updateSetting({ subTrafficDepletedTemplate: v })}
  225. maxLength={256}
  226. />
  227. </SettingListItem>
  228. <SettingListItem
  229. paddings="small"
  230. title={t('pages.settings.subUpdates')}
  231. badge={<DefaultSettingTag settingKey="subUpdates" value={allSetting.subUpdates} />}
  232. description={t('pages.settings.subUpdatesDesc')}
  233. >
  234. <InputNumber
  235. value={allSetting.subUpdates}
  236. min={0}
  237. max={525600}
  238. style={{ width: '100%' }}
  239. onChange={onNumber((v) => updateSetting({ subUpdates: v }))}
  240. />
  241. </SettingListItem>
  242. <SettingListItem
  243. paddings="small"
  244. title={t('pages.settings.externalSubUserAgent')}
  245. badge={
  246. <DefaultSettingTag
  247. settingKey="externalSubUserAgent"
  248. value={allSetting.externalSubUserAgent}
  249. />
  250. }
  251. description={t('pages.settings.externalSubUserAgentDesc')}
  252. >
  253. <Input
  254. value={allSetting.externalSubUserAgent}
  255. placeholder="v2rayNG/1.8.5"
  256. maxLength={512}
  257. allowClear
  258. onChange={(e) => updateSetting({ externalSubUserAgent: e.target.value })}
  259. />
  260. </SettingListItem>
  261. </>
  262. ),
  263. },
  264. {
  265. key: '3',
  266. label: catTabLabel(<IdcardOutlined />, t('pages.settings.profile'), isMobile),
  267. children: (
  268. <>
  269. <SettingListItem
  270. paddings="small"
  271. title={t('pages.settings.subTitle')}
  272. description={t('pages.settings.subTitleDesc')}
  273. >
  274. <RemarkTemplateField
  275. value={allSetting.subTitle}
  276. onChange={(v) => updateSetting({ subTitle: v })}
  277. metadataOnly
  278. />
  279. </SettingListItem>
  280. <SettingListItem
  281. paddings="small"
  282. title={t('pages.settings.subSupportUrl')}
  283. description={t('pages.settings.subSupportUrlDesc')}
  284. >
  285. <RemarkTemplateField
  286. value={allSetting.subSupportUrl}
  287. placeholder="https://example.com"
  288. onChange={(v) => updateSetting({ subSupportUrl: v })}
  289. metadataOnly
  290. />
  291. </SettingListItem>
  292. <SettingListItem
  293. paddings="small"
  294. title={t('pages.settings.subProfileMode')}
  295. description={t('pages.settings.subProfileModeDesc')}
  296. >
  297. <Select<SubProfileMode>
  298. id="sub-profile-mode"
  299. aria-label={t('pages.settings.subProfileMode')}
  300. value={allSetting.subProfileMode}
  301. style={{ width: '100%' }}
  302. onChange={(value) => updateSetting({ subProfileMode: value })}
  303. options={[
  304. { value: 'none', label: t('pages.settings.subProfileModeNone') },
  305. { value: 'builtin', label: t('pages.settings.subProfileModeBuiltin') },
  306. { value: 'custom', label: t('pages.settings.subProfileModeCustom') },
  307. ]}
  308. />
  309. </SettingListItem>
  310. {allSetting.subProfileMode === 'builtin' ? (
  311. <Alert
  312. type="warning"
  313. showIcon
  314. style={{ margin: '12px 20px' }}
  315. title={t('pages.settings.subProfileBuiltinWarning')}
  316. />
  317. ) : null}
  318. {allSetting.subProfileMode === 'custom' ? (
  319. <SettingListItem
  320. paddings="small"
  321. title={t('pages.settings.subProfileUrl')}
  322. description={t('pages.settings.subProfileUrlDesc')}
  323. >
  324. <RemarkTemplateField
  325. value={allSetting.subProfileUrl}
  326. placeholder="https://example.com"
  327. onChange={(v) => updateSetting({ subProfileUrl: v })}
  328. metadataOnly
  329. />
  330. </SettingListItem>
  331. ) : null}
  332. <SettingListItem
  333. paddings="small"
  334. title={t('pages.settings.subAnnounce')}
  335. description={t('pages.settings.subAnnounceDesc')}
  336. >
  337. <RemarkTemplateField
  338. value={allSetting.subAnnounce}
  339. onChange={(v) => updateSetting({ subAnnounce: v })}
  340. multiline
  341. rows={3}
  342. metadataOnly
  343. />
  344. </SettingListItem>
  345. <SettingListItem
  346. paddings="small"
  347. title={t('pages.settings.subThemeDir')}
  348. description={
  349. <>
  350. {t('pages.settings.subThemeDirDesc')}{' '}
  351. <a
  352. href="https://github.com/MHSanaei/3x-ui/blob/main/docs/custom-subscription-templates.md"
  353. target="_blank"
  354. rel="noopener noreferrer"
  355. >
  356. {t('pages.settings.subThemeDirDocs')}
  357. </a>
  358. </>
  359. }
  360. >
  361. <Input
  362. value={allSetting.subThemeDir}
  363. placeholder="/etc/3x-ui/sub_templates/my-theme/"
  364. onChange={(e) => updateSetting({ subThemeDir: e.target.value })}
  365. />
  366. </SettingListItem>
  367. </>
  368. ),
  369. },
  370. {
  371. key: '4',
  372. label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
  373. children: (
  374. <>
  375. <SettingListItem
  376. paddings="small"
  377. title={t('pages.settings.subCertPath')}
  378. description={t('pages.settings.subCertPathDesc')}
  379. >
  380. <Input
  381. value={allSetting.subCertFile}
  382. onChange={(e) => updateSetting({ subCertFile: e.target.value })}
  383. />
  384. </SettingListItem>
  385. <SettingListItem
  386. paddings="small"
  387. title={t('pages.settings.subKeyPath')}
  388. description={t('pages.settings.subKeyPathDesc')}
  389. >
  390. <Input
  391. value={allSetting.subKeyFile}
  392. onChange={(e) => updateSetting({ subKeyFile: e.target.value })}
  393. />
  394. </SettingListItem>
  395. </>
  396. ),
  397. },
  398. {
  399. key: '5',
  400. label: catTabLabel(<BranchesOutlined />, 'Happ', isMobile),
  401. children: (
  402. <HappSettingsContent
  403. allSetting={allSetting}
  404. updateSetting={updateSetting}
  405. isMobile={isMobile}
  406. remoteSourceBadge={remoteSourceBadge}
  407. />
  408. ),
  409. },
  410. {
  411. key: '6',
  412. label: catTabLabel(<NodeIndexOutlined />, 'Clash / Mihomo', isMobile),
  413. children: (
  414. <>
  415. <SettingListItem
  416. paddings="small"
  417. title={t('pages.settings.subClashEnableRouting')}
  418. description={t('pages.settings.subClashEnableRoutingDesc')}
  419. >
  420. <Switch
  421. checked={allSetting.subClashEnableRouting}
  422. onChange={(v) => updateSetting({ subClashEnableRouting: v })}
  423. />
  424. </SettingListItem>
  425. <SettingListItem
  426. paddings="small"
  427. title={t('pages.settings.subClashRoutingRules')}
  428. badge={remoteSourceBadge(allSetting.subClashRules)}
  429. description={t('pages.settings.subClashRoutingRulesDesc')}
  430. >
  431. <Input.TextArea
  432. value={allSetting.subClashRules}
  433. rows={8}
  434. placeholder={
  435. 'https://.../routing.yaml\n\nor inline rules:\nGEOSITE,category-ir,DIRECT'
  436. }
  437. onChange={(e) => updateSetting({ subClashRules: e.target.value })}
  438. />
  439. </SettingListItem>
  440. </>
  441. ),
  442. },
  443. {
  444. key: '7',
  445. label: catTabLabel(<CompassOutlined />, 'Incy', isMobile),
  446. children: (
  447. <IncySettingsContent
  448. allSetting={allSetting}
  449. updateSetting={updateSetting}
  450. isMobile={isMobile}
  451. remoteSourceBadge={remoteSourceBadge}
  452. />
  453. ),
  454. },
  455. ]}
  456. />
  457. );
  458. }