diff --git a/package.json b/package.json index 2ddf6be7c07768145ba11d65141e940875e50a4e..1a934a48d553c0a315205dea0bb4a51c86a3fdd2 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-router-dom": "^6.0.2", + "react-virtuoso": "^2.3.1", "recoil": "^0.5.2" }, "devDependencies": { diff --git a/src/components/proxy-group.tsx b/src/components/proxy-group.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6be59f4873e2664f16a8039f097888166efb9365 --- /dev/null +++ b/src/components/proxy-group.tsx @@ -0,0 +1,142 @@ +import { useState } from "react"; +import { Virtuoso } from "react-virtuoso"; +import { + Box, + Collapse, + Divider, + IconButton, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import { + SendRounded, + ExpandLessRounded, + ExpandMoreRounded, + MyLocationRounded, + NetworkCheckRounded, + CheckCircleOutlineRounded, +} from "@mui/icons-material"; +import services from "../services"; +import type { ProxyItem, ProxyGroupItem } from "../services/proxy"; + +interface ItemProps { + proxy: ProxyItem; + selected: boolean; + onClick?: (name: string) => void; +} + +const Item = ({ proxy, selected, onClick }: ItemProps) => { + return ( + <ListItem sx={{ py: 0, pl: 4 }}> + <ListItemButton + selected={selected} + onClick={() => onClick?.(proxy.name)} + sx={{ borderRadius: 1, py: 0.5 }} + > + <ListItemText title={proxy.name} secondary={proxy.name} /> + <ListItemIcon + sx={{ justifyContent: "flex-end", color: "primary.main" }} + > + {selected && <CheckCircleOutlineRounded sx={{ fontSize: 16 }} />} + </ListItemIcon> + </ListItemButton> + </ListItem> + ); +}; + +interface Props { + group: ProxyGroupItem; +} + +const ProxyGroup = ({ group }: Props) => { + const [open, setOpen] = useState(false); + const [now, setNow] = useState(group.now); + + const proxies = group.all ?? []; + + const onUpdate = async (name: string) => { + // can not call update + if (group.type !== "Selector") { + // Todo + // error Tips + return; + } + const oldValue = now; + try { + setNow(name); + await services.updateProxy(group.name, name); + } catch { + setNow(oldValue); + // Todo + // error tips + } + }; + + return ( + <> + <ListItem button onClick={() => setOpen(!open)}> + <ListItemText + primary={group.name} + secondary={ + <> + <SendRounded color="primary" sx={{ mr: 1, fontSize: 14 }} /> + <span>{now}</span> + </> + } + secondaryTypographyProps={{ + sx: { display: "flex", alignItems: "center" }, + }} + /> + + {open ? <ExpandLessRounded /> : <ExpandMoreRounded />} + </ListItem> + + <Collapse in={open} timeout="auto" unmountOnExit> + <Box sx={{ pl: 4, pr: 3, my: 0.5 }}> + <IconButton size="small" title="location"> + <MyLocationRounded /> + </IconButton> + <IconButton size="small" title="check"> + <NetworkCheckRounded /> + </IconButton> + </Box> + + {proxies.length >= 10 ? ( + <Virtuoso + style={{ height: "400px", marginBottom: "4px" }} + totalCount={proxies.length} + itemContent={(index) => ( + <Item + proxy={proxies[index]} + selected={proxies[index].name === now} + onClick={onUpdate} + /> + )} + /> + ) : ( + <List + component="div" + disablePadding + sx={{ maxHeight: "400px", overflow: "auto", mb: "4px" }} + > + {proxies.map((proxy) => ( + <Item + key={proxy.name} + proxy={proxy} + selected={proxy.name === now} + onClick={onUpdate} + /> + ))} + </List> + )} + + <Divider variant="middle" /> + </Collapse> + </> + ); +}; + +export default ProxyGroup; diff --git a/src/pages/proxy.tsx b/src/pages/proxy.tsx index 2808614d425710f089180c4022f4c5d9f5585628..2bbcbcc051dc4da3a3829704e846ebb32d5a442e 100644 --- a/src/pages/proxy.tsx +++ b/src/pages/proxy.tsx @@ -1,5 +1,33 @@ +import { useEffect, useState } from "react"; +import { Box, List, Typography } from "@mui/material"; +import services from "../services"; +import ProxyGroup from "../components/proxy-group"; +import type { ProxyGroupItem } from "../services/proxy"; + const ProxyPage = () => { - return <h1>Proxy</h1>; + const [groups, setGroups] = useState<ProxyGroupItem[]>([]); + + useEffect(() => { + // Todo + // result cache + services.getProxyInfo().then((res) => { + setGroups(res.groups); + }); + }, []); + + return ( + <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}> + <Typography variant="h4" component="h1" sx={{ py: 2 }}> + Proxy Groups + </Typography> + + <List sx={{ borderRadius: 1, boxShadow: 2 }}> + {groups.map((group) => ( + <ProxyGroup key={group.name} group={group} /> + ))} + </List> + </Box> + ); }; export default ProxyPage; diff --git a/src/services/index.ts b/src/services/index.ts index a15cef0bde17916d2e0a58df508065943f506375..6832043ac008cbb3f4e8b0089939f31dc03f9c89 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,7 @@ +import * as proxy from "./proxy"; import * as traffic from "./traffic"; export default { + ...proxy, ...traffic, }; diff --git a/src/services/proxy.ts b/src/services/proxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..452926ea8ae499a22c2c6d188c686b68dd54ba23 --- /dev/null +++ b/src/services/proxy.ts @@ -0,0 +1,47 @@ +import axiosIns from "./base"; + +export interface ProxyItem { + name: string; + type: string; + udp: boolean; + history: { + time: string; + delay: number; + }[]; + all?: string[]; + now?: string; +} + +export type ProxyGroupItem = Omit<ProxyItem, "all" | "now"> & { + all?: ProxyItem[]; + now?: string; +}; + +/// Get the Proxy infomation +export async function getProxyInfo() { + const response = (await axiosIns.get("/proxies")) as any; + const results = (response?.proxies ?? {}) as Record<string, ProxyItem>; + + const global = results["GLOBAL"] || results["global"]; + const proxies = Object.values(results).filter((each) => each.all == null); + + const groups = Object.values(results).filter( + (each) => each.name.toLocaleUpperCase() !== "GLOBAL" && each.all != null + ) as ProxyGroupItem[]; + + groups.forEach((each) => { + // @ts-ignore + each.all = each.all?.map((item) => results[item]).filter((e) => e); + }); + + return { + global, + groups, + proxies, + }; +} + +/// Update the Proxy Choose +export async function updateProxy(group: string, proxy: string) { + return axiosIns.put(`/proxies/${group}`, { name: proxy }); +}