profiles.tsx 5.3 KB

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