feat: support to change proxy layout column

This commit is contained in:
GyDi 2022-12-13 17:34:39 +08:00
parent 5c5177ec57
commit 4d2b35e09d
No known key found for this signature in database
GPG Key ID: 9C3AD40F1F99880A
7 changed files with 316 additions and 13 deletions

View File

@ -68,6 +68,9 @@ pub struct IVerge {
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@ -117,6 +120,7 @@ impl IVerge {
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
enable_builtin_enhanced: Some(true),
proxy_layout_column: Some(1),
..Self::default()
}
}
@ -159,6 +163,7 @@ impl IVerge {
patch!(auto_close_connection);
patch!(default_latency_test);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
}
/// 在初始化前尝试拿到单例端口的值

View File

@ -68,8 +68,10 @@ export const ProxyGroups = (props: Props) => {
// 测全部延迟
const handleCheckAll = useLockFn(async (groupName: string) => {
const proxies = renderList
.filter((e) => e.type === 2 && e.group?.name === groupName)
.map((e) => e.proxy!)
.filter(
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4)
)
.flatMap((e) => e.proxyCol || e.proxy!)
.filter(Boolean);
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
@ -92,7 +94,10 @@ export const ProxyGroups = (props: Props) => {
const { name, now } = group;
const index = renderList.findIndex(
(e) => e.type === 2 && e.group?.name === name && e.proxy?.name === now
(e) =>
e.group?.name === name &&
((e.type === 2 && e.proxy?.name === now) ||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now)))
);
if (index >= 0) {

View File

@ -0,0 +1,182 @@
import { useEffect, useState } from "react";
import { useLockFn } from "ahooks";
import { CheckCircleOutlineRounded } from "@mui/icons-material";
import {
alpha,
Box,
ListItemButton,
ListItemIcon,
ListItemText,
styled,
} from "@mui/material";
import { BaseLoading } from "@/components/base";
import delayManager from "@/services/delay";
interface Props {
groupName: string;
proxy: IProxyItem;
selected: boolean;
showType?: boolean;
onClick?: (name: string) => void;
}
// 多列布局
export const ProxyItemMini = (props: Props) => {
const { groupName, proxy, selected, showType = true, onClick } = props;
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
};
}, [proxy.name, groupName]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName));
}, [proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName));
});
return (
<ListItemButton
dense
selected={selected}
onClick={() => onClick?.(proxy.name)}
sx={[
{ borderRadius: 1, pl: 1.5, pr: 1 },
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light"
? alpha(primary.main, 0.15)
: alpha(primary.main, 0.35);
const color = mode === "light" ? primary.main : primary.light;
const showDelay = delay > 0;
return {
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
"&:hover .the-icon": { display: "none" },
"&.Mui-selected": { bgcolor },
"&.Mui-selected .MuiListItemText-secondary": { color },
};
},
]}
>
<ListItemText
title={proxy.name}
secondary={
<div>
<div
style={{
textOverflow: "ellipsis",
wordBreak: "break-all",
overflow: "hidden",
whiteSpace: showType ? "nowrap" : "inherit",
}}
>
{proxy.name}
</div>
{showType && (
<>
{!!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
)}
<TypeBox component="span">{proxy.type}</TypeBox>
{proxy.udp && <TypeBox component="span">UDP</TypeBox>}
</>
)}
</div>
}
/>
<ListItemIcon sx={{ justifyContent: "flex-end", color: "primary.main" }}>
{delay === -2 && (
<Widget>
<BaseLoading />
</Widget>
)}
{!proxy.provider && delay !== -2 && (
// provider的节点不支持检测
<Widget
className="the-check"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelay();
}}
sx={({ palette }) => ({
display: "none", // hover才显示
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
})}
>
Check
</Widget>
)}
{delay > 0 && (
// 显示延迟
<Widget
className="the-delay"
onClick={(e) => {
if (proxy.provider) return;
e.preventDefault();
e.stopPropagation();
onDelay();
}}
color={
delay > 500
? "error.main"
: delay < 100
? "success.main"
: "text.secondary"
}
sx={({ palette }) =>
!proxy.provider
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
: {}
}
>
{delay > 1e5 ? "Error" : delay > 3000 ? "Timeout" : `${delay}`}
</Widget>
)}
{delay !== -2 && delay <= 0 && selected && (
// 展示已选择的icon
<CheckCircleOutlineRounded
className="the-icon"
sx={{ fontSize: 16 }}
/>
)}
</ListItemIcon>
</ListItemButton>
);
};
const Widget = styled(Box)(() => ({
padding: "3px 6px",
fontSize: 14,
borderRadius: "4px",
}));
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,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));

View File

