quick.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import { useState, useMemo, useRef, useEffect } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { Virtuoso } from "react-virtuoso";
  4. import {
  5. getRules,
  6. getClashConfig,
  7. closeAllConnections,
  8. updateConfigs,
  9. } from "@/services/api";
  10. import { BaseEmpty, BasePage, Notice } from "@/components/base";
  11. import RuleItem from "@/components/rule/rule-item";
  12. import { ProviderButton } from "@/components/rule/provider-button";
  13. import { useCustomTheme } from "@/components/layout/use-custom-theme";
  14. import { BaseSearchBox } from "@/components/base/base-search-box";
  15. import {
  16. Box,
  17. Button,
  18. Grid,
  19. IconButton,
  20. Stack,
  21. ButtonGroup,
  22. } from "@mui/material";
  23. import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
  24. import { readText } from "@tauri-apps/api/clipboard";
  25. import {
  26. ClearRounded,
  27. ContentPasteRounded,
  28. LocalFireDepartmentRounded,
  29. RefreshRounded,
  30. TextSnippetOutlined,
  31. } from "@mui/icons-material";
  32. import { LoadingButton } from "@mui/lab";
  33. import {
  34. ProfileViewer,
  35. ProfileViewerRef,
  36. } from "@/components/profile/profile-viewer";
  37. import {
  38. getProfiles,
  39. importProfile,
  40. enhanceProfiles,
  41. getRuntimeLogs,
  42. deleteProfile,
  43. updateProfile,
  44. reorderProfile,
  45. createProfile,
  46. } from "@/services/cmds";
  47. import useSWR, { mutate } from "swr";
  48. import { useProfiles } from "@/hooks/use-profiles";
  49. import { ProxyGroups } from "@/components/proxy/proxy-groups";
  50. import { useVerge } from "@/hooks/use-verge";
  51. import { useLockFn } from "ahooks";
  52. import getSystem from "@/utils/get-system";
  53. import {
  54. installService,
  55. uninstallService,
  56. checkService,
  57. patchClashConfig,
  58. } from "@/services/cmds";
  59. import {
  60. DndContext,
  61. closestCenter,
  62. KeyboardSensor,
  63. PointerSensor,
  64. useSensor,
  65. useSensors,
  66. DragEndEvent,
  67. } from "@dnd-kit/core";
  68. import {
  69. SortableContext,
  70. sortableKeyboardCoordinates,
  71. } from "@dnd-kit/sortable";
  72. import { ProfileItem } from "@/components/profile/profile-item";
  73. import { ProfileMore } from "@/components/profile/profile-more";
  74. import { useSetLoadingCache, useThemeMode } from "@/services/states";
  75. const QuickPage = () => {
  76. const { t } = useTranslation();
  77. const { data = [] } = useSWR("getRules", getRules);
  78. const { theme } = useCustomTheme();
  79. const isDark = theme.palette.mode === "dark";
  80. const [match, setMatch] = useState(() => (_: string) => true);
  81. const rules = useMemo(() => {
  82. return data.filter((item) => match(item.payload));
  83. }, [data, match]);
  84. const [url, setUrl] = useState("");
  85. const [disabled, setDisabled] = useState(false);
  86. const [activatings, setActivatings] = useState<string[]>([]);
  87. const [loading, setLoading] = useState(false);
  88. const onCopyLink = async () => {
  89. const text = await readText();
  90. if (text) setUrl(text);
  91. };
  92. const {
  93. profiles = {},
  94. activateSelected,
  95. patchProfiles,
  96. mutateProfiles,
  97. } = useProfiles();
  98. const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
  99. "getRuntimeLogs",
  100. getRuntimeLogs
  101. );
  102. const onImport = async () => {
  103. if (!url) return;
  104. setLoading(true);
  105. try {
  106. await importProfile(url);
  107. Notice.success(t("Profile Imported Successfully"));
  108. setUrl("");
  109. setLoading(false);
  110. getProfiles().then(async (newProfiles) => {
  111. mutate("getProfiles", newProfiles);
  112. const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
  113. if (newProfiles.current && remoteItem) {
  114. const current = remoteItem.uid;
  115. await patchProfiles({ current });
  116. mutateLogs();
  117. setTimeout(() => activateSelected(), 2000);
  118. }
  119. });
  120. } catch (err: any) {
  121. Notice.error(err.message || err.toString());
  122. setLoading(false);
  123. } finally {
  124. setDisabled(false);
  125. setLoading(false);
  126. }
  127. };
  128. const viewerRef = useRef<ProfileViewerRef>(null);
  129. const { data: clashConfig, mutate: mutateClash } = useSWR(
  130. "getClashConfig",
  131. getClashConfig
  132. );
  133. const curMode = clashConfig?.mode?.toLowerCase();
  134. const sensors = useSensors(
  135. useSensor(PointerSensor),
  136. useSensor(KeyboardSensor, {
  137. coordinateGetter: sortableKeyboardCoordinates,
  138. })
  139. );
  140. const onDragEnd = async (event: DragEndEvent) => {
  141. const { active, over } = event;
  142. if (over) {
  143. if (active.id !== over.id) {
  144. await reorderProfile(active.id.toString(), over.id.toString());
  145. mutateProfiles();
  146. }
  147. }
  148. };
  149. const profileItems = useMemo(() => {
  150. const items = profiles.items || [];
  151. const type1 = ["local", "remote"];
  152. const profileItems = items.filter((i) => i && type1.includes(i.type!));
  153. return profileItems;
  154. }, [profiles]);
  155. const isEmpty = profileItems.length === 0;
  156. const currentActivatings = () => {
  157. return [...new Set([profiles.current ?? ""])].filter(Boolean);
  158. };
  159. const onSelect = useLockFn(async (current: string, force: boolean) => {
  160. if (!force && current === profiles.current) return;
  161. // 避免大多数情况下loading态闪烁
  162. const reset = setTimeout(() => {
  163. setActivatings([...currentActivatings(), current]);
  164. }, 100);
  165. try {
  166. await patchProfiles({ current });
  167. await mutateLogs();
  168. closeAllConnections();
  169. activateSelected().then(() => {
  170. Notice.success(t("Profile Switched"), 1000);
  171. });
  172. } catch (err: any) {
  173. Notice.error(err?.message || err.toString(), 4000);
  174. } finally {
  175. clearTimeout(reset);
  176. setActivatings([]);
  177. }
  178. });
  179. const onEnhance = useLockFn(async () => {
  180. setActivatings(currentActivatings());
  181. try {
  182. await enhanceProfiles();
  183. mutateLogs();
  184. Notice.success(t("Profile Reactivated"), 1000);
  185. } catch (err: any) {
  186. Notice.error(err.message || err.toString(), 3000);
  187. } finally {
  188. setActivatings([]);
  189. }
  190. });
  191. const onDelete = useLockFn(async (uid: string) => {
  192. const current = profiles.current === uid;
  193. try {
  194. setActivatings([...(current ? currentActivatings() : []), uid]);
  195. await deleteProfile(uid);
  196. mutateProfiles();
  197. mutateLogs();
  198. current && (await onEnhance());
  199. } catch (err: any) {
  200. Notice.error(err?.message || err.toString());
  201. } finally {
  202. setActivatings([]);
  203. }
  204. });
  205. const mode = useThemeMode();
  206. const islight = mode === "light" ? true : false;
  207. const dividercolor = islight
  208. ? "rgba(0, 0, 0, 0.06)"
  209. : "rgba(255, 255, 255, 0.06)";
  210. const { verge, mutateVerge, patchVerge } = useVerge();
  211. const onChangeData = (patch: Partial<IVergeConfig>) => {
  212. mutateVerge({ ...verge, ...patch }, false);
  213. };
  214. const { data: serviceStatus, mutate: themutate } = useSWR(
  215. "checkService",
  216. checkService,
  217. {
  218. revalidateIfStale: false,
  219. shouldRetryOnError: false,
  220. focusThrottleInterval: 36e5, // 1 hour
  221. }
  222. );
  223. const isWindows = getSystem() === "windows";
  224. const isActive = serviceStatus === "active";
  225. const isInstalled = serviceStatus === "installed";
  226. const isUninstall =
  227. serviceStatus === "uninstall" || serviceStatus === "unknown";
  228. const [serviceLoading, setServiceLoading] = useState(false);
  229. const [openInstall, setOpenInstall] = useState(false);
  230. const [openUninstall, setOpenUninstall] = useState(false);
  231. const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
  232. // const mutate = 'active';
  233. async function install(passwd: string) {
  234. try {
  235. setOpenInstall(false);
  236. await installService(passwd);
  237. await themutate();
  238. setTimeout(() => {
  239. themutate();
  240. }, 2000);
  241. Notice.success(t("Service Installed Successfully"));
  242. setServiceLoading(false);
  243. } catch (err: any) {
  244. await themutate();
  245. setTimeout(() => {
  246. themutate();
  247. }, 2000);
  248. Notice.error(err.message || err.toString());
  249. setServiceLoading(false);
  250. }
  251. }
  252. const onInstallOrEnableService = useLockFn(async () => {
  253. setServiceLoading(true);
  254. if (isUninstall) {
  255. // install service
  256. if (isWindows) {
  257. await install("");
  258. } else {
  259. setOpenInstall(true);
  260. }
  261. } else {
  262. try {
  263. // enable or disable service
  264. await patchVerge({ enable_service_mode: !isActive });
  265. onChangeData({ enable_service_mode: !isActive });
  266. await themutate();
  267. setTimeout(() => {
  268. themutate();
  269. }, 2000);
  270. setServiceLoading(false);
  271. } catch (err: any) {
  272. await themutate();
  273. Notice.error(err.message || err.toString());
  274. setServiceLoading(false);
  275. }
  276. }
  277. });
  278. async function uninstall(passwd: string) {
  279. try {
  280. setOpenUninstall(false);
  281. await uninstallService(passwd);
  282. await themutate();
  283. setTimeout(() => {
  284. themutate();
  285. }, 2000);
  286. Notice.success(t("Service Uninstalled Successfully"));
  287. setUninstallServiceLoading(false);
  288. } catch (err: any) {
  289. await themutate();
  290. setTimeout(() => {
  291. themutate();
  292. }, 2000);
  293. Notice.error(err.message || err.toString());
  294. setUninstallServiceLoading(false);
  295. }
  296. }
  297. const onUninstallService = useLockFn(async () => {
  298. setUninstallServiceLoading(true);
  299. if (isWindows) {
  300. await uninstall("");
  301. } else {
  302. setOpenUninstall(true);
  303. }
  304. });
  305. const [result, setResult] = useState(false);
  306. useEffect(() => {
  307. if (data) {
  308. const { enable_tun_mode, enable_system_proxy, enable_service_mode } =
  309. verge ?? {};
  310. // 当 enable_tun_mode, enable_system_proxy, enable_service_mode 同为 true 时,result 为 true
  311. // 否则,result 为 false
  312. // setResult(enable_tun_mode && enable_system_proxy && enable_service_mode);
  313. setResult(
  314. !!(enable_tun_mode && enable_system_proxy && enable_service_mode)
  315. );
  316. }
  317. }, [data]);
  318. const link = async () => {
  319. if (!isEmpty) {
  320. onInstallOrEnableService();
  321. await patchVerge({ enable_service_mode: true });
  322. onChangeData({ enable_service_mode: true });
  323. onChangeData({ enable_tun_mode: true });
  324. patchVerge({ enable_tun_mode: true });
  325. onChangeData({ enable_system_proxy: true });
  326. patchVerge({ enable_system_proxy: true });
  327. setResult(true);
  328. } else {
  329. Notice.error(t("Profiles Null"));
  330. }
  331. };
  332. const cancelink = async () => {
  333. await patchVerge({ enable_service_mode: false });
  334. onChangeData({ enable_service_mode: false });
  335. onChangeData({ enable_tun_mode: false });
  336. patchVerge({ enable_tun_mode: false });
  337. onChangeData({ enable_system_proxy: false });
  338. patchVerge({ enable_system_proxy: false });
  339. setResult(false);
  340. };
  341. const modeList = ["rule", "global", "direct"];
  342. const onChangeMode = useLockFn(async (mode: string) => {
  343. // 断开连接
  344. if (mode !== curMode && verge?.auto_close_connection) {
  345. closeAllConnections();
  346. }
  347. await updateConfigs({ mode });
  348. await patchClashConfig({ mode });
  349. mutateClash();
  350. });
  351. useEffect(() => {
  352. if (curMode && !modeList.includes(curMode)) {
  353. onChangeMode("rule");
  354. }
  355. }, [curMode]);
  356. return (
  357. <BasePage
  358. title={t("Quick")}
  359. contentStyle={{ height: "100%" }}
  360. header={
  361. <Box display="flex" alignItems="center" gap={1}>
  362. <ProviderButton />
  363. <ButtonGroup size="small">
  364. {modeList.map((mode) => (
  365. <Button
  366. key={mode}
  367. variant={mode === curMode ? "contained" : "outlined"}
  368. onClick={() => onChangeMode(mode)}
  369. sx={{ textTransform: "capitalize" }}
  370. >
  371. {t(mode)}
  372. </Button>
  373. ))}
  374. </ButtonGroup>
  375. </Box>
  376. }
  377. >
  378. <div className="quickCon">
  379. {result ? (
  380. <div className="aquickCon1">
  381. <div className="aquickCon2">
  382. <div className="aquick" onClick={cancelink}>
  383. {t("Close Connection")}
  384. </div>
  385. </div>
  386. </div>
  387. ) : (
  388. <div className="quickCon1">
  389. <div className="quickCon2">
  390. <div className="quick" onClick={link}>
  391. {t("Quick Connection")}
  392. </div>
  393. </div>
  394. </div>
  395. )}
  396. </div>
  397. <Stack
  398. direction="row"
  399. spacing={1}
  400. sx={{
  401. pt: 1,
  402. mb: 0.5,
  403. mx: "10px",
  404. height: "36px",
  405. display: "flex",
  406. alignItems: "center",
  407. }}
  408. >
  409. <BaseStyledTextField
  410. value={url}
  411. variant="outlined"
  412. onChange={(e) => setUrl(e.target.value)}
  413. placeholder={t("Profile URL")}
  414. InputProps={{
  415. sx: { pr: 1 },
  416. endAdornment: !url ? (
  417. <IconButton
  418. size="small"
  419. sx={{ p: 0.5 }}
  420. title={t("Paste")}
  421. onClick={onCopyLink}
  422. >
  423. <ContentPasteRounded fontSize="inherit" />
  424. </IconButton>
  425. ) : (
  426. <IconButton
  427. size="small"
  428. sx={{ p: 0.5 }}
  429. title={t("Clear")}
  430. onClick={() => setUrl("")}
  431. >
  432. <ClearRounded fontSize="inherit" />
  433. </IconButton>
  434. ),
  435. }}
  436. />
  437. <LoadingButton
  438. disabled={!url || disabled}
  439. loading={loading}
  440. variant="contained"
  441. size="small"
  442. sx={{ borderRadius: "6px" }}
  443. onClick={onImport}
  444. >
  445. {t("Import")}
  446. </LoadingButton>
  447. </Stack>
  448. <Box
  449. sx={{
  450. pt: 1,
  451. mb: 0.5,
  452. pl: "10px",
  453. mr: "10px",
  454. overflowY: "auto",
  455. }}
  456. >
  457. <DndContext
  458. sensors={sensors}
  459. collisionDetection={closestCenter}
  460. onDragEnd={onDragEnd}
  461. >
  462. <Box sx={{ mb: 1.5 }}>
  463. <Grid container spacing={{ xs: 1, lg: 1 }}>
  464. <SortableContext
  465. items={profileItems.map((x) => {
  466. return x.uid;
  467. })}
  468. >
  469. {profileItems.map((item) => (
  470. <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
  471. <ProfileItem
  472. id={item.uid}
  473. selected={profiles.current === item.uid}
  474. activating={activatings.includes(item.uid)}
  475. itemData={item}
  476. onSelect={(f) => onSelect(item.uid, f)}
  477. onEdit={() => viewerRef.current?.edit(item)}
  478. onSave={async (prev, curr) => {
  479. if (prev !== curr && profiles.current === item.uid) {
  480. await onEnhance();
  481. }
  482. }}
  483. onDelete={() => onDelete(item.uid)}
  484. />
  485. </Grid>
  486. ))}
  487. </SortableContext>
  488. </Grid>
  489. </Box>
  490. </DndContext>
  491. </Box>
  492. <ProxyGroups mode={curMode!} />
  493. </BasePage>
  494. );
  495. };
  496. export default QuickPage;