profile.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { useEffect, useRef, useState } from "react";
  2. import useSWR, { useSWRConfig } from "swr";
  3. import { Box, Button, Grid, TextField, Typography } from "@mui/material";
  4. import {
  5. getProfiles,
  6. putProfiles,
  7. setProfiles,
  8. importProfile,
  9. } from "../services/cmds";
  10. import { getProxies, updateProxy } from "../services/api";
  11. import ProfileItemComp from "../components/profile-item";
  12. import useNotice from "../utils/use-notice";
  13. import noop from "../utils/noop";
  14. const ProfilePage = () => {
  15. const [url, setUrl] = useState("");
  16. const [disabled, setDisabled] = useState(false);
  17. const [notice, noticeElement] = useNotice();
  18. const { mutate } = useSWRConfig();
  19. const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
  20. useEffect(() => {
  21. if (profiles.current == null) return;
  22. if (!profiles.items) profiles.items = [];
  23. const profile = profiles.items![profiles.current];
  24. if (!profile) return;
  25. getProxies().then((proxiesData) => {
  26. mutate("getProxies", proxiesData);
  27. // init selected array
  28. const { selected = [] } = profile;
  29. const selectedMap = Object.fromEntries(
  30. selected.map((each) => [each.name!, each.now!])
  31. );
  32. // todo: enhance error handle
  33. let hasChange = false;
  34. proxiesData.groups.forEach((group) => {
  35. const { name, now } = group;
  36. if (!now || selectedMap[name] === now) return;
  37. if (selectedMap[name] == null) {
  38. selectedMap[name] = now!;
  39. } else {
  40. hasChange = true;
  41. updateProxy(name, selectedMap[name]);
  42. }
  43. });
  44. // update profile selected list
  45. profile.selected = Object.entries(selectedMap).map(([name, now]) => ({
  46. name,
  47. now,
  48. }));
  49. setProfiles(profiles.current!, profile).catch(console.error);
  50. // update proxies cache
  51. if (hasChange) mutate("getProxies", getProxies());
  52. });
  53. }, [profiles]);
  54. const onImport = async () => {
  55. if (!url) return;
  56. setUrl("");
  57. setDisabled(true);
  58. try {
  59. await importProfile(url);
  60. mutate("getProfiles", getProfiles());
  61. if (!profiles.items?.length) putProfiles(0).catch(noop);
  62. notice.success("Successfully import profile.");
  63. } catch {
  64. notice.error("Failed to import profile.");
  65. } finally {
  66. setDisabled(false);
  67. }
  68. };
  69. const lockRef = useRef(false);
  70. const onProfileChange = (index: number) => {
  71. if (lockRef.current) return;
  72. lockRef.current = true;
  73. putProfiles(index)
  74. .then(() => {
  75. mutate("getProfiles", { ...profiles, current: index }, true);
  76. })
  77. .catch((err) => {
  78. console.error(err);
  79. })
  80. .finally(() => {
  81. lockRef.current = false;
  82. });
  83. };
  84. return (
  85. <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}>
  86. <Typography variant="h4" component="h1" sx={{ py: 2, mb: 1 }}>
  87. Profiles
  88. </Typography>
  89. <Box sx={{ display: "flex", mb: 3 }}>
  90. <TextField
  91. id="profile_url"
  92. name="profile_url"
  93. label="Profile URL"
  94. size="small"
  95. fullWidth
  96. value={url}
  97. onChange={(e) => setUrl(e.target.value)}
  98. sx={{ mr: 4 }}
  99. />
  100. <Button
  101. disabled={!url || disabled}
  102. variant="contained"
  103. onClick={onImport}
  104. >
  105. Import
  106. </Button>
  107. </Box>
  108. <Grid container spacing={3}>
  109. {profiles?.items?.map((item, idx) => (
  110. <Grid item xs={12} sm={6} key={item.file}>
  111. <ProfileItemComp
  112. index={idx}
  113. selected={profiles.current === idx}
  114. itemData={item}
  115. onClick={() => onProfileChange(idx)}
  116. />
  117. </Grid>
  118. ))}
  119. </Grid>
  120. {noticeElement}
  121. </Box>
  122. );
  123. };
  124. export default ProfilePage;