profiles.tsx 5.0 KB

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