@ -14,6 +14,7 @@ import {
import { HeadState } from "./use-head-state";
import { ProxyHead } from "./proxy-head";
import { ProxyItem } from "./proxy-item";
import { ProxyItemMini } from "./proxy-item-mini";
import type { IRenderItem } from "./use-render-list";
interface RenderProps {
@ -28,7 +29,7 @@ interface RenderProps {
export const ProxyRender = (props: RenderProps) => {
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
props;
const { type, group, headState, proxy } = item;
const { type, group, headState, proxy, proxyCol } = item;
if (type === 0) {
return (
@ -105,6 +106,32 @@ export const ProxyRender = (props: RenderProps) => {
);
}
if (type === 4) {
return (
<Box
sx={{
display: "grid",
gap: 1,
pl: indent ? 4 : 2,
pr: 2,
pb: 1,
gridTemplateColumns: `repeat(${item.col! || 2}, 1fr)`,
}}
>
{proxyCol?.map((proxy) => (
<ProxyItemMini
key={item.key + proxy.name}
groupName={group.name}
proxy={proxy!}
selected={group.now === proxy?.name}
showType={headState?.showType}
onClick={() => onChangeProxy(group, proxy!)}
/>
))}
</Box>
);
}
return null;
};

View File

@ -1,6 +1,7 @@
import useSWR from "swr";
import { getProxies } from "@/services/api";
import { useEffect, useMemo } from "react";
import { getProxies } from "@/services/api";
import { useVerge } from "@/hooks/use-verge";
import { filterSort } from "./use-filter-sort";
import {
useHeadStateNew,
@ -9,10 +10,13 @@ import {
} from "./use-head-state";
export interface IRenderItem {
type: 0 | 1 | 2 | 3; // 组 head item empty
// 组 head item empty | item col
type: 0 | 1 | 2 | 3 | 4;
key: string;
group: IProxyGroupItem;
proxy?: IProxyItem;
col?: number;
proxyCol?: IProxyItem[];
headState?: HeadState;
}
@ -23,6 +27,9 @@ export const useRenderList = (mode: string) => {
{ refreshInterval: 45000 }
);
const { verge } = useVerge();
const col = verge?.proxy_layout_column || 1;
const [headStates, setHeadState] = useHeadStateNew();
// make sure that fetch the proxies successfully
@ -62,10 +69,24 @@ export const useRenderList = (mode: string) => {
headState.sortType
);
ret.push({ type: 1, key: `head${group.name}`, group, headState });
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
if (!proxies.length) {
ret.push({ type: 3, key: `empty${group.name}`, group, headState });
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
}
// 支持多列布局
if (col > 1) {
return ret.concat(
groupList(proxies, col).map((proxyCol) => ({
type: 4,
key: `col-${group.name}`,
group,
headState,
col,
proxyCol,
}))
);
}
return ret.concat(
@ -83,7 +104,7 @@ export const useRenderList = (mode: string) => {
if (!useRule) return retList.slice(1);
return retList;
}, [headStates, proxiesData, mode]);
}, [headStates, proxiesData, mode, col]);
return {
renderList,
@ -91,3 +112,18 @@ export const useRenderList = (mode: string) => {
onHeadState: setHeadState,
};
};
function groupList<T = any>(list: T[], size: number): T[][] {
return list.reduce((p, n) => {
if (!p.length) return [[n]];
const i = p.length - 1;
if (p[i].length < size) {
p[i].push(n);
return p;
}
p.push([n]);
return p;
}, [] as T[][]);
}

View File

@ -1,7 +1,15 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { List, ListItem, ListItemText, Switch, TextField } from "@mui/material";
import {
List,
ListItem,
ListItemText,
MenuItem,
Select,
Switch,
TextField,
} from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
@ -12,6 +20,8 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
autoCloseConnection: false,
enableBuiltinEnhanced: true,
proxyLayoutColumn: 1,
defaultLatencyTest: "",
});
@ -19,7 +29,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
open: () => {
setOpen(true);
setValues({
autoCloseConnection: verge?.auto_close_connection || false,
autoCloseConnection: verge?.auto_close_connection ?? false,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 1,
defaultLatencyTest: verge?.default_latency_test || "",
});
},
@ -30,6 +42,8 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
try {
await patchVerge({
auto_close_connection: values.autoCloseConnection,
enable_builtin_enhanced: values.enableBuiltinEnhanced,
proxy_layout_column: values.proxyLayoutColumn,
default_latency_test: values.defaultLatencyTest,
});
setOpen(false);
@ -42,7 +56,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog
open={open}
title={t("Miscellaneous")}
contentSx={{ width: 420 }}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
@ -61,6 +75,38 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Enable Builtin Enhanced" />
<Switch
edge="end"
checked={values.enableBuiltinEnhanced}
onChange={(_, c) =>
setValues((v) => ({ ...v, enableBuiltinEnhanced: c }))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Proxy Layout Column" />
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
value={values.proxyLayoutColumn}
onChange={(e) => {
setValues((v) => ({
...v,
proxyLayoutColumn: e.target.value as number,
}));
}}
>
{[1, 2, 3, 4, 5].map((i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Default Latency Test" />
<TextField
@ -69,7 +115,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 200 }}
sx={{ width: 250 }}
value={values.defaultLatencyTest}
placeholder="http://www.gstatic.com/generate_204"
onChange={(e) =>

View File

@ -163,6 +163,8 @@ interface IVergeConfig {
};
auto_close_connection?: boolean;
default_latency_test?: string;
enable_builtin_enhanced?: boolean;
proxy_layout_column?: number;
}
type IClashConfigValue = any;