HappSettingsContent.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Input, Modal, Select, Space, Switch, Tabs, message } from 'antd';
  4. import {
  5. BranchesOutlined,
  6. BuildOutlined,
  7. CloudSyncOutlined,
  8. DesktopOutlined,
  9. MobileOutlined,
  10. NotificationOutlined,
  11. ThunderboltOutlined,
  12. } from '@ant-design/icons';
  13. import type { AllSetting } from '@/models/setting';
  14. import { SettingListItem } from '@/components/ui';
  15. import { buildHappPresetDeeplink, parseList, toBase64Utf8 } from './happPresets';
  16. interface HappSettingsContentProps {
  17. allSetting: AllSetting;
  18. updateSetting: (patch: Partial<AllSetting>) => void;
  19. isMobile: boolean;
  20. remoteSourceBadge: (val: string) => React.ReactNode;
  21. }
  22. export default function HappSettingsContent({
  23. allSetting,
  24. updateSetting,
  25. isMobile,
  26. remoteSourceBadge,
  27. }: HappSettingsContentProps) {
  28. const { t } = useTranslation();
  29. const [selectedPreset, setSelectedPreset] = useState<string>('iran-bypass');
  30. const [isModalOpen, setIsModalOpen] = useState(false);
  31. const [directDomains, setDirectDomains] = useState('');
  32. const [proxyDomains, setProxyDomains] = useState('');
  33. const [blockDomains, setBlockDomains] = useState('');
  34. const [directIPs, setDirectIPs] = useState('');
  35. const [proxyIPs, setProxyIPs] = useState('');
  36. const [blockIPs, setBlockIPs] = useState('');
  37. const applyPreset = () => {
  38. const payload = buildHappPresetDeeplink(selectedPreset);
  39. if (payload) {
  40. updateSetting({ subRoutingRules: payload });
  41. message.success(t('pages.settings.subHappPresetApplied'));
  42. }
  43. };
  44. const handleBuildDeeplink = () => {
  45. interface FieldRule {
  46. type: string;
  47. outboundTag: string;
  48. domain?: string[];
  49. ip?: string[];
  50. network?: string;
  51. }
  52. const rules: FieldRule[] = [];
  53. const bDom = parseList(blockDomains);
  54. const bIp = parseList(blockIPs);
  55. if (bDom.length > 0 || bIp.length > 0) {
  56. rules.push({
  57. type: 'field',
  58. outboundTag: 'block',
  59. ...(bDom.length > 0 ? { domain: bDom } : {}),
  60. ...(bIp.length > 0 ? { ip: bIp } : {}),
  61. });
  62. }
  63. const dDom = parseList(directDomains);
  64. const dIp = parseList(directIPs);
  65. if (dDom.length > 0 || dIp.length > 0) {
  66. rules.push({
  67. type: 'field',
  68. outboundTag: 'direct',
  69. ...(dDom.length > 0 ? { domain: dDom } : {}),
  70. ...(dIp.length > 0 ? { ip: dIp } : {}),
  71. });
  72. }
  73. const pDom = parseList(proxyDomains);
  74. const pIp = parseList(proxyIPs);
  75. if (pDom.length > 0 || pIp.length > 0) {
  76. rules.push({
  77. type: 'field',
  78. outboundTag: 'proxy',
  79. ...(pDom.length > 0 ? { domain: pDom } : {}),
  80. ...(pIp.length > 0 ? { ip: pIp } : {}),
  81. });
  82. }
  83. rules.push({
  84. type: 'field',
  85. outboundTag: 'proxy',
  86. network: 'tcp,udp',
  87. });
  88. const deeplink = 'happ://routing/onadd/' + toBase64Utf8(JSON.stringify({ rules }));
  89. updateSetting({ subRoutingRules: deeplink });
  90. setIsModalOpen(false);
  91. message.success(t('pages.settings.subHappDeeplinkGenerated'));
  92. };
  93. return (
  94. <>
  95. <Tabs
  96. type="card"
  97. size="small"
  98. items={[
  99. {
  100. key: 'routing',
  101. label: (
  102. <span>
  103. <BranchesOutlined /> {!isMobile && t('pages.settings.subHappGroupRouting')}
  104. </span>
  105. ),
  106. children: (
  107. <>
  108. <SettingListItem
  109. paddings="small"
  110. title={t('pages.settings.subEnableRouting')}
  111. description={t('pages.settings.subEnableRoutingDesc')}
  112. >
  113. <Switch
  114. checked={allSetting.subEnableRouting}
  115. onChange={(v) => updateSetting({ subEnableRouting: v })}
  116. />
  117. </SettingListItem>
  118. <SettingListItem
  119. paddings="small"
  120. title={t('pages.settings.subHappPresets')}
  121. description={t('pages.settings.subHappPresetsDesc')}
  122. >
  123. <Space orientation="horizontal" style={{ width: '100%' }}>
  124. <Select
  125. value={selectedPreset}
  126. style={{ minWidth: 170 }}
  127. onChange={setSelectedPreset}
  128. options={[
  129. { value: 'iran-bypass', label: t('pages.settings.subHappPresetIran') },
  130. { value: 'china-direct', label: t('pages.settings.subHappPresetChina') },
  131. { value: 'adblock', label: t('pages.settings.subHappPresetAdblock') },
  132. { value: 'global', label: t('pages.settings.subHappPresetGlobal') },
  133. { value: 'off', label: t('pages.settings.subHappPresetOff') },
  134. ]}
  135. />
  136. <Button type="primary" onClick={applyPreset}>
  137. {t('pages.settings.subHappPresets')}
  138. </Button>
  139. </Space>
  140. </SettingListItem>
  141. <SettingListItem
  142. paddings="small"
  143. title={t('pages.settings.subHappVisualBuilder')}
  144. description={t('pages.settings.subHappVisualBuilderDesc')}
  145. >
  146. <Button icon={<BuildOutlined />} onClick={() => setIsModalOpen(true)}>
  147. {t('pages.settings.subHappVisualBuilder')}
  148. </Button>
  149. </SettingListItem>
  150. <SettingListItem
  151. paddings="small"
  152. title={t('pages.settings.subRoutingRules')}
  153. badge={remoteSourceBadge(allSetting.subRoutingRules)}
  154. description={t('pages.settings.subRoutingRulesDesc')}
  155. >
  156. <Input.TextArea
  157. value={allSetting.subRoutingRules}
  158. rows={4}
  159. placeholder="happ://routing/onadd/... or https://.../DEFAULT.DEEPLINK"
  160. onChange={(e) => updateSetting({ subRoutingRules: e.target.value })}
  161. />
  162. </SettingListItem>
  163. <SettingListItem
  164. paddings="small"
  165. title={t('pages.settings.subHappNoLimit')}
  166. description={t('pages.settings.subHappNoLimitDesc')}
  167. >
  168. <Switch
  169. checked={allSetting.subHappNoLimit}
  170. onChange={(v) => updateSetting({ subHappNoLimit: v })}
  171. />
  172. </SettingListItem>
  173. <SettingListItem
  174. paddings="small"
  175. title={t('pages.settings.subHideSettings')}
  176. description={t('pages.settings.subHideSettingsDesc')}
  177. >
  178. <Switch
  179. checked={allSetting.subHideSettings}
  180. onChange={(v) => updateSetting({ subHideSettings: v })}
  181. />
  182. </SettingListItem>
  183. </>
  184. ),
  185. },
  186. {
  187. key: 'banners',
  188. label: (
  189. <span>
  190. <NotificationOutlined /> {!isMobile && t('pages.settings.subHappGroupBanners')}
  191. </span>
  192. ),
  193. children: (
  194. <>
  195. <SettingListItem
  196. paddings="small"
  197. title={t('pages.settings.subHappSubInfoText')}
  198. description={t('pages.settings.subHappSubInfoTextDesc')}
  199. >
  200. <Input
  201. value={allSetting.subHappSubInfoText}
  202. maxLength={200}
  203. placeholder="Welcome to our high-speed network!"
  204. onChange={(e) => updateSetting({ subHappSubInfoText: e.target.value })}
  205. />
  206. </SettingListItem>
  207. <SettingListItem
  208. paddings="small"
  209. title={t('pages.settings.subHappSubInfoColor')}
  210. description={t('pages.settings.subHappSubInfoColorDesc')}
  211. >
  212. <Select
  213. value={allSetting.subHappSubInfoColor || 'blue'}
  214. style={{ width: '100%' }}
  215. onChange={(v) => updateSetting({ subHappSubInfoColor: v })}
  216. options={[
  217. { value: 'blue', label: t('pages.settings.subHappColorBlue') },
  218. { value: 'green', label: t('pages.settings.subHappColorGreen') },
  219. { value: 'red', label: t('pages.settings.subHappColorRed') },
  220. ]}
  221. />
  222. </SettingListItem>
  223. <SettingListItem
  224. paddings="small"
  225. title={t('pages.settings.subHappSubInfoButtonText')}
  226. description={t('pages.settings.subHappSubInfoButtonTextDesc')}
  227. >
  228. <Input
  229. value={allSetting.subHappSubInfoButtonText}
  230. maxLength={25}
  231. placeholder="Support Channel"
  232. onChange={(e) => updateSetting({ subHappSubInfoButtonText: e.target.value })}
  233. />
  234. </SettingListItem>
  235. <SettingListItem
  236. paddings="small"
  237. title={t('pages.settings.subHappSubInfoButtonLink')}
  238. description={t('pages.settings.subHappSubInfoButtonLinkDesc')}
  239. >
  240. <Input
  241. value={allSetting.subHappSubInfoButtonLink}
  242. placeholder="https://t.me/your_channel"
  243. onChange={(e) => updateSetting({ subHappSubInfoButtonLink: e.target.value })}
  244. />
  245. </SettingListItem>
  246. <SettingListItem
  247. paddings="small"
  248. title={t('pages.settings.subHappSubExpire')}
  249. description={t('pages.settings.subHappSubExpireDesc')}
  250. >
  251. <Switch
  252. checked={allSetting.subHappSubExpire}
  253. onChange={(v) => updateSetting({ subHappSubExpire: v })}
  254. />
  255. </SettingListItem>
  256. <SettingListItem
  257. paddings="small"
  258. title={t('pages.settings.subHappSubExpireButtonLink')}
  259. description={t('pages.settings.subHappSubExpireButtonLinkDesc')}
  260. >
  261. <Input
  262. value={allSetting.subHappSubExpireButtonLink}
  263. placeholder="https://example.com/renew"
  264. onChange={(e) => updateSetting({ subHappSubExpireButtonLink: e.target.value })}
  265. />
  266. </SettingListItem>
  267. <SettingListItem
  268. paddings="small"
  269. title={t('pages.settings.subHappNotificationExpire')}
  270. description={t('pages.settings.subHappNotificationExpireDesc')}
  271. >
  272. <Switch
  273. checked={allSetting.subHappNotificationExpire}
  274. onChange={(v) => updateSetting({ subHappNotificationExpire: v })}
  275. />
  276. </SettingListItem>
  277. </>
  278. ),
  279. },
  280. {
  281. key: 'network',
  282. label: (
  283. <span>
  284. <ThunderboltOutlined /> {!isMobile && t('pages.settings.subHappGroupNetwork')}
  285. </span>
  286. ),
  287. children: (
  288. <>
  289. <SettingListItem
  290. paddings="small"
  291. title={t('pages.settings.subHappTunMode')}
  292. description={t('pages.settings.subHappTunModeDesc')}
  293. >
  294. <Select
  295. value={allSetting.subHappTunMode}
  296. style={{ width: '100%' }}
  297. onChange={(v) => updateSetting({ subHappTunMode: v })}
  298. options={[
  299. // happ.su documents tun-mode as system|gvisor only, so
  300. // Default is the unset state rather than a third value.
  301. { value: '', label: t('pages.settings.subHappTunModeDefault') },
  302. { value: 'system', label: t('pages.settings.subHappTunModeSystem') },
  303. { value: 'gvisor', label: t('pages.settings.subHappTunModeGvisor') },
  304. ]}
  305. />
  306. </SettingListItem>
  307. <SettingListItem
  308. paddings="small"
  309. title={t('pages.settings.subHappTunType')}
  310. description={t('pages.settings.subHappTunTypeDesc')}
  311. >
  312. <Select
  313. value={allSetting.subHappTunType || 'default'}
  314. style={{ width: '100%' }}
  315. onChange={(v) => updateSetting({ subHappTunType: v })}
  316. options={[
  317. { value: 'singbox', label: t('pages.settings.subHappTunTypeSingbox') },
  318. { value: 'tun2proxy', label: t('pages.settings.subHappTunTypeTun2proxy') },
  319. { value: 'default', label: t('pages.settings.subHappTunTypeDefault') },
  320. { value: 'xray', label: t('pages.settings.subHappTunTypeXray') },
  321. ]}
  322. />
  323. </SettingListItem>
  324. <SettingListItem
  325. paddings="small"
  326. title={t('pages.settings.subHappExcludeRoutes')}
  327. description={t('pages.settings.subHappExcludeRoutesDesc')}
  328. >
  329. <Input
  330. value={allSetting.subHappExcludeRoutes}
  331. placeholder="192.168.0.0/16, 10.0.0.0/8"
  332. onChange={(e) => updateSetting({ subHappExcludeRoutes: e.target.value })}
  333. />
  334. </SettingListItem>
  335. <SettingListItem
  336. paddings="small"
  337. title={t('pages.settings.subHappExcludeApns')}
  338. description={t('pages.settings.subHappExcludeApnsDesc')}
  339. >
  340. <Switch
  341. checked={allSetting.subHappExcludeApns}
  342. onChange={(v) => updateSetting({ subHappExcludeApns: v })}
  343. />
  344. </SettingListItem>
  345. <SettingListItem
  346. paddings="small"
  347. title={t('pages.settings.subHappPingType')}
  348. description={t('pages.settings.subHappPingTypeDesc')}
  349. >
  350. <Select
  351. value={allSetting.subHappPingType || 'proxy'}
  352. style={{ width: '100%' }}
  353. onChange={(v) => updateSetting({ subHappPingType: v })}
  354. options={[
  355. { value: 'proxy', label: t('pages.settings.subHappPingProxy') },
  356. { value: 'proxy-head', label: t('pages.settings.subHappPingProxyHead') },
  357. { value: 'tcp', label: t('pages.settings.subHappPingTcp') },
  358. { value: 'icmp', label: t('pages.settings.subHappPingIcmp') },
  359. ]}
  360. />
  361. </SettingListItem>
  362. <SettingListItem
  363. paddings="small"
  364. title={t('pages.settings.subHappAutoConnect')}
  365. description={t('pages.settings.subHappAutoConnectDesc')}
  366. >
  367. <Switch
  368. checked={allSetting.subHappAutoConnect}
  369. onChange={(v) => updateSetting({ subHappAutoConnect: v })}
  370. />
  371. </SettingListItem>
  372. <SettingListItem
  373. paddings="small"
  374. title={t('pages.settings.subHappAutoConnectType')}
  375. description={t('pages.settings.subHappAutoConnectTypeDesc')}
  376. >
  377. <Select
  378. value={allSetting.subHappAutoConnectType || 'lowestdelay'}
  379. style={{ width: '100%' }}
  380. onChange={(v) => updateSetting({ subHappAutoConnectType: v })}
  381. options={[
  382. {
  383. value: 'lowestdelay',
  384. label: t('pages.settings.subHappAutoConnectLowestDelay'),
  385. },
  386. { value: 'lastused', label: t('pages.settings.subHappAutoConnectLastUsed') },
  387. { value: 'random', label: t('pages.settings.subHappAutoConnectRandom') },
  388. ]}
  389. />
  390. </SettingListItem>
  391. </>
  392. ),
  393. },
  394. {
  395. key: 'themes',
  396. label: (
  397. <span>
  398. <DesktopOutlined /> {!isMobile && t('pages.settings.subHappGroupThemes')}
  399. </span>
  400. ),
  401. children: (
  402. <>
  403. <SettingListItem
  404. paddings="small"
  405. title={t('pages.settings.subHappColorProfile')}
  406. description={t('pages.settings.subHappColorProfileDesc')}
  407. >
  408. <Input
  409. value={allSetting.subHappColorProfile}
  410. placeholder="default, violet, turquoise, cyberpunk, or custom JSON"
  411. onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
  412. />
  413. </SettingListItem>
  414. </>
  415. ),
  416. },
  417. {
  418. key: 'failover',
  419. label: (
  420. <span>
  421. <CloudSyncOutlined /> {!isMobile && t('pages.settings.subHappGroupFailover')}
  422. </span>
  423. ),
  424. children: (
  425. <>
  426. <SettingListItem
  427. paddings="small"
  428. title={t('pages.settings.subHappAutoDetect')}
  429. description={t('pages.settings.subHappAutoDetectDesc')}
  430. >
  431. <Switch
  432. checked={allSetting.subHappAutoDetect}
  433. onChange={(v) => updateSetting({ subHappAutoDetect: v })}
  434. />
  435. </SettingListItem>
  436. <SettingListItem
  437. paddings="small"
  438. title={t('pages.settings.subHappProviderId')}
  439. description={t('pages.settings.subHappProviderIdDesc')}
  440. >
  441. <Input
  442. value={allSetting.subHappProviderId}
  443. placeholder="my-happ-provider"
  444. onChange={(e) => updateSetting({ subHappProviderId: e.target.value })}
  445. />
  446. </SettingListItem>
  447. <SettingListItem
  448. paddings="small"
  449. title={t('pages.settings.subHappNewUrl')}
  450. description={t('pages.settings.subHappNewUrlDesc')}
  451. >
  452. <Input
  453. value={allSetting.subHappNewUrl}
  454. placeholder="https://new-domain.com/sub/..."
  455. onChange={(e) => updateSetting({ subHappNewUrl: e.target.value })}
  456. />
  457. </SettingListItem>
  458. <SettingListItem
  459. paddings="small"
  460. title={t('pages.settings.subHappFallbackUrl')}
  461. description={t('pages.settings.subHappFallbackUrlDesc')}
  462. >
  463. <Input
  464. value={allSetting.subHappFallbackUrl}
  465. placeholder="https://backup-domain.com/sub/..."
  466. onChange={(e) => updateSetting({ subHappFallbackUrl: e.target.value })}
  467. />
  468. </SettingListItem>
  469. <SettingListItem
  470. paddings="small"
  471. title={t('pages.settings.subHappAlwaysHwid')}
  472. description={t('pages.settings.subHappAlwaysHwidDesc')}
  473. >
  474. <Switch
  475. checked={allSetting.subHappAlwaysHwid}
  476. onChange={(v) => updateSetting({ subHappAlwaysHwid: v })}
  477. />
  478. </SettingListItem>
  479. </>
  480. ),
  481. },
  482. {
  483. key: 'android',
  484. label: (
  485. <span>
  486. <MobileOutlined /> {!isMobile && t('pages.settings.subHappGroupAndroid')}
  487. </span>
  488. ),
  489. children: (
  490. <>
  491. <SettingListItem
  492. paddings="small"
  493. title={t('pages.settings.subHappPerAppMode')}
  494. description={t('pages.settings.subHappPerAppModeDesc')}
  495. >
  496. <Select
  497. value={allSetting.subHappPerAppMode || 'off'}
  498. style={{ width: '100%' }}
  499. onChange={(v) => updateSetting({ subHappPerAppMode: v })}
  500. options={[
  501. { value: 'off', label: t('pages.settings.subHappPerAppOff') },
  502. { value: 'on', label: t('pages.settings.subHappPerAppOn') },
  503. { value: 'bypass', label: t('pages.settings.subHappPerAppBypass') },
  504. ]}
  505. />
  506. </SettingListItem>
  507. <SettingListItem
  508. paddings="small"
  509. title={t('pages.settings.subHappPerAppList')}
  510. description={t('pages.settings.subHappPerAppListDesc')}
  511. >
  512. <Input.TextArea
  513. value={allSetting.subHappPerAppList}
  514. rows={4}
  515. placeholder="org.telegram.messenger, com.google.android.youtube"
  516. onChange={(e) => updateSetting({ subHappPerAppList: e.target.value })}
  517. />
  518. </SettingListItem>
  519. </>
  520. ),
  521. },
  522. ]}
  523. />
  524. <Modal
  525. title={t('pages.settings.subHappModalTitle')}
  526. open={isModalOpen}
  527. onCancel={() => setIsModalOpen(false)}
  528. onOk={handleBuildDeeplink}
  529. okText={t('pages.settings.subHappBuildDeeplink')}
  530. width={650}
  531. >
  532. <Space direction="vertical" style={{ width: '100%', marginTop: 12 }} size="middle">
  533. <div>
  534. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  535. {t('pages.settings.subHappDirectDomains')}
  536. </div>
  537. <Input.TextArea
  538. rows={2}
  539. value={directDomains}
  540. placeholder="domain:ir, domain:cn, example.local"
  541. onChange={(e) => setDirectDomains(e.target.value)}
  542. />
  543. </div>
  544. <div>
  545. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  546. {t('pages.settings.subHappProxyDomains')}
  547. </div>
  548. <Input.TextArea
  549. rows={2}
  550. value={proxyDomains}
  551. placeholder="geosite:google, youtube.com"
  552. onChange={(e) => setProxyDomains(e.target.value)}
  553. />
  554. </div>
  555. <div>
  556. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  557. {t('pages.settings.subHappBlockDomains')}
  558. </div>
  559. <Input.TextArea
  560. rows={2}
  561. value={blockDomains}
  562. placeholder="geosite:category-ads-all, analytics.google.com"
  563. onChange={(e) => setBlockDomains(e.target.value)}
  564. />
  565. </div>
  566. <div>
  567. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  568. {t('pages.settings.subHappDirectIPs')}
  569. </div>
  570. <Input.TextArea
  571. rows={2}
  572. value={directIPs}
  573. placeholder="geoip:ir, 192.168.0.0/16, 10.0.0.0/8"
  574. onChange={(e) => setDirectIPs(e.target.value)}
  575. />
  576. </div>
  577. <div>
  578. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  579. {t('pages.settings.subHappProxyIPs')}
  580. </div>
  581. <Input.TextArea
  582. rows={2}
  583. value={proxyIPs}
  584. placeholder="1.1.1.1/32, 8.8.8.8/32"
  585. onChange={(e) => setProxyIPs(e.target.value)}
  586. />
  587. </div>
  588. <div>
  589. <div style={{ fontWeight: 600, marginBottom: 4 }}>
  590. {t('pages.settings.subHappBlockIPs')}
  591. </div>
  592. <Input.TextArea
  593. rows={2}
  594. value={blockIPs}
  595. placeholder="geoip:phishing, 0.0.0.0/8"
  596. onChange={(e) => setBlockIPs(e.target.value)}
  597. />
  598. </div>
  599. </Space>
  600. </Modal>
  601. </>
  602. );
  603. }