From 9df361935f757108d7bdc23abcfb351d808a4700 Mon Sep 17 00:00:00 2001 From: GyDi Date: Sun, 27 Feb 2022 00:58:14 +0800 Subject: [PATCH] feat: filter proxy and display type --- src/components/proxy/proxy-global.tsx | 87 ++++++++++++++---- src/components/proxy/proxy-group.tsx | 109 ++++++++++++++++++----- src/components/proxy/proxy-item.tsx | 36 +++++++- src/components/proxy/use-filter-proxy.ts | 49 ++++++++++ src/services/delay.ts | 33 +++++++ 5 files changed, 269 insertions(+), 45 deletions(-) create mode 100644 src/components/proxy/use-filter-proxy.ts diff --git a/src/components/proxy/proxy-global.tsx b/src/components/proxy/proxy-global.tsx index ab26799..de28b7f 100644 --- a/src/components/proxy/proxy-global.tsx +++ b/src/components/proxy/proxy-global.tsx @@ -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(); const [now, setNow] = useState(curProxy || "DIRECT"); + const [showType, setShowType] = useState(false); + const [showFilter, setShowFilter] = useState(false); + const [filterText, setFilterText] = useState(""); + + const virtuosoRef = useRef(); + 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 ( <> - + { > + + + setShowType(!showType)} + > + {showType ? : } + + + setShowFilter(!showFilter)} + > + {showFilter ? : } + + + {showFilter && ( + setFilterText(e.target.value)} + sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} + /> + )} ( diff --git a/src/components/proxy/proxy-group.tsx b/src/components/proxy/proxy-group.tsx index e52b01c..2a3d61d 100644 --- a/src/components/proxy/proxy-group.tsx +++ b/src/components/proxy/proxy-group.tsx @@ -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(); const proxies = group.all ?? []; + const virtuosoRef = useRef(); + 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) => { - + { > + + + setShowType(!showType)} + > + {showType ? : } + + + setShowFilter(!showFilter)} + > + {showFilter ? : } + + + {showFilter && ( + setFilterText(e.target.value)} + sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} + /> + )} - {proxies.length >= 10 ? ( + {!filterProxies.length && ( + + Empty + + )} + + {filterProxies.length >= 10 ? ( ( )} /> @@ -160,14 +222,15 @@ const ProxyGroup = ({ group }: Props) => { disablePadding sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }} > - {proxies.map((proxy) => ( + {filterProxies.map((proxy) => ( ))} diff --git a/src/components/proxy/proxy-item.tsx b/src/components/proxy/proxy-item.tsx index 88d90cc..17578d7 100644 --- a/src/components/proxy/proxy-item.tsx +++ b/src/components/proxy/proxy-item.tsx @@ -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; 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) => { }, ]} > - + + {proxy.name} + + {showType && {proxy.type}} + {showType && proxy.udp && UDP} + + } + /> ])(\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]); +} diff --git a/src/services/delay.ts b/src/services/delay.ts index 8b2b8f0..8004144 100644 --- a/src/services/delay.ts +++ b/src/services/delay.ts @@ -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();