SubscriptionGeneralTab.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { useMemo } from 'react';
  2. import { Divider, Input, InputNumber, Select, Space, Switch, Tabs } from 'antd';
  3. import { ClockCircleOutlined, InfoCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
  4. import { useTranslation } from 'react-i18next';
  5. import type { AllSetting } from '@/models/setting';
  6. import { SettingListItem } from '@/components/ui';
  7. import { useMediaQuery } from '@/hooks/useMediaQuery';
  8. import { catTabLabel } from './catTabLabel';
  9. import { sanitizePath, normalizePath } from './uriPath';
  10. const REMARK_MODELS: Record<string, string> = { i: 'Inbound', e: 'Email', o: 'Other' };
  11. const REMARK_SAMPLES: Record<string, string> = { i: 'Germany', e: 'john', o: 'Relay' };
  12. const REMARK_SEPARATORS = [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'];
  13. interface SubscriptionGeneralTabProps {
  14. allSetting: AllSetting;
  15. updateSetting: (patch: Partial<AllSetting>) => void;
  16. }
  17. export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) {
  18. const { t } = useTranslation();
  19. const { isMobile } = useMediaQuery();
  20. const remarkModel = useMemo(() => {
  21. const rm = allSetting.remarkModel || '';
  22. return rm.length > 1 ? rm.substring(1).split('') : [];
  23. }, [allSetting.remarkModel]);
  24. const remarkSeparator = useMemo(() => {
  25. const rm = allSetting.remarkModel || '-';
  26. return rm.length > 1 ? rm.charAt(0) : '-';
  27. }, [allSetting.remarkModel]);
  28. const remarkSample = useMemo(() => {
  29. const parts = remarkModel.map((k) => REMARK_SAMPLES[k]);
  30. return parts.length === 0 ? '' : parts.join(remarkSeparator);
  31. }, [remarkModel, remarkSeparator]);
  32. function setRemarkModel(parts: string[]) {
  33. updateSetting({ remarkModel: remarkSeparator + parts.join('') });
  34. }
  35. function setRemarkSeparator(sep: string) {
  36. const tail = (allSetting.remarkModel || '-').substring(1);
  37. updateSetting({ remarkModel: sep + tail });
  38. }
  39. return (
  40. <Tabs defaultActiveKey="1" items={[
  41. {
  42. key: '1',
  43. label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
  44. children: (
  45. <>
  46. <SettingListItem paddings="small" title={t('pages.settings.subEnable')} description={t('pages.settings.subEnableDesc')}>
  47. <Switch checked={allSetting.subEnable} onChange={(v) => updateSetting({ subEnable: v })} />
  48. </SettingListItem>
  49. <SettingListItem paddings="small" title={t('pages.settings.subJsonEnableTitle')} description={t('pages.settings.subJsonEnable')}>
  50. <Switch checked={allSetting.subJsonEnable} onChange={(v) => updateSetting({ subJsonEnable: v })} />
  51. </SettingListItem>
  52. <SettingListItem paddings="small" title={t('pages.settings.subClashEnableTitle')}>
  53. <Switch checked={allSetting.subClashEnable} onChange={(v) => updateSetting({ subClashEnable: v })} />
  54. </SettingListItem>
  55. <SettingListItem paddings="small" title={t('pages.settings.subListen')} description={t('pages.settings.subListenDesc')}>
  56. <Input value={allSetting.subListen} onChange={(e) => updateSetting({ subListen: e.target.value })} />
  57. </SettingListItem>
  58. <SettingListItem paddings="small" title={t('pages.settings.subDomain')} description={t('pages.settings.subDomainDesc')}>
  59. <Input value={allSetting.subDomain} onChange={(e) => updateSetting({ subDomain: e.target.value })} />
  60. </SettingListItem>
  61. <SettingListItem paddings="small" title={t('pages.settings.subPort')} description={t('pages.settings.subPortDesc')}>
  62. <InputNumber value={allSetting.subPort} min={1} max={65535} style={{ width: '100%' }}
  63. onChange={(v) => updateSetting({ subPort: Number(v) || 0 })} />
  64. </SettingListItem>
  65. <SettingListItem paddings="small" title={t('pages.settings.subPath')} description={t('pages.settings.subPathDesc')}>
  66. <Input
  67. value={allSetting.subPath}
  68. placeholder="/sub/"
  69. onChange={(e) => updateSetting({ subPath: sanitizePath(e.target.value) })}
  70. onBlur={() => updateSetting({ subPath: normalizePath(allSetting.subPath) })}
  71. />
  72. </SettingListItem>
  73. <SettingListItem paddings="small" title={t('pages.settings.subURI')} description={t('pages.settings.subURIDesc')}>
  74. <Input value={allSetting.subURI} placeholder="(http|https)://domain[:port]/path/"
  75. onChange={(e) => updateSetting({ subURI: e.target.value })} />
  76. </SettingListItem>
  77. </>
  78. ),
  79. },
  80. {
  81. key: '2',
  82. label: catTabLabel(<InfoCircleOutlined />, t('pages.settings.information'), isMobile),
  83. children: (
  84. <>
  85. <SettingListItem paddings="small" title={t('pages.settings.subEncrypt')} description={t('pages.settings.subEncryptDesc')}>
  86. <Switch checked={allSetting.subEncrypt} onChange={(v) => updateSetting({ subEncrypt: v })} />
  87. </SettingListItem>
  88. <SettingListItem paddings="small" title={t('pages.settings.subShowInfo')} description={t('pages.settings.subShowInfoDesc')}>
  89. <Switch checked={allSetting.subShowInfo} onChange={(v) => updateSetting({ subShowInfo: v })} />
  90. </SettingListItem>
  91. <SettingListItem paddings="small" title={t('pages.settings.subEmailInRemark')} description={t('pages.settings.subEmailInRemarkDesc')}>
  92. <Switch checked={allSetting.subEmailInRemark} onChange={(v) => updateSetting({ subEmailInRemark: v })} />
  93. </SettingListItem>
  94. <SettingListItem
  95. paddings="small"
  96. title={t('pages.settings.remarkModel')}
  97. description={
  98. <>
  99. {t('pages.settings.sampleRemark')}:{' '}
  100. <span
  101. style={{
  102. fontFamily: 'monospace',
  103. padding: '1px 6px',
  104. borderRadius: 4,
  105. border: '1px solid var(--ant-color-border)',
  106. background: 'var(--ant-color-fill-tertiary)',
  107. whiteSpace: 'pre',
  108. }}
  109. >
  110. {remarkSample ? `#${remarkSample}` : '—'}
  111. </span>
  112. </>
  113. }
  114. >
  115. <Space.Compact style={{ width: '100%' }}>
  116. <Select
  117. mode="multiple"
  118. value={remarkModel}
  119. onChange={setRemarkModel}
  120. style={{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }}
  121. options={Object.entries(REMARK_MODELS).map(([k, l]) => ({ value: k, label: l }))}
  122. />
  123. <Select
  124. value={remarkSeparator}
  125. onChange={setRemarkSeparator}
  126. style={{ width: '20%' }}
  127. options={REMARK_SEPARATORS.map((s) => ({ value: s, label: s === ' ' ? '␣' : s }))}
  128. />
  129. </Space.Compact>
  130. </SettingListItem>
  131. <Divider>{t('pages.settings.subTitle')}</Divider>
  132. <SettingListItem paddings="small" title={t('pages.settings.subTitle')} description={t('pages.settings.subTitleDesc')}>
  133. <Input value={allSetting.subTitle} onChange={(e) => updateSetting({ subTitle: e.target.value })} />
  134. </SettingListItem>
  135. <SettingListItem paddings="small" title={t('pages.settings.subSupportUrl')} description={t('pages.settings.subSupportUrlDesc')}>
  136. <Input value={allSetting.subSupportUrl} placeholder="https://example.com"
  137. onChange={(e) => updateSetting({ subSupportUrl: e.target.value })} />
  138. </SettingListItem>
  139. <SettingListItem paddings="small" title={t('pages.settings.subProfileUrl')} description={t('pages.settings.subProfileUrlDesc')}>
  140. <Input value={allSetting.subProfileUrl} placeholder="https://example.com"
  141. onChange={(e) => updateSetting({ subProfileUrl: e.target.value })} />
  142. </SettingListItem>
  143. <SettingListItem paddings="small" title={t('pages.settings.subAnnounce')} description={t('pages.settings.subAnnounceDesc')}>
  144. <Input.TextArea value={allSetting.subAnnounce}
  145. onChange={(e) => updateSetting({ subAnnounce: e.target.value })} />
  146. </SettingListItem>
  147. <SettingListItem
  148. paddings="small"
  149. title={t('pages.settings.subThemeDir')}
  150. description={(
  151. <>
  152. {t('pages.settings.subThemeDirDesc')}{' '}
  153. <a
  154. href="https://github.com/MHSanaei/3x-ui/blob/main/docs/custom-subscription-templates.md"
  155. target="_blank"
  156. rel="noopener noreferrer"
  157. >
  158. {t('pages.settings.subThemeDirDocs')}
  159. </a>
  160. </>
  161. )}
  162. >
  163. <Input value={allSetting.subThemeDir} placeholder="/etc/3x-ui/sub_templates/my-theme/"
  164. onChange={(e) => updateSetting({ subThemeDir: e.target.value })} />
  165. </SettingListItem>
  166. <Divider>Happ</Divider>
  167. <SettingListItem paddings="small" title={t('pages.settings.subEnableRouting')} description={t('pages.settings.subEnableRoutingDesc')}>
  168. <Switch checked={allSetting.subEnableRouting} onChange={(v) => updateSetting({ subEnableRouting: v })} />
  169. </SettingListItem>
  170. <SettingListItem paddings="small" title={t('pages.settings.subRoutingRules')} description={t('pages.settings.subRoutingRulesDesc')}>
  171. <Input.TextArea value={allSetting.subRoutingRules} placeholder="happ://routing/add/..."
  172. onChange={(e) => updateSetting({ subRoutingRules: e.target.value })} />
  173. </SettingListItem>
  174. <Divider>Clash / Mihomo</Divider>
  175. <SettingListItem paddings="small" title={t('pages.settings.subClashEnableRouting')} description={t('pages.settings.subClashEnableRoutingDesc')}>
  176. <Switch checked={allSetting.subClashEnableRouting} onChange={(v) => updateSetting({ subClashEnableRouting: v })} />
  177. </SettingListItem>
  178. <SettingListItem paddings="small" title={t('pages.settings.subClashRoutingRules')} description={t('pages.settings.subClashRoutingRulesDesc')}>
  179. <Input.TextArea
  180. value={allSetting.subClashRules}
  181. rows={8}
  182. placeholder={'GEOSITE,category-ir,DIRECT\nGEOIP,private,DIRECT'}
  183. onChange={(e) => updateSetting({ subClashRules: e.target.value })}
  184. />
  185. </SettingListItem>
  186. </>
  187. ),
  188. },
  189. {
  190. key: '3',
  191. label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
  192. children: (
  193. <>
  194. <SettingListItem paddings="small" title={t('pages.settings.subCertPath')} description={t('pages.settings.subCertPathDesc')}>
  195. <Input value={allSetting.subCertFile} onChange={(e) => updateSetting({ subCertFile: e.target.value })} />
  196. </SettingListItem>
  197. <SettingListItem paddings="small" title={t('pages.settings.subKeyPath')} description={t('pages.settings.subKeyPathDesc')}>
  198. <Input value={allSetting.subKeyFile} onChange={(e) => updateSetting({ subKeyFile: e.target.value })} />
  199. </SettingListItem>
  200. </>
  201. ),
  202. },
  203. {
  204. key: '4',
  205. label: catTabLabel(<ClockCircleOutlined />, t('pages.settings.intervals'), isMobile),
  206. children: (
  207. <>
  208. <SettingListItem paddings="small" title={t('pages.settings.subUpdates')} description={t('pages.settings.subUpdatesDesc')}>
  209. <InputNumber value={allSetting.subUpdates} min={1} style={{ width: '100%' }}
  210. onChange={(v) => updateSetting({ subUpdates: Number(v) || 0 })} />
  211. </SettingListItem>
  212. </>
  213. ),
  214. },
  215. ]} />
  216. );
  217. }