proxy-group.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { useEffect, useRef, useState } from "react";
  2. import { useSWRConfig } from "swr";
  3. import { Virtuoso } from "react-virtuoso";
  4. import {
  5. Box,
  6. Collapse,
  7. Divider,
  8. IconButton,
  9. List,
  10. ListItem,
  11. ListItemText,
  12. } from "@mui/material";
  13. import {
  14. SendRounded,
  15. ExpandLessRounded,
  16. ExpandMoreRounded,
  17. MyLocationRounded,
  18. NetworkCheckRounded,
  19. } from "@mui/icons-material";
  20. import { ApiType } from "../../services/types";
  21. import { updateProxy } from "../../services/api";
  22. import { getProfiles, patchProfile } from "../../services/cmds";
  23. import delayManager from "../../services/delay";
  24. import ProxyItem from "./proxy-item";
  25. interface Props {
  26. group: ApiType.ProxyGroupItem;
  27. }
  28. const ProxyGroup = ({ group }: Props) => {
  29. const { mutate } = useSWRConfig();
  30. const [open, setOpen] = useState(false);
  31. const [now, setNow] = useState(group.now);
  32. const virtuosoRef = useRef<any>();
  33. const proxies = group.all ?? [];
  34. const selectLockRef = useRef(false);
  35. const onSelect = async (name: string) => {
  36. // Todo: support another proxy group type
  37. if (group.type !== "Selector") return;
  38. if (selectLockRef.current) return;
  39. selectLockRef.current = true;
  40. const oldValue = now;
  41. try {
  42. setNow(name);
  43. await updateProxy(group.name, name);
  44. } catch {
  45. setNow(oldValue);
  46. return; // do not update profile
  47. } finally {
  48. selectLockRef.current = false;
  49. }
  50. try {
  51. const profiles = await getProfiles();
  52. const profile = profiles.items![profiles.current!]!;
  53. if (!profile) return;
  54. if (!profile.selected) profile.selected = [];
  55. const index = profile.selected.findIndex(
  56. (item) => item.name === group.name
  57. );
  58. if (index < 0) {
  59. profile.selected.push({ name: group.name, now: name });
  60. } else {
  61. profile.selected[index] = { name: group.name, now: name };
  62. }
  63. await patchProfile(profiles.current!, profile);
  64. } catch (err) {
  65. console.error(err);
  66. }
  67. };
  68. const onLocation = (smooth = true) => {
  69. const index = proxies.findIndex((p) => p.name === now);
  70. if (index >= 0) {
  71. virtuosoRef.current?.scrollToIndex?.({
  72. index,
  73. align: "center",
  74. behavior: smooth ? "smooth" : "auto",
  75. });
  76. }
  77. };
  78. const checkLockRef = useRef(false);
  79. const onCheckAll = async () => {
  80. if (checkLockRef.current) return;
  81. checkLockRef.current = true;
  82. // rerender quickly
  83. if (proxies.length) setTimeout(() => mutate("getProxies"), 500);
  84. let names = proxies.map((p) => p.name);
  85. while (names.length) {
  86. const list = names.slice(0, 8);
  87. names = names.slice(8);
  88. await Promise.all(
  89. list.map((n) => delayManager.checkDelay(n, group.name))
  90. );
  91. mutate("getProxies");
  92. }
  93. checkLockRef.current = false;
  94. };
  95. // auto scroll to current index
  96. useEffect(() => {
  97. if (open) {
  98. setTimeout(() => onLocation(false), 5);
  99. }
  100. }, [open]);
  101. return (
  102. <>
  103. <ListItem button onClick={() => setOpen(!open)} dense>
  104. <ListItemText
  105. primary={group.name}
  106. secondary={
  107. <>
  108. <SendRounded color="primary" sx={{ mr: 1, fontSize: 14 }} />
  109. <span>{now}</span>
  110. </>
  111. }
  112. secondaryTypographyProps={{
  113. sx: { display: "flex", alignItems: "center" },
  114. }}
  115. />
  116. {open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
  117. </ListItem>
  118. <Collapse in={open} timeout="auto" unmountOnExit>
  119. <Box sx={{ pl: 4, pr: 3, my: 0.5 }}>
  120. <IconButton
  121. size="small"
  122. title="location"
  123. onClick={() => onLocation(true)}
  124. >
  125. <MyLocationRounded />
  126. </IconButton>
  127. <IconButton size="small" title="check" onClick={onCheckAll}>
  128. <NetworkCheckRounded />
  129. </IconButton>
  130. </Box>
  131. {proxies.length >= 10 ? (
  132. <Virtuoso
  133. ref={virtuosoRef}
  134. style={{ height: "320px", marginBottom: "4px" }}
  135. totalCount={proxies.length}
  136. itemContent={(index) => (
  137. <ProxyItem
  138. groupName={group.name}
  139. proxy={proxies[index]}
  140. selected={proxies[index].name === now}
  141. sx={{ py: 0, pl: 4 }}
  142. onClick={onSelect}
  143. />
  144. )}
  145. />
  146. ) : (
  147. <List
  148. component="div"
  149. disablePadding
  150. sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }}
  151. >
  152. {proxies.map((proxy) => (
  153. <ProxyItem
  154. key={proxy.name}
  155. groupName={group.name}
  156. proxy={proxy}
  157. selected={proxy.name === now}
  158. sx={{ py: 0, pl: 4 }}
  159. onClick={onSelect}
  160. />
  161. ))}
  162. </List>
  163. )}
  164. <Divider variant="middle" />
  165. </Collapse>
  166. </>
  167. );
  168. };
  169. export default ProxyGroup;