feat: support to change proxy layout column
This commit is contained in:
parent
5c5177ec57
commit
4d2b35e09d
@ -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);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
@ -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) {
|
||||
|
182
src/components/proxy/proxy-item-mini.tsx
Normal file
182
src/components/proxy/proxy-item-mini.tsx
Normal 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,
|
||||
}));
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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[][]);
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
2
src/services/types.d.ts
vendored
2
src/services/types.d.ts
vendored
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user