proxy-item-mini.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { useEffect, useState } from "react";
  2. import { useLockFn } from "ahooks";
  3. import { CheckCircleOutlineRounded } from "@mui/icons-material";
  4. import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
  5. import { BaseLoading } from "@/components/base";
  6. import delayManager from "@/services/delay";
  7. import { useVerge } from "@/hooks/use-verge";
  8. interface Props {
  9. group: IProxyGroupItem;
  10. proxy: IProxyItem;
  11. selected: boolean;
  12. showType?: boolean;
  13. onClick?: (name: string) => void;
  14. }
  15. // 多列布局
  16. export const ProxyItemMini = (props: Props) => {
  17. const { group, proxy, selected, showType = true, onClick } = props;
  18. // -1/<=0 为 不显示
  19. // -2 为 loading
  20. const [delay, setDelay] = useState(-1);
  21. const { verge } = useVerge();
  22. const timeout = verge?.default_latency_timeout || 10000;
  23. useEffect(() => {
  24. delayManager.setListener(proxy.name, group.name, setDelay);
  25. return () => {
  26. delayManager.removeListener(proxy.name, group.name);
  27. };
  28. }, [proxy.name, group.name]);
  29. useEffect(() => {
  30. if (!proxy) return;
  31. setDelay(delayManager.getDelayFix(proxy, group.name));
  32. }, [proxy]);
  33. const onDelay = useLockFn(async () => {
  34. setDelay(-2);
  35. setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
  36. });
  37. return (
  38. <ListItemButton
  39. dense
  40. selected={selected}
  41. onClick={() => onClick?.(proxy.name)}
  42. sx={[
  43. {
  44. height: 56,
  45. borderRadius: 1.5,
  46. pl: 1.5,
  47. pr: 1,
  48. justifyContent: "space-between",
  49. alignItems: "center",
  50. },
  51. ({ palette: { mode, primary } }) => {
  52. const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
  53. const showDelay = delay > 0;
  54. const selectColor = mode === "light" ? primary.main : primary.light;
  55. return {
  56. "&:hover .the-check": { display: !showDelay ? "block" : "none" },
  57. "&:hover .the-delay": { display: showDelay ? "block" : "none" },
  58. "&:hover .the-icon": { display: "none" },
  59. "& .the-pin, & .the-unpin": {
  60. position: "absolute",
  61. fontSize: "12px",
  62. top: "-5px",
  63. right: "-5px",
  64. },
  65. "& .the-unpin": { filter: "grayscale(1)" },
  66. "&.Mui-selected": {
  67. width: `calc(100% + 3px)`,
  68. marginLeft: `-3px`,
  69. borderLeft: `3px solid ${selectColor}`,
  70. bgcolor:
  71. mode === "light"
  72. ? alpha(primary.main, 0.15)
  73. : alpha(primary.main, 0.35),
  74. },
  75. backgroundColor: bgcolor,
  76. };
  77. },
  78. ]}
  79. >
  80. <Box
  81. title={`${proxy.name}\n${proxy.now ?? ""}`}
  82. sx={{ overflow: "hidden" }}
  83. >
  84. <Typography
  85. variant="body2"
  86. component="div"
  87. color="text.primary"
  88. sx={{
  89. display: "block",
  90. textOverflow: "ellipsis",
  91. wordBreak: "break-all",
  92. overflow: "hidden",
  93. whiteSpace: "nowrap",
  94. }}
  95. >
  96. {proxy.name}
  97. </Typography>
  98. {showType && (
  99. <Box
  100. sx={{
  101. display: "flex",
  102. flexWrap: "nowrap",
  103. flex: "none",
  104. marginTop: "4px",
  105. }}
  106. >
  107. {proxy.now && (
  108. <Typography
  109. variant="body2"
  110. component="div"
  111. color="text.secondary"
  112. sx={{
  113. display: "block",
  114. textOverflow: "ellipsis",
  115. wordBreak: "break-all",
  116. overflow: "hidden",
  117. whiteSpace: "nowrap",
  118. marginRight: "8px",
  119. }}
  120. >
  121. {proxy.now}
  122. </Typography>
  123. )}
  124. {!!proxy.provider && (
  125. <TypeBox color="text.secondary" component="span">
  126. {proxy.provider}
  127. </TypeBox>
  128. )}
  129. <TypeBox color="text.secondary" component="span">
  130. {proxy.type}
  131. </TypeBox>
  132. {proxy.udp && (
  133. <TypeBox color="text.secondary" component="span">
  134. UDP
  135. </TypeBox>
  136. )}
  137. {proxy.xudp && (
  138. <TypeBox color="text.secondary" component="span">
  139. XUDP
  140. </TypeBox>
  141. )}
  142. {proxy.tfo && (
  143. <TypeBox color="text.secondary" component="span">
  144. TFO
  145. </TypeBox>
  146. )}
  147. </Box>
  148. )}
  149. </Box>
  150. <Box sx={{ ml: 0.5, color: "primary.main" }}>
  151. {delay === -2 && (
  152. <Widget>
  153. <BaseLoading />
  154. </Widget>
  155. )}
  156. {!proxy.provider && delay !== -2 && (
  157. // provider的节点不支持检测
  158. <Widget
  159. className="the-check"
  160. onClick={(e) => {
  161. e.preventDefault();
  162. e.stopPropagation();
  163. onDelay();
  164. }}
  165. sx={({ palette }) => ({
  166. display: "none", // hover才显示
  167. ":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
  168. })}
  169. >
  170. Check
  171. </Widget>
  172. )}
  173. {delay > 0 && (
  174. // 显示延迟
  175. <Widget
  176. className="the-delay"
  177. onClick={(e) => {
  178. if (proxy.provider) return;
  179. e.preventDefault();
  180. e.stopPropagation();
  181. onDelay();
  182. }}
  183. color={delayManager.formatDelayColor(delay, timeout)}
  184. sx={({ palette }) =>
  185. !proxy.provider
  186. ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
  187. : {}
  188. }
  189. >
  190. {delayManager.formatDelay(delay, timeout)}
  191. </Widget>
  192. )}
  193. {delay !== -2 && delay <= 0 && selected && (
  194. // 展示已选择的icon
  195. <CheckCircleOutlineRounded
  196. className="the-icon"
  197. sx={{ fontSize: 16, mr: 0.5, display: "block" }}
  198. />
  199. )}
  200. </Box>
  201. {group.fixed && group.fixed === proxy.name && (
  202. // 展示fixed状态
  203. <span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
  204. 📌
  205. </span>
  206. )}
  207. </ListItemButton>
  208. );
  209. };
  210. const Widget = styled(Box)(({ theme: { typography } }) => ({
  211. padding: "2px 4px",
  212. fontSize: 14,
  213. fontFamily: typography.fontFamily,
  214. borderRadius: "4px",
  215. }));
  216. const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
  217. display: "inline-block",
  218. border: "1px solid #ccc",
  219. borderColor: "text.secondary",
  220. color: "text.secondary",
  221. borderRadius: 4,
  222. fontSize: 10,
  223. fontFamily: typography.fontFamily,
  224. marginRight: "4px",
  225. marginTop: "auto",
  226. padding: "0 4px",
  227. lineHeight: 1.5,
  228. }));