feat: optimize proxy page ui
This commit is contained in:
parent
6eafb15cf9
commit
a4ce7a4037
@ -1,5 +1,5 @@
|
|||||||
import { alpha, Box, Typography } from "@mui/material";
|
import { alpha, Box, Typography } from "@mui/material";
|
||||||
import { BlurOnRounded } from "@mui/icons-material";
|
import { InboxRounded } from "@mui/icons-material";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
@ -21,7 +21,7 @@ const BaseEmpty = (props: Props) => {
|
|||||||
color: alpha(palette.text.secondary, 0.75),
|
color: alpha(palette.text.secondary, 0.75),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<BlurOnRounded sx={{ fontSize: "4em" }} />
|
<InboxRounded sx={{ fontSize: "4em" }} />
|
||||||
<Typography sx={{ fontSize: "1.25em" }}>{text}</Typography>
|
<Typography sx={{ fontSize: "1.25em" }}>{text}</Typography>
|
||||||
{extra}
|
{extra}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import useSWR, { useSWRConfig } from "swr";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useLockFn } from "ahooks";
|
|
||||||
import { Virtuoso } from "react-virtuoso";
|
|
||||||
import { providerHealthCheck, updateProxy } from "@/services/api";
|
|
||||||
import { getProfiles, patchProfile } from "@/services/cmds";
|
|
||||||
import delayManager from "@/services/delay";
|
|
||||||
import useHeadState from "./use-head-state";
|
|
||||||
import useFilterSort from "./use-filter-sort";
|
|
||||||
import ProxyHead from "./proxy-head";
|
|
||||||
import ProxyItem from "./proxy-item";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
groupName: string;
|
|
||||||
curProxy?: string;
|
|
||||||
proxies: IProxyItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// this component will be used for DIRECT/GLOBAL
|
|
||||||
const ProxyGlobal = (props: Props) => {
|
|
||||||
const { groupName, curProxy, proxies } = props;
|
|
||||||
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
const [now, setNow] = useState(curProxy || "DIRECT");
|
|
||||||
|
|
||||||
const [headState, setHeadState] = useHeadState(groupName);
|
|
||||||
|
|
||||||
const virtuosoRef = useRef<any>();
|
|
||||||
const sortedProxies = useFilterSort(
|
|
||||||
proxies,
|
|
||||||
groupName,
|
|
||||||
headState.filterText,
|
|
||||||
headState.sortType
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: profiles } = useSWR("getProfiles", getProfiles);
|
|
||||||
|
|
||||||
const onChangeProxy = useLockFn(async (name: string) => {
|
|
||||||
await updateProxy(groupName, name);
|
|
||||||
setNow(name);
|
|
||||||
|
|
||||||
if (groupName === "DIRECT") return;
|
|
||||||
|
|
||||||
// update global selected
|
|
||||||
const profile = profiles?.items?.find((p) => p.uid === profiles.current);
|
|
||||||
if (!profile) return;
|
|
||||||
if (!profile.selected) profile.selected = [];
|
|
||||||
|
|
||||||
const index = profile.selected.findIndex((item) => item.name === groupName);
|
|
||||||
if (index < 0) {
|
|
||||||
profile.selected.unshift({ name: groupName, now: name });
|
|
||||||
} else {
|
|
||||||
profile.selected[index] = { name: groupName, now: name };
|
|
||||||
}
|
|
||||||
|
|
||||||
await patchProfile(profiles!.current!, { selected: profile.selected });
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLocation = (smooth = true) => {
|
|
||||||
const index = sortedProxies.findIndex((p) => p.name === now);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
|
||||||
index,
|
|
||||||
align: "center",
|
|
||||||
behavior: smooth ? "smooth" : "auto",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckAll = useLockFn(async () => {
|
|
||||||
const providers = new Set(
|
|
||||||
sortedProxies.map((p) => p.provider!).filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (providers.size) {
|
|
||||||
Promise.allSettled(
|
|
||||||
[...providers].map((p) => providerHealthCheck(p))
|
|
||||||
).then(() => mutate("getProxies"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await delayManager.checkListDelay(
|
|
||||||
sortedProxies.filter((p) => !p.provider).map((p) => p.name),
|
|
||||||
groupName,
|
|
||||||
16
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate("getProxies");
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => onLocation(false), [groupName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (groupName === "DIRECT") setNow("DIRECT");
|
|
||||||
else if (groupName === "GLOBAL") {
|
|
||||||
if (profiles) {
|
|
||||||
const current = profiles.current;
|
|
||||||
const profile = profiles.items?.find((p) => p.uid === current);
|
|
||||||
|
|
||||||
profile?.selected?.forEach((item) => {
|
|
||||||
if (item.name === "GLOBAL") {
|
|
||||||
if (item.now && item.now !== curProxy) {
|
|
||||||
updateProxy("GLOBAL", item.now).then(() => setNow(item!.now!));
|
|
||||||
mutate("getProxies");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setNow(curProxy || "DIRECT");
|
|
||||||
}
|
|
||||||
}, [groupName, curProxy, profiles]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProxyHead
|
|
||||||
sx={{ px: 3, my: 0.5, button: { mr: 0.5 } }}
|
|
||||||
groupName={groupName}
|
|
||||||
headState={headState}
|
|
||||||
onLocation={onLocation}
|
|
||||||
onCheckDelay={onCheckAll}
|
|
||||||
onHeadState={setHeadState}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Virtuoso
|
|
||||||
ref={virtuosoRef}
|
|
||||||
style={{ height: "calc(100% - 40px)" }}
|
|
||||||
totalCount={sortedProxies.length}
|
|
||||||
itemContent={(index) => (
|
|
||||||
<ProxyItem
|
|
||||||
groupName={groupName}
|
|
||||||
proxy={sortedProxies[index]}
|
|
||||||
selected={sortedProxies[index].name === now}
|
|
||||||
showType={headState.showType}
|
|
||||||
onClick={onChangeProxy}
|
|
||||||
sx={{ py: 0, px: 2 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProxyGlobal;
|
|
@ -1,232 +0,0 @@
|
|||||||
import useSWR, { useSWRConfig } from "swr";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useLockFn } from "ahooks";
|
|
||||||
import { Virtuoso } from "react-virtuoso";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Collapse,
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
} from "@mui/material";
|
|
||||||
import {
|
|
||||||
SendRounded,
|
|
||||||
ExpandLessRounded,
|
|
||||||
ExpandMoreRounded,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import {
|
|
||||||
getConnections,
|
|
||||||
providerHealthCheck,
|
|
||||||
updateProxy,
|
|
||||||
deleteConnection,
|
|
||||||
} from "@/services/api";
|
|
||||||
import { getProfiles, patchProfile } from "@/services/cmds";
|
|
||||||
import { useVergeConfig } from "@/hooks/use-verge-config";
|
|
||||||
import delayManager from "@/services/delay";
|
|
||||||
import useHeadState from "./use-head-state";
|
|
||||||
import useFilterSort from "./use-filter-sort";
|
|
||||||
import ProxyHead from "./proxy-head";
|
|
||||||
import ProxyItem from "./proxy-item";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
group: IProxyGroupItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProxyGroup = ({ group }: Props) => {
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
const [now, setNow] = useState(group.now);
|
|
||||||
|
|
||||||
const [headState, setHeadState] = useHeadState(group.name);
|
|
||||||
|
|
||||||
const virtuosoRef = useRef<any>();
|
|
||||||
const sortedProxies = useFilterSort(
|
|
||||||
group.all,
|
|
||||||
group.name,
|
|
||||||
headState.filterText,
|
|
||||||
headState.sortType
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: profiles } = useSWR("getProfiles", getProfiles);
|
|
||||||
const { data: vergeConfig } = useVergeConfig();
|
|
||||||
|
|
||||||
const onChangeProxy = useLockFn(async (name: string) => {
|
|
||||||
// Todo: support another proxy group type
|
|
||||||
if (group.type !== "Selector") return;
|
|
||||||
|
|
||||||
const oldValue = now;
|
|
||||||
try {
|
|
||||||
setNow(name);
|
|
||||||
await updateProxy(group.name, name);
|
|
||||||
|
|
||||||
if (vergeConfig?.auto_close_connection) {
|
|
||||||
getConnections().then((snapshot) => {
|
|
||||||
snapshot.connections.forEach((conn) => {
|
|
||||||
if (conn.chains.includes(oldValue!)) {
|
|
||||||
deleteConnection(conn.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setNow(oldValue);
|
|
||||||
return; // do not update profile
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const profile = profiles?.items?.find((p) => p.uid === profiles.current);
|
|
||||||
if (!profile) return;
|
|
||||||
if (!profile.selected) profile.selected = [];
|
|
||||||
|
|
||||||
const index = profile.selected.findIndex(
|
|
||||||
(item) => item.name === group.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
profile.selected.push({ name: group.name, now: name });
|
|
||||||
} else {
|
|
||||||
profile.selected[index] = { name: group.name, now: name };
|
|
||||||
}
|
|
||||||
await patchProfile(profiles!.current!, { selected: profile.selected });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLocation = (smooth = true) => {
|
|
||||||
const index = sortedProxies.findIndex((p) => p.name === now);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
|
||||||
index,
|
|
||||||
align: "center",
|
|
||||||
behavior: smooth ? "smooth" : "auto",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckAll = useLockFn(async () => {
|
|
||||||
const providers = new Set(
|
|
||||||
sortedProxies.map((p) => p.provider!).filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (providers.size) {
|
|
||||||
Promise.allSettled(
|
|
||||||
[...providers].map((p) => providerHealthCheck(p))
|
|
||||||
).then(() => mutate("getProxies"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await delayManager.checkListDelay(
|
|
||||||
sortedProxies.filter((p) => !p.provider).map((p) => p.name),
|
|
||||||
group.name,
|
|
||||||
16
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate("getProxies");
|
|
||||||
});
|
|
||||||
|
|
||||||
// auto scroll to current index
|
|
||||||
useEffect(() => {
|
|
||||||
if (headState.open) {
|
|
||||||
setTimeout(() => onLocation(false), 10);
|
|
||||||
}
|
|
||||||
}, [headState.open, sortedProxies]);
|
|
||||||
|
|
||||||
// // auto scroll when sorted changed
|
|
||||||
// const timerRef = useRef<any>();
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (headState.open) {
|
|
||||||
// clearTimeout(timerRef.current);
|
|
||||||
// timerRef.current = setTimeout(() => onLocation(false), 500);
|
|
||||||
// }
|
|
||||||
// }, [headState.open, sortedProxies]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ListItem
|
|
||||||
button
|
|
||||||
dense
|
|
||||||
onClick={() => setHeadState({ open: !headState.open })}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={group.name}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<SendRounded color="primary" sx={{ mr: 1, fontSize: 14 }} />
|
|
||||||
<span>{now}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
secondaryTypographyProps={{
|
|
||||||
sx: { display: "flex", alignItems: "center" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{headState.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<Collapse in={headState.open} timeout="auto" unmountOnExit>
|
|
||||||
<ProxyHead
|
|
||||||
sx={{ pl: 4, pr: 3, my: 0.5, button: { mr: 0.5 } }}
|
|
||||||
groupName={group.name}
|
|
||||||
headState={headState}
|
|
||||||
onLocation={onLocation}
|
|
||||||
onCheckDelay={onCheckAll}
|
|
||||||
onHeadState={setHeadState}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!sortedProxies.length && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
py: 3,
|
|
||||||
fontSize: 18,
|
|
||||||
textAlign: "center",
|
|
||||||
color: "text.secondary",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Empty
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sortedProxies.length >= 10 ? (
|
|
||||||
<Virtuoso
|
|
||||||
ref={virtuosoRef}
|
|
||||||
style={{ height: "320px", marginBottom: "4px" }}
|
|
||||||
totalCount={sortedProxies.length}
|
|
||||||
itemContent={(index) => (
|
|
||||||
<ProxyItem
|
|
||||||
groupName={group.name}
|
|
||||||
proxy={sortedProxies[index]}
|
|
||||||
selected={sortedProxies[index].name === now}
|
|
||||||
showType={headState.showType}
|
|
||||||
sx={{ py: 0, pl: 4 }}
|
|
||||||
onClick={onChangeProxy}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<List
|
|
||||||
component="div"
|
|
||||||
disablePadding
|
|
||||||
sx={{ maxHeight: "320px", overflow: "auto", mb: "4px" }}
|
|
||||||
>
|
|
||||||
{sortedProxies.map((proxy) => (
|
|
||||||
<ProxyItem
|
|
||||||
key={proxy.name}
|
|
||||||
groupName={group.name}
|
|
||||||
proxy={proxy}
|
|
||||||
selected={proxy.name === now}
|
|
||||||
showType={headState.showType}
|
|
||||||
sx={{ py: 0, pl: 4 }}
|
|
||||||
onClick={onChangeProxy}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider variant="middle" />
|
|
||||||
</Collapse>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProxyGroup;
|
|
225
src/components/proxy/proxy-groups.tsx
Normal file
225
src/components/proxy/proxy-groups.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||||
|
import {
|
||||||
|
ExpandLessRounded,
|
||||||
|
ExpandMoreRounded,
|
||||||
|
InboxRounded,
|
||||||
|
SendRounded,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
getConnections,
|
||||||
|
providerHealthCheck,
|
||||||
|
updateProxy,
|
||||||
|
deleteConnection,
|
||||||
|
} from "@/services/api";
|
||||||
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
|
import { useVergeConfig } from "@/hooks/use-verge-config";
|
||||||
|
import { useRenderList, type IRenderItem } from "./use-render-list";
|
||||||
|
import { HeadState } from "./use-head-state";
|
||||||
|
import { ProxyHead } from "./proxy-head";
|
||||||
|
import { ProxyItem } from "./proxy-item";
|
||||||
|
import delayManager from "@/services/delay";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProxyGroups = (props: Props) => {
|
||||||
|
const { mode } = props;
|
||||||
|
|
||||||
|
const { renderList, onProxies, onHeadState } = useRenderList(mode);
|
||||||
|
|
||||||
|
const { data: vergeConfig } = useVergeConfig();
|
||||||
|
const { current, patchCurrent } = useProfiles();
|
||||||
|
|
||||||
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
|
|
||||||
|
// 切换分组的节点代理
|
||||||
|
const handleChangeProxy = useLockFn(
|
||||||
|
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||||
|
if (group.type !== "Selector") return;
|
||||||
|
|
||||||
|
const { name, now } = group;
|
||||||
|
await updateProxy(name, proxy.name);
|
||||||
|
onProxies();
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
if (vergeConfig?.auto_close_connection) {
|
||||||
|
getConnections().then(({ connections }) => {
|
||||||
|
connections.forEach((conn) => {
|
||||||
|
if (conn.chains.includes(now!)) {
|
||||||
|
deleteConnection(conn.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到selected中
|
||||||
|
if (!current) return;
|
||||||
|
if (!current.selected) current.selected = [];
|
||||||
|
|
||||||
|
const index = current.selected.findIndex(
|
||||||
|
(item) => item.name === group.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
current.selected.push({ name, now: proxy.name });
|
||||||
|
} else {
|
||||||
|
current.selected[index] = { name, now: proxy.name };
|
||||||
|
}
|
||||||
|
await patchCurrent({ selected: current.selected });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测全部延迟
|
||||||
|
const handleCheckAll = useLockFn(async (groupName: string) => {
|
||||||
|
const proxies = renderList
|
||||||
|
.filter((e) => e.type === 2 && e.group?.name === groupName)
|
||||||
|
.map((e) => e.proxy!)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
|
||||||
|
|
||||||
|
if (providers.size) {
|
||||||
|
Promise.allSettled(
|
||||||
|
[...providers].map((p) => providerHealthCheck(p))
|
||||||
|
).then(() => onProxies());
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
||||||
|
await delayManager.checkListDelay(names, groupName, 24);
|
||||||
|
|
||||||
|
onProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 滚到对应的节点
|
||||||
|
const handleLocation = (group: IProxyGroupItem) => {
|
||||||
|
if (!group) return;
|
||||||
|
const { name, now } = group;
|
||||||
|
|
||||||
|
const index = renderList.findIndex(
|
||||||
|
(e) => e.type === 2 && e.group?.name === name && e.proxy?.name === now
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
virtuosoRef.current?.scrollToIndex?.({
|
||||||
|
index,
|
||||||
|
align: "center",
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
totalCount={renderList.length}
|
||||||
|
itemContent={(index) => (
|
||||||
|
<ProxyRenderItem
|
||||||
|
key={renderList[index].key}
|
||||||
|
item={renderList[index]}
|
||||||
|
indent={mode === "rule" || mode === "script"}
|
||||||
|
onLocation={handleLocation}
|
||||||
|
onCheckAll={handleCheckAll}
|
||||||
|
onHeadState={onHeadState}
|
||||||
|
onChangeProxy={handleChangeProxy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RenderProps {
|
||||||
|
item: IRenderItem;
|
||||||
|
indent: boolean;
|
||||||
|
onLocation: (group: IProxyGroupItem) => void;
|
||||||
|
onCheckAll: (groupName: string) => void;
|
||||||
|
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
|
||||||
|
onChangeProxy: (group: IProxyGroupItem, proxy: IProxyItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProxyRenderItem(props: RenderProps) {
|
||||||
|
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
|
||||||
|
props;
|
||||||
|
const { type, group, headState, proxy } = item;
|
||||||
|
|
||||||
|
if (type === 0) {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
dense
|
||||||
|
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={group.name}
|
||||||
|
secondary={
|
||||||
|
<>
|
||||||
|
<SendRounded color="primary" sx={{ mr: 1, fontSize: 14 }} />
|
||||||
|
{/* <span>{group.type}</span> */}
|
||||||
|
<span>{group.now}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
sx: { display: "flex", alignItems: "center" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 1) {
|
||||||
|
return (
|
||||||
|
<ProxyHead
|
||||||
|
sx={{ pl: indent ? 4.5 : 2.5, pr: 3, my: 1, button: { mr: 0.5 } }}
|
||||||
|
groupName={group.name}
|
||||||
|
headState={headState!}
|
||||||
|
onLocation={() => onLocation(group)}
|
||||||
|
onCheckDelay={() => onCheckAll(group.name)}
|
||||||
|
onHeadState={(p) => onHeadState(group.name, p)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 2) {
|
||||||
|
return (
|
||||||
|
<ProxyItem
|
||||||
|
groupName={group.name}
|
||||||
|
proxy={proxy!}
|
||||||
|
selected={group.now === proxy?.name}
|
||||||
|
showType={headState?.showType}
|
||||||
|
sx={{ py: 0, pl: indent ? 4 : 2 }}
|
||||||
|
onClick={() => onChangeProxy(group, proxy!)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 3) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
pl: indent ? 4.5 : 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InboxRounded sx={{ fontSize: "2.5em", color: "inherit" }} />
|
||||||
|
<Typography sx={{ color: "inherit" }}>No Proxies</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -28,7 +28,7 @@ interface Props {
|
|||||||
onHeadState: (val: Partial<HeadState>) => void;
|
onHeadState: (val: Partial<HeadState>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProxyHead = (props: Props) => {
|
export const ProxyHead = (props: Props) => {
|
||||||
const { sx = {}, groupName, headState, onHeadState } = props;
|
const { sx = {}, groupName, headState, onHeadState } = props;
|
||||||
|
|
||||||
const { showType, sortType, filterText, textState, testUrl } = headState;
|
const { showType, sortType, filterText, textState, testUrl } = headState;
|
||||||
@ -163,5 +163,3 @@ const ProxyHead = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProxyHead;
|
|
||||||
|
@ -42,7 +42,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ProxyItem = (props: Props) => {
|
export const ProxyItem = (props: Props) => {
|
||||||
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
|
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
|
||||||
|
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
@ -174,5 +174,3 @@ const ProxyItem = (props: Props) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProxyItem;
|
|
||||||
|
@ -36,6 +36,17 @@ export default function useFilterSort(
|
|||||||
}, [proxies, groupName, filterText, sortType, refresh]);
|
}, [proxies, groupName, filterText, sortType, refresh]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterSort(
|
||||||
|
proxies: IProxyItem[],
|
||||||
|
groupName: string,
|
||||||
|
filterText: string,
|
||||||
|
sortType: ProxySortType
|
||||||
|
) {
|
||||||
|
const fp = filterProxies(proxies, groupName, filterText);
|
||||||
|
const sp = sortProxies(fp, groupName, sortType);
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可以通过延迟数/节点类型 过滤
|
* 可以通过延迟数/节点类型 过滤
|
||||||
*/
|
*/
|
||||||
|
@ -15,7 +15,7 @@ export interface HeadState {
|
|||||||
type HeadStateStorage = Record<string, Record<string, HeadState>>;
|
type HeadStateStorage = Record<string, Record<string, HeadState>>;
|
||||||
|
|
||||||
const HEAD_STATE_KEY = "proxy-head-state";
|
const HEAD_STATE_KEY = "proxy-head-state";
|
||||||
const DEFAULT_STATE: HeadState = {
|
export const DEFAULT_STATE: HeadState = {
|
||||||
open: false,
|
open: false,
|
||||||
showType: false,
|
showType: false,
|
||||||
sortType: 0,
|
sortType: 0,
|
||||||
@ -78,3 +78,59 @@ export default function useHeadState(groupName: string) {
|
|||||||
|
|
||||||
return [state, setHeadState] as const;
|
return [state, setHeadState] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useHeadStateNew() {
|
||||||
|
const current = useRecoilValue(atomCurrentProfile);
|
||||||
|
|
||||||
|
const [state, setState] = useState<Record<string, HeadState>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!current) {
|
||||||
|
setState({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(
|
||||||
|
localStorage.getItem(HEAD_STATE_KEY)!
|
||||||
|
) as HeadStateStorage;
|
||||||
|
|
||||||
|
const value = data[current] || {};
|
||||||
|
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
setState(value);
|
||||||
|
} else {
|
||||||
|
setState({});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, [current]);
|
||||||
|
|
||||||
|
const setHeadState = useCallback(
|
||||||
|
(groupName: string, obj: Partial<HeadState>) => {
|
||||||
|
setState((old) => {
|
||||||
|
const state = old[groupName] || DEFAULT_STATE;
|
||||||
|
const ret = { ...old, [groupName]: { ...state, ...obj } };
|
||||||
|
|
||||||
|
// 保存到存储中
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const item = localStorage.getItem(HEAD_STATE_KEY);
|
||||||
|
|
||||||
|
let data = (item ? JSON.parse(item) : {}) as HeadStateStorage;
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object") data = {};
|
||||||
|
|
||||||
|
data[current] = ret;
|
||||||
|
|
||||||
|
localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data));
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[current]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [state, setHeadState] as const;
|
||||||
|
}
|
||||||
|
84
src/components/proxy/use-render-list.ts
Normal file
84
src/components/proxy/use-render-list.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import { getProxies } from "@/services/api";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { filterSort } from "./use-filter-sort";
|
||||||
|
import { useHeadStateNew, type HeadState } from "./use-head-state";
|
||||||
|
|
||||||
|
export interface IRenderItem {
|
||||||
|
type: 0 | 1 | 2 | 3; // 组 | head | item | empty
|
||||||
|
key: string;
|
||||||
|
group: IProxyGroupItem;
|
||||||
|
proxy?: IProxyItem;
|
||||||
|
headState?: HeadState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRenderList = (mode: string) => {
|
||||||
|
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
||||||
|
"getProxies",
|
||||||
|
getProxies,
|
||||||
|
{ refreshInterval: 45000, suspense: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
|
|
||||||
|
// make sure that fetch the proxies successfully
|
||||||
|
useEffect(() => {
|
||||||
|
if (!proxiesData) return;
|
||||||
|
const { groups, proxies } = proxiesData;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(mode === "rule" && !groups.length) ||
|
||||||
|
(mode === "global" && proxies.length < 2)
|
||||||
|
) {
|
||||||
|
setTimeout(() => mutateProxies(), 500);
|
||||||
|
}
|
||||||
|
}, [proxiesData, mode]);
|
||||||
|
|
||||||
|
const renderList: IRenderItem[] = useMemo(() => {
|
||||||
|
const useRule = mode === "rule" || mode === "script";
|
||||||
|
const renderGroups =
|
||||||
|
(useRule ? proxiesData?.groups : [proxiesData?.global!]) || [];
|
||||||
|
|
||||||
|
const retList = renderGroups.flatMap((group) => {
|
||||||
|
const headState = headStates[group.name];
|
||||||
|
const ret: IRenderItem[] = [
|
||||||
|
{ type: 0, key: group.name, group, headState },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (headState?.open) {
|
||||||
|
const proxies = filterSort(
|
||||||
|
group.all,
|
||||||
|
group.name,
|
||||||
|
headState.filterText,
|
||||||
|
headState.sortType
|
||||||
|
);
|
||||||
|
|
||||||
|
ret.push({ type: 1, key: `head${group.name}`, group, headState });
|
||||||
|
|
||||||
|
if (!proxies.length) {
|
||||||
|
ret.push({ type: 3, key: `empty${group.name}`, group, headState });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.concat(
|
||||||
|
proxies.map((proxy) => ({
|
||||||
|
type: 2,
|
||||||
|
key: `${group.name}-${proxy!.name}`,
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
headState,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!useRule) return retList.slice(1);
|
||||||
|
return retList;
|
||||||
|
}, [headStates, proxiesData, mode]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderList,
|
||||||
|
onProxies: mutateProxies,
|
||||||
|
onHeadState: setHeadState,
|
||||||
|
};
|
||||||
|
};
|
29
src/hooks/use-profiles.ts
Normal file
29
src/hooks/use-profiles.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import {
|
||||||
|
getProfiles,
|
||||||
|
patchProfile,
|
||||||
|
patchProfilesConfig,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
|
||||||
|
export const useProfiles = () => {
|
||||||
|
const { data: profiles, mutate } = useSWR("getProfiles", getProfiles);
|
||||||
|
|
||||||
|
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
||||||
|
await patchProfilesConfig(value);
|
||||||
|
mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||||
|
if (profiles?.current) {
|
||||||
|
await patchProfile(profiles.current, value);
|
||||||
|
mutate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
profiles,
|
||||||
|
current: profiles?.items?.find((p) => p.uid === profiles.current),
|
||||||
|
patchProfiles,
|
||||||
|
patchCurrent,
|
||||||
|
};
|
||||||
|
};
|
@ -1,62 +1,32 @@
|
|||||||
import useSWR, { useSWRConfig } from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, ButtonGroup, List, Paper } from "@mui/material";
|
import { Button, ButtonGroup, Paper } from "@mui/material";
|
||||||
import { getClashConfig, updateConfigs } from "@/services/api";
|
import { getClashConfig, updateConfigs } from "@/services/api";
|
||||||
import { patchClashConfig } from "@/services/cmds";
|
import { patchClashConfig } from "@/services/cmds";
|
||||||
import { getProxies } from "@/services/api";
|
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||||
import BasePage from "@/components/base/base-page";
|
import BasePage from "@/components/base/base-page";
|
||||||
import BaseEmpty from "@/components/base/base-empty";
|
|
||||||
import ProxyGroup from "@/components/proxy/proxy-group";
|
|
||||||
|
|
||||||
const ProxyPage = () => {
|
const ProxyPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
const { data: proxiesData } = useSWR("getProxies", getProxies, {
|
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||||
refreshInterval: 45000, // 45s
|
"getClashConfig",
|
||||||
});
|
getClashConfig
|
||||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
);
|
||||||
|
|
||||||
const modeList = ["rule", "global", "direct", "script"];
|
const modeList = ["rule", "global", "direct", "script"];
|
||||||
const curMode = clashConfig?.mode.toLowerCase();
|
const curMode = clashConfig?.mode.toLowerCase();
|
||||||
const { global, groups = [], proxies = [] } = proxiesData ?? {};
|
|
||||||
|
|
||||||
// make sure that fetch the proxies successfully
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(curMode === "rule" && !groups.length) ||
|
|
||||||
(curMode === "global" && proxies.length < 2)
|
|
||||||
) {
|
|
||||||
setTimeout(() => mutate("getProxies"), 500);
|
|
||||||
}
|
|
||||||
}, [groups, proxies, curMode]);
|
|
||||||
|
|
||||||
const onChangeMode = useLockFn(async (mode: string) => {
|
const onChangeMode = useLockFn(async (mode: string) => {
|
||||||
// switch rapidly
|
|
||||||
await updateConfigs({ mode });
|
await updateConfigs({ mode });
|
||||||
await patchClashConfig({ mode });
|
await patchClashConfig({ mode });
|
||||||
mutate("getClashConfig");
|
mutateClash();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 仅mode为全局和直连的时候展示global分组
|
|
||||||
const displayGroups = useMemo(() => {
|
|
||||||
if (!global) return groups;
|
|
||||||
if (curMode === "global" || curMode === "direct" || groups.length === 0)
|
|
||||||
return [global, ...groups];
|
|
||||||
return groups;
|
|
||||||
}, [global, groups, curMode]);
|
|
||||||
|
|
||||||
// difference style
|
|
||||||
const showGroup = displayGroups.length > 0;
|
|
||||||
const pageStyle = showGroup ? {} : { height: "100%" };
|
|
||||||
const paperStyle: any = showGroup
|
|
||||||
? { mb: 0.5 }
|
|
||||||
: { py: 1, height: "100%", boxSizing: "border-box" };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
contentStyle={pageStyle}
|
contentStyle={{ height: "100%" }}
|
||||||
title={t("Proxy Groups")}
|
title={t("Proxy Groups")}
|
||||||
header={
|
header={
|
||||||
<ButtonGroup size="small">
|
<ButtonGroup size="small">
|
||||||
@ -73,16 +43,16 @@ const ProxyPage = () => {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Paper sx={{ borderRadius: 1, boxShadow: 2, ...paperStyle }}>
|
<Paper
|
||||||
{displayGroups.length > 0 ? (
|
sx={{
|
||||||
<List>
|
borderRadius: 1,
|
||||||
{displayGroups.map((group) => (
|
boxShadow: 2,
|
||||||
<ProxyGroup key={group.name} group={group} />
|
height: "100%",
|
||||||
))}
|
boxSizing: "border-box",
|
||||||
</List>
|
py: 1,
|
||||||
) : (
|
}}
|
||||||
<BaseEmpty />
|
>
|
||||||
)}
|
<ProxyGroups mode={curMode!} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user