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

View File

@ -10,6 +10,7 @@ import {
List,
ListItem,
ListItemText,
TextField,
} from "@mui/material";
import {
SendRounded,
@ -17,11 +18,16 @@ import {
ExpandMoreRounded,
MyLocationRounded,
NetworkCheckRounded,
FilterAltRounded,
FilterAltOffRounded,
VisibilityRounded,
VisibilityOffRounded,
} from "@mui/icons-material";
import { ApiType } from "../../services/types";
import { updateProxy } from "../../services/api";
import { getProfiles, patchProfile } from "../../services/cmds";
import delayManager from "../../services/delay";
import useFilterProxy from "./use-filter-proxy";
import ProxyItem from "./proxy-item";
interface Props {
@ -32,11 +38,15 @@ const ProxyGroup = ({ group }: Props) => {
const { mutate } = useSWRConfig();
const [open, setOpen] = useState(false);
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 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
if (group.type !== "Selector") return;
@ -71,7 +81,7 @@ const ProxyGroup = ({ group }: Props) => {
});
const onLocation = (smooth = true) => {
const index = proxies.findIndex((p) => p.name === now);
const index = filterProxies.findIndex((p) => p.name === now);
if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({
@ -83,22 +93,21 @@ const ProxyGroup = ({ group }: Props) => {
};
const onCheckAll = useLockFn(async () => {
// rerender quickly
if (proxies.length) setTimeout(() => mutate("getProxies"), 500);
const names = filterProxies.map((p) => p.name);
const groupName = group.name;
let names = proxies.map((p) => p.name);
while (names.length) {
const list = names.slice(0, 8);
names = names.slice(8);
await delayManager.checkListDelay(
{ names, groupName, skipNum: 8, maxTimeout: 600 },
() => mutate("getProxies")
);
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
useEffect(() => {
if (open) {
@ -126,7 +135,16 @@ const ProxyGroup = ({ group }: Props) => {
</ListItem>
<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
size="small"
title="location"
@ -134,23 +152,67 @@ const ProxyGroup = ({ group }: Props) => {
>
<MyLocationRounded />
</IconButton>
<IconButton size="small" title="check" onClick={onCheckAll}>
<NetworkCheckRounded />
</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>
{proxies.length >= 10 ? (
{!filterProxies.length && (
<Box
sx={{
py: 3,
fontSize: 18,
textAlign: "center",
color: "text.secondary",
}}
>
Empty
</Box>
)}
{filterProxies.length >= 10 ? (
<Virtuoso
ref={virtuosoRef}
style={{ height: "320px", marginBottom: "4px" }}
totalCount={proxies.length}
totalCount={filterProxies.length}
itemContent={(index) => (
<ProxyItem
groupName={group.name}
proxy={proxies[index]}
selected={proxies[index].name === now}
proxy={filterProxies[index]}
selected={filterProxies[index].name === now}
showType={showType}
sx={{ py: 0, pl: 4 }}
onClick={onSelect}
onClick={onChangeProxy}
/>
)}
/>
@ -160,14 +222,15 @@ const ProxyGroup = ({ group }: Props) => {
disablePadding
sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }}
>
{proxies.map((proxy) => (
{filterProxies.map((proxy) => (
<ProxyItem
key={proxy.name}
groupName={group.name}
proxy={proxy}
selected={proxy.name === now}
showType={showType}
sx={{ py: 0, pl: 4 }}
onClick={onSelect}
onClick={onChangeProxy}
/>
))}
</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 {
alpha,
@ -18,6 +18,7 @@ interface Props {
groupName: string;
proxy: ApiType.ProxyItem;
selected: boolean;
showType?: boolean;
sx?: SxProps<Theme>;
onClick?: (name: string) => void;
}
@ -27,8 +28,20 @@ const Widget = styled(Box)(() => ({
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 { groupName, proxy, selected, sx, onClick } = props;
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
const [delay, setDelay] = useState(-1);
useEffect(() => {
@ -37,14 +50,19 @@ const ProxyItem = (props: Props) => {
}
}, [proxy]);
const delayRef = useRef(false);
const onDelay = (e: any) => {
e.preventDefault();
e.stopPropagation();
if (delayRef.current) return;
delayRef.current = true;
delayManager
.checkDelay(proxy.name, groupName)
.then((result) => setDelay(result))
.catch(() => setDelay(1e6));
.catch(() => setDelay(1e6))
.finally(() => (delayRef.current = false));
};
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
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);
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();