From 9df361935f757108d7bdc23abcfb351d808a4700 Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
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<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 }}
           />
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<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);
-
-    let names = proxies.map((p) => p.name);
-    while (names.length) {
-      const list = names.slice(0, 8);
-      names = names.slice(8);
+    const names = filterProxies.map((p) => p.name);
+    const groupName = group.name;
 
-      await Promise.all(
-        list.map((n) => delayManager.checkDelay(n, group.name))
-      );
+    await delayManager.checkListDelay(
+      { names, groupName, skipNum: 8, maxTimeout: 600 },
+      () => mutate("getProxies")
+    );
 
-      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>
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<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" }}
diff --git a/src/components/proxy/use-filter-proxy.ts b/src/components/proxy/use-filter-proxy.ts
new file mode 100644
index 0000000..eb32057
--- /dev/null
+++ b/src/components/proxy/use-filter-proxy.ts
@@ -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]);
+}
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();
-- 
GitLab