profiles.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import useSWR, { useSWRConfig } from "swr";
  2. import { useLockFn } from "ahooks";
  3. import { useEffect, useMemo, useState } from "react";
  4. import { useSetRecoilState } from "recoil";
  5. import { Box, Button, Grid, TextField } from "@mui/material";
  6. import { useTranslation } from "react-i18next";
  7. import {
  8. getProfiles,
  9. patchProfile,
  10. selectProfile,
  11. importProfile,
  12. } from "../services/cmds";
  13. import { getProxies, updateProxy } from "../services/api";
  14. import { atomCurrentProfile } from "../services/states";
  15. import Notice from "../components/base/base-notice";
  16. import BasePage from "../components/base/base-page";
  17. import ProfileNew from "../components/profile/profile-new";
  18. import ProfileItem from "../components/profile/profile-item";
  19. import EnhancedMode from "../components/profile/enhanced";
  20. const ProfilePage = () => {
  21. const { t } = useTranslation();
  22. const { mutate } = useSWRConfig();
  23. const [url, setUrl] = useState("");
  24. const [disabled, setDisabled] = useState(false);
  25. const [dialogOpen, setDialogOpen] = useState(false);
  26. const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
  27. const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
  28. // distinguish type
  29. const { regularItems, enhanceItems } = useMemo(() => {
  30. const items = profiles.items || [];
  31. const chain = profiles.chain || [];
  32. const type1 = ["local", "remote"];
  33. const type2 = ["merge", "script"];
  34. const regularItems = items.filter((i) => type1.includes(i.type!));
  35. const restItems = items.filter((i) => type2.includes(i.type!));
  36. const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
  37. const enhanceItems = chain
  38. .map((i) => restMap[i]!)
  39. .concat(restItems.filter((i) => !chain.includes(i.uid)));
  40. return { regularItems, enhanceItems };
  41. }, [profiles]);
  42. // sync selected proxy
  43. useEffect(() => {
  44. if (profiles.current == null) return;
  45. const current = profiles.current;
  46. const profile = regularItems.find((p) => p.uid === current);
  47. setCurrentProfile(current);
  48. if (!profile) return;
  49. setTimeout(async () => {
  50. const proxiesData = await getProxies();
  51. mutate("getProxies", proxiesData);
  52. // init selected array
  53. const { selected = [] } = profile;
  54. const selectedMap = Object.fromEntries(
  55. selected.map((each) => [each.name!, each.now!])
  56. );
  57. let hasChange = false;
  58. const { global, groups } = proxiesData;
  59. [global, ...groups].forEach((group) => {
  60. const { name, now } = group;
  61. if (!now || selectedMap[name] === now) return;
  62. if (selectedMap[name] == null) {
  63. selectedMap[name] = now!;
  64. } else {
  65. hasChange = true;
  66. updateProxy(name, selectedMap[name]);
  67. }
  68. });
  69. // update profile selected list
  70. profile.selected = Object.entries(selectedMap).map(([name, now]) => ({
  71. name,
  72. now,
  73. }));
  74. patchProfile(current!, { selected: profile.selected });
  75. // update proxies cache
  76. if (hasChange) mutate("getProxies", getProxies());
  77. }, 100);
  78. }, [profiles, regularItems]);
  79. const onImport = async () => {
  80. if (!url) return;
  81. setUrl("");
  82. setDisabled(true);
  83. try {
  84. await importProfile(url);
  85. Notice.success("Successfully import profile.");
  86. getProfiles().then((newProfiles) => {
  87. mutate("getProfiles", newProfiles);
  88. if (!newProfiles.current && newProfiles.items?.length) {
  89. const current = newProfiles.items[0].uid;
  90. selectProfile(current);
  91. mutate("getProfiles", { ...newProfiles, current }, true);
  92. }
  93. });
  94. } catch {
  95. Notice.error("Failed to import profile.");
  96. } finally {
  97. setDisabled(false);
  98. }
  99. };
  100. const onSelect = useLockFn(async (uid: string, force: boolean) => {
  101. if (!force && uid === profiles.current) return;
  102. try {
  103. await selectProfile(uid);
  104. setCurrentProfile(uid);
  105. mutate("getProfiles", { ...profiles, current: uid }, true);
  106. if (force) Notice.success("Refresh clash config", 1000);
  107. } catch (err: any) {
  108. Notice.error(err?.message || err.toString());
  109. }
  110. });
  111. return (
  112. <BasePage title={t("Profiles")}>
  113. <Box sx={{ display: "flex", mb: 2.5 }}>
  114. <TextField
  115. id="clas_verge_profile_url"
  116. name="profile_url"
  117. label={t("Profile URL")}
  118. size="small"
  119. fullWidth
  120. value={url}
  121. onChange={(e) => setUrl(e.target.value)}
  122. sx={{ mr: 1 }}
  123. />
  124. <Button
  125. disabled={!url || disabled}
  126. variant="contained"
  127. onClick={onImport}
  128. sx={{ mr: 1 }}
  129. >
  130. {t("Import")}
  131. </Button>
  132. <Button variant="contained" onClick={() => setDialogOpen(true)}>
  133. {t("New")}
  134. </Button>
  135. </Box>
  136. <Grid container spacing={2}>
  137. {regularItems.map((item) => (
  138. <Grid item xs={12} sm={6} key={item.file}>
  139. <ProfileItem
  140. selected={profiles.current === item.uid}
  141. itemData={item}
  142. onSelect={(f) => onSelect(item.uid, f)}
  143. />
  144. </Grid>
  145. ))}
  146. </Grid>
  147. {enhanceItems.length > 0 && (
  148. <EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
  149. )}
  150. <ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
  151. </BasePage>
  152. );
  153. };
  154. export default ProfilePage;