diff --git a/src/components/proxy/proxy-group.tsx b/src/components/proxy/proxy-group.tsx index 263866071d6696b7b260d4633e5a93fabdb18497..addca600152babbe0d96c9b8ce402d4f7fae7b95 100644 --- a/src/components/proxy/proxy-group.tsx +++ b/src/components/proxy/proxy-group.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react"; -import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; +import { useSWRConfig } from "swr"; +import { Virtuoso } from "react-virtuoso"; import { Box, Collapse, @@ -19,6 +20,7 @@ import { import { ApiType } from "../../services/types"; import { updateProxy } from "../../services/api"; import { getProfiles, patchProfile } from "../../services/cmds"; +import delayManager from "../../services/delay"; import ProxyItem from "./proxy-item"; interface Props { @@ -26,13 +28,15 @@ interface Props { } const ProxyGroup = ({ group }: Props) => { + const { mutate } = useSWRConfig(); + const listRef = useRef<any>(); const [open, setOpen] = useState(false); const [now, setNow] = useState(group.now); const proxies = group.all ?? []; - const onUpdate = async (name: string) => { + const onSelect = async (name: string) => { // can not call update if (group.type !== "Selector") { // Todo @@ -80,6 +84,21 @@ const ProxyGroup = ({ group }: Props) => { } }; + const onCheckAll = async () => { + let names = proxies.map((p) => p.name); + + while (names.length) { + const list = names.slice(0, 10); + names = names.slice(10); + + await Promise.all( + list.map((n) => delayManager.checkDelay(n, group.name)) + ); + + mutate("getProxies"); + } + }; + return ( <> <ListItem button onClick={() => setOpen(!open)} dense> @@ -104,7 +123,7 @@ const ProxyGroup = ({ group }: Props) => { <IconButton size="small" title="location" onClick={onLocation}> <MyLocationRounded /> </IconButton> - <IconButton size="small" title="check"> + <IconButton size="small" title="check" onClick={onCheckAll}> <NetworkCheckRounded /> </IconButton> </Box> @@ -116,10 +135,11 @@ const ProxyGroup = ({ group }: Props) => { totalCount={proxies.length} itemContent={(index) => ( <ProxyItem + groupName={group.name} proxy={proxies[index]} selected={proxies[index].name === now} sx={{ py: 0, pl: 4 }} - onClick={onUpdate} + onClick={onSelect} /> )} /> @@ -132,10 +152,11 @@ const ProxyGroup = ({ group }: Props) => { {proxies.map((proxy) => ( <ProxyItem key={proxy.name} + groupName={group.name} proxy={proxy} selected={proxy.name === now} sx={{ py: 0, pl: 4 }} - onClick={onUpdate} + onClick={onSelect} /> ))} </List> diff --git a/src/components/proxy/proxy-item.tsx b/src/components/proxy/proxy-item.tsx index b8bb33799c1b86fc013a7962c95294a071caf9eb..88d90ccc2acc29aad532a372c4e07f1397313e44 100644 --- a/src/components/proxy/proxy-item.tsx +++ b/src/components/proxy/proxy-item.tsx @@ -1,24 +1,51 @@ +import { useEffect, useState } from "react"; import { CheckCircleOutlineRounded } from "@mui/icons-material"; import { alpha, + Box, ListItem, ListItemButton, ListItemIcon, ListItemText, + styled, SxProps, Theme, } from "@mui/material"; import { ApiType } from "../../services/types"; +import delayManager from "../../services/delay"; interface Props { + groupName: string; proxy: ApiType.ProxyItem; selected: boolean; sx?: SxProps<Theme>; onClick?: (name: string) => void; } +const Widget = styled(Box)(() => ({ + padding: "4px 6px", + fontSize: 14, +})); + const ProxyItem = (props: Props) => { - const { proxy, selected, sx, onClick } = props; + const { groupName, proxy, selected, sx, onClick } = props; + const [delay, setDelay] = useState(-1); + + useEffect(() => { + if (proxy) { + setDelay(delayManager.getDelay(proxy.name, groupName)); + } + }, [proxy]); + + const onDelay = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + + delayManager + .checkDelay(proxy.name, groupName) + .then((result) => setDelay(result)) + .catch(() => setDelay(1e6)); + }; return ( <ListItem sx={sx}> @@ -27,9 +54,7 @@ const ProxyItem = (props: Props) => { selected={selected} onClick={() => onClick?.(proxy.name)} sx={[ - { - borderRadius: 1, - }, + { borderRadius: 1 }, ({ palette: { mode, primary } }) => { const bgcolor = mode === "light" @@ -37,7 +62,16 @@ const ProxyItem = (props: Props) => { : alpha(primary.main, 0.35); const color = mode === "light" ? primary.main : primary.light; + const showDelay = delay > 0; + const showIcon = !showDelay && selected; + return { + ".the-check": { display: "none" }, + ".the-delay": { display: showDelay ? "block" : "none" }, + ".the-icon": { display: showIcon ? "block" : "none" }, + "&:hover .the-check": { display: !showDelay ? "block" : "none" }, + "&:hover .the-delay": { display: showDelay ? "block" : "none" }, + "&:hover .the-icon": { display: "none" }, "&.Mui-selected": { bgcolor }, "&.Mui-selected .MuiListItemText-secondary": { color }, }; @@ -45,10 +79,32 @@ const ProxyItem = (props: Props) => { ]} > <ListItemText title={proxy.name} secondary={proxy.name} /> + <ListItemIcon sx={{ justifyContent: "flex-end", color: "primary.main" }} > - {selected && <CheckCircleOutlineRounded sx={{ fontSize: 16 }} />} + <Widget className="the-check" onClick={onDelay}> + Check + </Widget> + + <Widget + className="the-delay" + onClick={onDelay} + color={ + delay > 500 + ? "error.main" + : delay < 100 + ? "success.main" + : "text.secondary" + } + > + {delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}ms`} + </Widget> + + <CheckCircleOutlineRounded + className="the-icon" + sx={{ fontSize: 16 }} + /> </ListItemIcon> </ListItemButton> </ListItem> diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index adb1892fe0805cc63a5e6539c0f7baae783477b6..c95b447205569c99dc87049657602cb9b80cbb94 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -114,6 +114,7 @@ const ProxyPage = () => { totalCount={filterProxies.length} itemContent={(index) => ( <ProxyItem + groupName="GLOBAL" proxy={filterProxies[index]} selected={filterProxies[index].name === curProxy} onClick={onChangeProxy} diff --git a/src/services/api.ts b/src/services/api.ts index ad034cca5e385fd9c6d07d7a003fbde7e73e523b..844c5e3711533a0133d8203850593f8aeea5ae9e 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -60,10 +60,24 @@ export async function getRules() { return instance.get("/rules") as Promise<ApiType.RuleItem[]>; } +/// Get Proxy delay +export async function getProxyDelay( + name: string, + url?: string +): Promise<{ delay: number }> { + const params = { + timeout: 3000, + url: url || "http://www.gstatic.com/generate_204", + }; + + const instance = await getAxios(); + return instance.get(`/proxies/${encodeURIComponent(name)}/delay`, { params }); +} + /// Update the Proxy Choose export async function updateProxy(group: string, proxy: string) { const instance = await getAxios(); - return instance.put(`/proxies/${group}`, { name: proxy }); + return instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }); } /// Get the Proxy infomation diff --git a/src/services/delay.ts b/src/services/delay.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b2b8f0a31f273b0e32702fa1e356a3f7cf38371 --- /dev/null +++ b/src/services/delay.ts @@ -0,0 +1,37 @@ +import { getProxyDelay } from "./api"; + +const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`; + +class DelayManager { + private cache = new Map<string, [number, number]>(); + + setDelay(name: string, group: string, delay: number) { + this.cache.set(hashKey(name, group), [Date.now(), delay]); + } + + getDelay(name: string, group: string) { + if (!name) return -1; + + const result = this.cache.get(hashKey(name, group)); + if (result && Date.now() - result[0] <= 18e5) { + return result[1]; + } + return -1; + } + + async checkDelay(name: string, group: string) { + let delay = -1; + + try { + const result = await getProxyDelay(name); + delay = result.delay; + } catch { + delay = 1e6; // error + } + + this.setDelay(name, group, delay); + return delay; + } +} + +export default new DelayManager();