feat: filter proxy and display type

This commit is contained in:
GyDi 2022-02-27 00:58:14 +08:00
parent 98b8a122b6
commit 9df361935f
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
5 changed files with 269 additions and 45 deletions

View File

@ -2,11 +2,19 @@ import { useEffect, useRef, useState } from "react";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { Box, IconButton } from "@mui/material"; import { Box, IconButton, TextField } from "@mui/material";
import { MyLocationRounded, NetworkCheckRounded } from "@mui/icons-material"; import {
MyLocationRounded,
NetworkCheckRounded,
FilterAltRounded,
FilterAltOffRounded,
VisibilityRounded,
VisibilityOffRounded,
} from "@mui/icons-material";
import { ApiType } from "../../services/types"; import { ApiType } from "../../services/types";
import { updateProxy } from "../../services/api"; import { updateProxy } from "../../services/api";
import delayManager from "../../services/delay"; import delayManager from "../../services/delay";
import useFilterProxy from "./use-filter-proxy";
import ProxyItem from "./proxy-item"; import ProxyItem from "./proxy-item";
interface Props { interface Props {
@ -19,8 +27,13 @@ const ProxyGlobal = (props: Props) => {
const { groupName, curProxy, proxies } = props; const { groupName, curProxy, proxies } = props;
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const virtuosoRef = useRef<any>();
const [now, setNow] = useState(curProxy || "DIRECT"); const [now, setNow] = useState(curProxy || "DIRECT");
const [showType, setShowType] = useState(false);
const [showFilter, setShowFilter] = useState(false);
const [filterText, setFilterText] = useState("");
const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(proxies, groupName, filterText);
const onChangeProxy = useLockFn(async (name: string) => { const onChangeProxy = useLockFn(async (name: string) => {
await updateProxy("GLOBAL", name); await updateProxy("GLOBAL", name);
@ -29,7 +42,7 @@ const ProxyGlobal = (props: Props) => {
}); });
const onLocation = (smooth = true) => { const onLocation = (smooth = true) => {
const index = proxies.findIndex((p) => p.name === now); const index = filterProxies.findIndex((p) => p.name === now);
if (index >= 0) { if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({ virtuosoRef.current?.scrollToIndex?.({
@ -41,22 +54,22 @@ const ProxyGlobal = (props: Props) => {
}; };
const onCheckAll = useLockFn(async () => { const onCheckAll = useLockFn(async () => {
// rerender quickly const names = filterProxies.map((p) => p.name);
if (proxies.length) setTimeout(() => mutate("getProxies"), 500);
let names = proxies.map((p) => p.name); await delayManager.checkListDelay(
while (names.length) { { names, groupName, skipNum: 8, maxTimeout: 600 },
const list = names.slice(0, 8); () => mutate("getProxies")
names = names.slice(8); );
await Promise.all(list.map((n) => delayManager.checkDelay(n, groupName)));
mutate("getProxies"); mutate("getProxies");
}
}); });
useEffect(() => onLocation(false), [groupName]); useEffect(() => onLocation(false), [groupName]);
useEffect(() => {
if (!showFilter) setFilterText("");
}, [showFilter]);
useEffect(() => { useEffect(() => {
if (groupName === "DIRECT") setNow("DIRECT"); if (groupName === "DIRECT") setNow("DIRECT");
if (groupName === "GLOBAL") setNow(curProxy || "DIRECT"); if (groupName === "GLOBAL") setNow(curProxy || "DIRECT");
@ -64,7 +77,15 @@ const ProxyGlobal = (props: Props) => {
return ( return (
<> <>
<Box sx={{ px: 3, my: 0.5 }}> <Box
sx={{
px: 3,
my: 0.5,
display: "flex",
alignItems: "center",
button: { mr: 0.5 },
}}
>
<IconButton <IconButton
size="small" size="small"
title="location" title="location"
@ -72,20 +93,50 @@ const ProxyGlobal = (props: Props) => {
> >
<MyLocationRounded /> <MyLocationRounded />
</IconButton> </IconButton>
<IconButton size="small" title="check" onClick={onCheckAll}> <IconButton size="small" title="check" onClick={onCheckAll}>
<NetworkCheckRounded /> <NetworkCheckRounded />
</IconButton> </IconButton>
<IconButton
size="small"
title="check"
onClick={() => setShowType(!showType)}
>
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
</IconButton>
<IconButton
size="small"
title="check"
onClick={() => setShowFilter(!showFilter)}
>
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
</IconButton>
{showFilter && (
<TextField
autoFocus
hiddenLabel
value={filterText}
size="small"
variant="outlined"
placeholder="Filter conditions"
onChange={(e) => setFilterText(e.target.value)}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
)}
</Box> </Box>
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
style={{ height: "calc(100% - 40px)" }} style={{ height: "calc(100% - 40px)" }}
totalCount={proxies.length} totalCount={filterProxies.length}
itemContent={(index) => ( itemContent={(index) => (
<ProxyItem <ProxyItem
groupName={groupName} groupName={groupName}
proxy={proxies[index]} proxy={filterProxies[index]}
selected={proxies[index].name === now} selected={filterProxies[index].name === now}
onClick={onChangeProxy} onClick={onChangeProxy}
sx={{ py: 0, px: 2 }} sx={{ py: 0, px: 2 }}
/> />

View File

@ -10,6 +10,7 @@ import {
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
TextField,
} from "@mui/material"; } from "@mui/material";
import { import {
SendRounded, SendRounded,
@ -17,11 +18,16 @@ import {
ExpandMoreRounded, ExpandMoreRounded,
MyLocationRounded, MyLocationRounded,
NetworkCheckRounded, NetworkCheckRounded,
FilterAltRounded,
FilterAltOffRounded,
VisibilityRounded,
VisibilityOffRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { ApiType } from "../../services/types"; import { ApiType } from "../../services/types";
import { updateProxy } from "../../services/api"; import { updateProxy } from "../../services/api";
import { getProfiles, patchProfile } from "../../services/cmds"; import { getProfiles, patchProfile } from "../../services/cmds";
import delayManager from "../../services/delay"; import delayManager from "../../services/delay";
import useFilterProxy from "./use-filter-proxy";
import ProxyItem from "./proxy-item"; import ProxyItem from "./proxy-item";
interface Props { interface Props {
@ -32,11 +38,15 @@ const ProxyGroup = ({ group }: Props) => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [now, setNow] = useState(group.now); const [now, setNow] = useState(group.now);
const [showType, setShowType] = useState(false);
const [showFilter, setShowFilter] = useState(false);
const [filterText, setFilterText] = useState("");
const virtuosoRef = useRef<any>();
const proxies = group.all ?? []; const proxies = group.all ?? [];
const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(proxies, group.name, filterText);
const onSelect = useLockFn(async (name: string) => { const onChangeProxy = useLockFn(async (name: string) => {
// Todo: support another proxy group type // Todo: support another proxy group type
if (group.type !== "Selector") return; if (group.type !== "Selector") return;
@ -71,7 +81,7 @@ const ProxyGroup = ({ group }: Props) => {
}); });
const onLocation = (smooth = true) => { const onLocation = (smooth = true) => {
const index = proxies.findIndex((p) => p.name === now); const index = filterProxies.findIndex((p) => p.name === now);
if (index >= 0) { if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({ virtuosoRef.current?.scrollToIndex?.({
@ -83,22 +93,21 @@ const ProxyGroup = ({ group }: Props) => {
}; };
const onCheckAll = useLockFn(async () => { const onCheckAll = useLockFn(async () => {
// rerender quickly const names = filterProxies.map((p) => p.name);
if (proxies.length) setTimeout(() => mutate("getProxies"), 500); const groupName = group.name;
let names = proxies.map((p) => p.name); await delayManager.checkListDelay(
while (names.length) { { names, groupName, skipNum: 8, maxTimeout: 600 },
const list = names.slice(0, 8); () => mutate("getProxies")
names = names.slice(8);
await Promise.all(
list.map((n) => delayManager.checkDelay(n, group.name))
); );
mutate("getProxies"); mutate("getProxies");
}
}); });
useEffect(() => {
if (!showFilter) setFilterText("");
}, [showFilter]);
// auto scroll to current index // auto scroll to current index
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -126,7 +135,16 @@ const ProxyGroup = ({ group }: Props) => {
</ListItem> </ListItem>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ pl: 4, pr: 3, my: 0.5 }}> <Box
sx={{
pl: 4,
pr: 3,
my: 0.5,
display: "flex",
alignItems: "center",
button: { mr: 0.5 },
}}
>
<IconButton <IconButton
size="small" size="small"
title="location" title="location"
@ -134,23 +152,67 @@ const ProxyGroup = ({ group }: Props) => {
> >
<MyLocationRounded /> <MyLocationRounded />
</IconButton> </IconButton>
<IconButton size="small" title="check" onClick={onCheckAll}> <IconButton size="small" title="check" onClick={onCheckAll}>
<NetworkCheckRounded /> <NetworkCheckRounded />
</IconButton> </IconButton>
<IconButton
size="small"
title="check"
onClick={() => setShowType(!showType)}
>
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
</IconButton>
<IconButton
size="small"
title="check"
onClick={() => setShowFilter(!showFilter)}
>
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
</IconButton>
{showFilter && (
<TextField
autoFocus
hiddenLabel
value={filterText}
size="small"
variant="outlined"
placeholder="Filter conditions"
onChange={(e) => setFilterText(e.target.value)}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
)}
</Box> </Box>
{proxies.length >= 10 ? ( {!filterProxies.length && (
<Box
sx={{
py: 3,
fontSize: 18,
textAlign: "center",
color: "text.secondary",
}}
>
Empty
</Box>
)}
{filterProxies.length >= 10 ? (
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
style={{ height: "320px", marginBottom: "4px" }} style={{ height: "320px", marginBottom: "4px" }}
totalCount={proxies.length} totalCount={filterProxies.length}
itemContent={(index) => ( itemContent={(index) => (
<ProxyItem <ProxyItem
groupName={group.name} groupName={group.name}
proxy={proxies[index]} proxy={filterProxies[index]}
selected={proxies[index].name === now} selected={filterProxies[index].name === now}
showType={showType}
sx={{ py: 0, pl: 4 }} sx={{ py: 0, pl: 4 }}
onClick={onSelect} onClick={onChangeProxy}
/> />
)} )}
/> />
@ -160,14 +222,15 @@ const ProxyGroup = ({ group }: Props) => {
disablePadding disablePadding
sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }} sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }}
> >
{proxies.map((proxy) => ( {filterProxies.map((proxy) => (
<ProxyItem <ProxyItem
key={proxy.name} key={proxy.name}
groupName={group.name} groupName={group.name}
proxy={proxy} proxy={proxy}
selected={proxy.name === now} selected={proxy.name === now}
showType={showType}
sx={{ py: 0, pl: 4 }} sx={{ py: 0, pl: 4 }}
onClick={onSelect} onClick={onChangeProxy}
/> />
))} ))}
</List> </List>

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { CheckCircleOutlineRounded } from "@mui/icons-material"; import { CheckCircleOutlineRounded } from "@mui/icons-material";
import { import {
alpha, alpha,
@ -18,6 +18,7 @@ interface Props {
groupName: string; groupName: string;
proxy: ApiType.ProxyItem; proxy: ApiType.ProxyItem;
selected: boolean; selected: boolean;
showType?: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
onClick?: (name: string) => void; onClick?: (name: string) => void;
} }
@ -27,8 +28,20 @@ const Widget = styled(Box)(() => ({
fontSize: 14, fontSize: 14,
})); }));
const TypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.text.secondary, 0.36),
color: alpha(theme.palette.text.secondary, 0.42),
borderRadius: 4,
fontSize: 10,
marginLeft: 4,
padding: "0 2px",
lineHeight: 1.25,
}));
const ProxyItem = (props: Props) => { const ProxyItem = (props: Props) => {
const { groupName, proxy, selected, sx, onClick } = props; const { groupName, proxy, selected, showType = true, sx, onClick } = props;
const [delay, setDelay] = useState(-1); const [delay, setDelay] = useState(-1);
useEffect(() => { useEffect(() => {
@ -37,14 +50,19 @@ const ProxyItem = (props: Props) => {
} }
}, [proxy]); }, [proxy]);
const delayRef = useRef(false);
const onDelay = (e: any) => { const onDelay = (e: any) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (delayRef.current) return;
delayRef.current = true;
delayManager delayManager
.checkDelay(proxy.name, groupName) .checkDelay(proxy.name, groupName)
.then((result) => setDelay(result)) .then((result) => setDelay(result))
.catch(() => setDelay(1e6)); .catch(() => setDelay(1e6))
.finally(() => (delayRef.current = false));
}; };
return ( return (
@ -78,7 +96,17 @@ const ProxyItem = (props: Props) => {
}, },
]} ]}
> >
<ListItemText title={proxy.name} secondary={proxy.name} /> <ListItemText
title={proxy.name}
secondary={
<>
{proxy.name}
{showType && <TypeBox>{proxy.type}</TypeBox>}
{showType && proxy.udp && <TypeBox>UDP</TypeBox>}
</>
}
/>
<ListItemIcon <ListItemIcon
sx={{ justifyContent: "flex-end", color: "primary.main" }} sx={{ justifyContent: "flex-end", color: "primary.main" }}

View File

@ -0,0 +1,49 @@
import { useMemo } from "react";
import { ApiType } from "../../services/types";
import delayManager from "../../services/delay";
const regex1 = /delay([=<>])(\d+|timeout|error)/i;
const regex2 = /type=(.*)/i;
/**
* filter the proxy
* according to the regular conditions
*/
export default function useFilterProxy(
proxies: ApiType.ProxyItem[],
groupName: string,
filterText: string
) {
return useMemo(() => {
if (!filterText) return proxies;
const res1 = regex1.exec(filterText);
if (res1) {
const symbol = res1[1];
const symbol2 = res1[2].toLowerCase();
const value =
symbol2 === "error" ? 1e5 : symbol2 === "timeout" ? 3000 : +symbol2;
return proxies.filter((p) => {
const delay = delayManager.getDelay(p.name, groupName);
if (delay < 0) return false;
if (symbol === "=" && symbol2 === "error") return delay >= 1e5;
if (symbol === "=" && symbol2 === "timeout")
return delay < 1e5 && delay >= 3000;
if (symbol === "=") return delay == value;
if (symbol === "<") return delay <= value;
if (symbol === ">") return delay >= value;
return false;
});
}
const res2 = regex2.exec(filterText);
if (res2) {
const type = res2[1].toLowerCase();
return proxies.filter((p) => p.type.toLowerCase().includes(type));
}
return proxies.filter((p) => p.name.includes(filterText.trim()));
}, [proxies, groupName, filterText]);
}

View File

@ -32,6 +32,39 @@ class DelayManager {
this.setDelay(name, group, delay); this.setDelay(name, group, delay);
return delay; return delay;
} }
async checkListDelay(
options: {
names: readonly string[];
groupName: string;
skipNum: number;
maxTimeout: number;
},
callback: Function
) {
let names = [...options.names];
const { groupName, skipNum, maxTimeout } = options;
while (names.length) {
const list = names.slice(0, skipNum);
names = names.slice(skipNum);
let called = false;
setTimeout(() => {
if (!called) {
called = true;
callback();
}
}, maxTimeout);
await Promise.all(list.map((n) => this.checkDelay(n, groupName)));
if (!called) {
called = true;
callback();
}
}
}
} }
export default new DelayManager(); export default new DelayManager();