mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat(ui): add left menu lock/unlock with reorder mode and context menu (#5168)
* feat: free menu * feat(ui): add left menu lock/unlock with reorder mode and context menu * docs: UPDATELOG.md
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
|
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
|
||||||
- 允许独立控制订阅自动更新
|
- 允许独立控制订阅自动更新
|
||||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||||
|
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||||
|
|
||||||
### 🚀 优化改进
|
### 🚀 优化改进
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ pub struct IVerge {
|
|||||||
/// menu icon
|
/// menu icon
|
||||||
pub menu_icon: Option<String>,
|
pub menu_icon: Option<String>,
|
||||||
|
|
||||||
|
/// menu order
|
||||||
|
pub menu_order: Option<Vec<String>>,
|
||||||
|
|
||||||
/// sysproxy tray icon
|
/// sysproxy tray icon
|
||||||
pub sysproxy_tray_icon: Option<bool>,
|
pub sysproxy_tray_icon: Option<bool>,
|
||||||
|
|
||||||
@@ -456,6 +459,7 @@ impl IVerge {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
patch!(tray_icon);
|
patch!(tray_icon);
|
||||||
patch!(menu_icon);
|
patch!(menu_icon);
|
||||||
|
patch!(menu_order);
|
||||||
patch!(common_tray_icon);
|
patch!(common_tray_icon);
|
||||||
patch!(sysproxy_tray_icon);
|
patch!(sysproxy_tray_icon);
|
||||||
patch!(tun_tray_icon);
|
patch!(tun_tray_icon);
|
||||||
@@ -554,6 +558,7 @@ pub struct IVergeResponse {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub tray_icon: Option<String>,
|
pub tray_icon: Option<String>,
|
||||||
pub menu_icon: Option<String>,
|
pub menu_icon: Option<String>,
|
||||||
|
pub menu_order: Option<Vec<String>>,
|
||||||
pub sysproxy_tray_icon: Option<bool>,
|
pub sysproxy_tray_icon: Option<bool>,
|
||||||
pub tun_tray_icon: Option<bool>,
|
pub tun_tray_icon: Option<bool>,
|
||||||
pub enable_tun_mode: Option<bool>,
|
pub enable_tun_mode: Option<bool>,
|
||||||
@@ -629,6 +634,7 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
tray_icon: verge.tray_icon,
|
tray_icon: verge.tray_icon,
|
||||||
menu_icon: verge.menu_icon,
|
menu_icon: verge.menu_icon,
|
||||||
|
menu_order: verge.menu_order,
|
||||||
sysproxy_tray_icon: verge.sysproxy_tray_icon,
|
sysproxy_tray_icon: verge.sysproxy_tray_icon,
|
||||||
tun_tray_icon: verge.tun_tray_icon,
|
tun_tray_icon: verge.tun_tray_icon,
|
||||||
enable_tun_mode: verge.enable_tun_mode,
|
enable_tun_mode: verge.enable_tun_mode,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import type {
|
||||||
|
DraggableAttributes,
|
||||||
|
DraggableSyntheticListeners,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -5,24 +9,46 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
import { useMatch, useResolvedPath, useNavigate } from "react-router";
|
import { useMatch, useResolvedPath, useNavigate } from "react-router";
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
|
interface SortableProps {
|
||||||
|
setNodeRef?: (element: HTMLElement | null) => void;
|
||||||
|
attributes?: DraggableAttributes;
|
||||||
|
listeners?: DraggableSyntheticListeners;
|
||||||
|
style?: CSSProperties;
|
||||||
|
isDragging?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
to: string;
|
to: string;
|
||||||
children: string;
|
children: string;
|
||||||
icon: React.ReactNode[];
|
icon: ReactNode[];
|
||||||
|
sortable?: SortableProps;
|
||||||
}
|
}
|
||||||
export const LayoutItem = (props: Props) => {
|
export const LayoutItem = (props: Props) => {
|
||||||
const { to, children, icon } = props;
|
const { to, children, icon, sortable } = props;
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { menu_icon } = verge ?? {};
|
const { menu_icon } = verge ?? {};
|
||||||
const resolved = useResolvedPath(to);
|
const resolved = useResolvedPath(to);
|
||||||
const match = useMatch({ path: resolved.pathname, end: true });
|
const match = useMatch({ path: resolved.pathname, end: true });
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { setNodeRef, attributes, listeners, style, isDragging } =
|
||||||
|
sortable ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem sx={{ py: 0.5, maxWidth: 250, mx: "auto", padding: "4px 0px" }}>
|
<ListItem
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
sx={[
|
||||||
|
{ py: 0.5, maxWidth: 250, mx: "auto", padding: "4px 0px" },
|
||||||
|
isDragging ? { opacity: 0.78 } : {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={!!match}
|
selected={!!match}
|
||||||
sx={[
|
sx={[
|
||||||
@@ -32,6 +58,7 @@ export const LayoutItem = (props: Props) => {
|
|||||||
paddingLeft: 1,
|
paddingLeft: 1,
|
||||||
paddingRight: 1,
|
paddingRight: 1,
|
||||||
marginRight: 1.25,
|
marginRight: 1.25,
|
||||||
|
cursor: sortable && !sortable.disabled ? "grab" : "pointer",
|
||||||
"& .MuiListItemText-primary": {
|
"& .MuiListItemText-primary": {
|
||||||
color: "text.primary",
|
color: "text.primary",
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
@@ -52,6 +79,8 @@ export const LayoutItem = (props: Props) => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onClick={() => navigate(to)}
|
onClick={() => navigate(to)}
|
||||||
|
{...(attributes ?? {})}
|
||||||
|
{...(listeners ?? {})}
|
||||||
>
|
>
|
||||||
{(menu_icon === "monochrome" || !menu_icon) && (
|
{(menu_icon === "monochrome" || !menu_icon) && (
|
||||||
<ListItemIcon sx={{ color: "text.primary", marginLeft: "6px" }}>
|
<ListItemIcon sx={{ color: "text.primary", marginLeft: "6px" }}>
|
||||||
|
|||||||
@@ -708,5 +708,8 @@
|
|||||||
"Prefer System Titlebar": "Prefer System Titlebar",
|
"Prefer System Titlebar": "Prefer System Titlebar",
|
||||||
"App Log Max Size": "App Log Max Size",
|
"App Log Max Size": "App Log Max Size",
|
||||||
"App Log Max Count": "App Log Max Count",
|
"App Log Max Count": "App Log Max Count",
|
||||||
"Allow Auto Update": "Allow Auto Update"
|
"Allow Auto Update": "Allow Auto Update",
|
||||||
|
"Menu reorder mode": "Menu reorder mode",
|
||||||
|
"Unlock menu order": "Unlock menu order",
|
||||||
|
"Lock menu order": "Lock menu order"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,5 +708,8 @@
|
|||||||
"Prefer System Titlebar": "优先使用系统标题栏",
|
"Prefer System Titlebar": "优先使用系统标题栏",
|
||||||
"App Log Max Size": "应用日志最大大小",
|
"App Log Max Size": "应用日志最大大小",
|
||||||
"App Log Max Count": "应用日志最大数量",
|
"App Log Max Count": "应用日志最大数量",
|
||||||
"Allow Auto Update": "允许自动更新"
|
"Allow Auto Update": "允许自动更新",
|
||||||
|
"Menu reorder mode": "菜单排序模式",
|
||||||
|
"Unlock menu order": "解锁菜单排序",
|
||||||
|
"Lock menu order": "锁定菜单排序"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
import { List, Paper, SvgIcon, ThemeProvider } from "@mui/material";
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
useSortable,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
SvgIcon,
|
||||||
|
ThemeProvider,
|
||||||
|
} from "@mui/material";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
@@ -16,11 +48,7 @@ import { LayoutItem } from "@/components/layout/layout-item";
|
|||||||
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
||||||
import { UpdateButton } from "@/components/layout/update-button";
|
import { UpdateButton } from "@/components/layout/update-button";
|
||||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
import { useLogData } from "@/hooks/use-log-data";
|
|
||||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
|
||||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { useWindowDecorations } from "@/hooks/use-window";
|
import { useWindowDecorations } from "@/hooks/use-window";
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from "@/services/states";
|
||||||
@@ -37,29 +65,217 @@ import "dayjs/locale/zh-cn";
|
|||||||
|
|
||||||
export const portableFlag = false;
|
export const portableFlag = false;
|
||||||
|
|
||||||
|
type NavItem = (typeof navItems)[number];
|
||||||
|
|
||||||
|
const createNavLookup = (items: NavItem[]) => {
|
||||||
|
const map = new Map(items.map((item) => [item.path, item]));
|
||||||
|
const defaultOrder = items.map((item) => item.path);
|
||||||
|
return { map, defaultOrder };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveMenuOrder = (
|
||||||
|
order: string[] | null | undefined,
|
||||||
|
defaultOrder: string[],
|
||||||
|
map: Map<string, NavItem>,
|
||||||
|
) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const resolved: string[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(order)) {
|
||||||
|
for (const path of order) {
|
||||||
|
if (map.has(path) && !seen.has(path)) {
|
||||||
|
resolved.push(path);
|
||||||
|
seen.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of defaultOrder) {
|
||||||
|
if (!seen.has(path)) {
|
||||||
|
resolved.push(path);
|
||||||
|
seen.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
};
|
||||||
|
|
||||||
|
const areOrdersEqual = (a: string[], b: string[]) =>
|
||||||
|
a.length === b.length && a.every((value, index) => value === b[index]);
|
||||||
|
|
||||||
|
type MenuContextPosition = { top: number; left: number };
|
||||||
|
type MenuOrderAction = { type: "sync"; payload: string[] };
|
||||||
|
|
||||||
|
const menuOrderReducer = (state: string[], action: MenuOrderAction) => {
|
||||||
|
const next = action.payload;
|
||||||
|
if (areOrdersEqual(state, next)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return [...next];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SortableNavMenuItemProps {
|
||||||
|
item: NavItem;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableNavMenuItem = ({ item, label }: SortableNavMenuItemProps) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: item.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
style.zIndex = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutItem
|
||||||
|
to={item.path}
|
||||||
|
icon={item.icon}
|
||||||
|
sortable={{
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
style,
|
||||||
|
isDragging,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</LayoutItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const OS = getSystem();
|
const OS = getSystem();
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const trafficData = useTrafficData();
|
|
||||||
const memoryData = useMemoryData();
|
|
||||||
const connectionData = useConnectionData();
|
|
||||||
const logData = useLogData();
|
|
||||||
|
|
||||||
const mode = useThemeMode();
|
const mode = useThemeMode();
|
||||||
const isDark = mode !== "light";
|
const isDark = mode !== "light";
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
const { verge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
const { language } = verge ?? {};
|
const { language } = verge ?? {};
|
||||||
const { switchLanguage } = useI18n();
|
const { switchLanguage } = useI18n();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const themeReady = useMemo(() => Boolean(theme), [theme]);
|
const themeReady = useMemo(() => Boolean(theme), [theme]);
|
||||||
|
|
||||||
|
const [menuUnlocked, setMenuUnlocked] = useState(false);
|
||||||
|
const [menuContextPosition, setMenuContextPosition] =
|
||||||
|
useState<MenuContextPosition | null>(null);
|
||||||
|
|
||||||
const windowControls = useRef<any>(null);
|
const windowControls = useRef<any>(null);
|
||||||
const { decorated } = useWindowDecorations();
|
const { decorated } = useWindowDecorations();
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 6,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { map: navItemMap, defaultOrder: defaultMenuOrder } = useMemo(
|
||||||
|
() => createNavLookup(navItems),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const configMenuOrder = useMemo(
|
||||||
|
() => resolveMenuOrder(verge?.menu_order, defaultMenuOrder, navItemMap),
|
||||||
|
[verge?.menu_order, defaultMenuOrder, navItemMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [menuOrder, dispatchMenuOrder] = useReducer(
|
||||||
|
menuOrderReducer,
|
||||||
|
configMenuOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatchMenuOrder({ type: "sync", payload: configMenuOrder });
|
||||||
|
}, [configMenuOrder]);
|
||||||
|
|
||||||
|
const handleMenuDragEnd = useCallback(
|
||||||
|
async (event: DragEndEvent) => {
|
||||||
|
if (!menuUnlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeId = String(active.id);
|
||||||
|
const overId = String(over.id);
|
||||||
|
|
||||||
|
const oldIndex = menuOrder.indexOf(activeId);
|
||||||
|
const newIndex = menuOrder.indexOf(overId);
|
||||||
|
|
||||||
|
if (oldIndex === -1 || newIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousOrder = [...menuOrder];
|
||||||
|
const nextOrder = arrayMove(menuOrder, oldIndex, newIndex);
|
||||||
|
|
||||||
|
dispatchMenuOrder({ type: "sync", payload: nextOrder });
|
||||||
|
mutateVerge(
|
||||||
|
(prev) => (prev ? { ...prev, menu_order: nextOrder } : prev),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await patchVerge({ menu_order: nextOrder });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update menu order:", error);
|
||||||
|
dispatchMenuOrder({ type: "sync", payload: previousOrder });
|
||||||
|
mutateVerge(
|
||||||
|
(prev) => (prev ? { ...prev, menu_order: previousOrder } : prev),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[menuUnlocked, menuOrder, mutateVerge, patchVerge],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMenuContextMenu = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setMenuContextPosition({ top: event.clientY, left: event.clientX });
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMenuContextClose = useCallback(() => {
|
||||||
|
setMenuContextPosition(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUnlockMenu = useCallback(() => {
|
||||||
|
setMenuUnlocked(true);
|
||||||
|
setMenuContextPosition(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLockMenu = useCallback(() => {
|
||||||
|
setMenuUnlocked(false);
|
||||||
|
setMenuContextPosition(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const customTitlebar = useMemo(
|
const customTitlebar = useMemo(
|
||||||
() =>
|
() =>
|
||||||
!decorated ? (
|
!decorated ? (
|
||||||
@@ -202,17 +418,105 @@ const Layout = () => {
|
|||||||
<UpdateButton className="the-newbtn" />
|
<UpdateButton className="the-newbtn" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List className="the-menu">
|
{menuUnlocked && (
|
||||||
{navItems.map((router) => (
|
<Box
|
||||||
<LayoutItem
|
sx={(theme) => ({
|
||||||
key={router.label}
|
px: 1.5,
|
||||||
to={router.path}
|
py: 0.75,
|
||||||
icon={router.icon}
|
mx: "auto",
|
||||||
>
|
mb: 1,
|
||||||
{t(router.label)}
|
maxWidth: 250,
|
||||||
</LayoutItem>
|
borderRadius: 1.5,
|
||||||
))}
|
fontSize: 12,
|
||||||
</List>
|
fontWeight: 600,
|
||||||
|
textAlign: "center",
|
||||||
|
color: theme.palette.warning.contrastText,
|
||||||
|
bgcolor:
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.warning.main
|
||||||
|
: theme.palette.warning.dark,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t("Menu reorder mode")}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{menuUnlocked ? (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleMenuDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext items={menuOrder}>
|
||||||
|
<List
|
||||||
|
className="the-menu"
|
||||||
|
onContextMenu={handleMenuContextMenu}
|
||||||
|
>
|
||||||
|
{menuOrder.map((path) => {
|
||||||
|
const item = navItemMap.get(path);
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SortableNavMenuItem
|
||||||
|
key={item.path}
|
||||||
|
item={item}
|
||||||
|
label={t(item.label)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
className="the-menu"
|
||||||
|
onContextMenu={handleMenuContextMenu}
|
||||||
|
>
|
||||||
|
{menuOrder.map((path) => {
|
||||||
|
const item = navItemMap.get(path);
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LayoutItem
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
icon={item.icon}
|
||||||
|
>
|
||||||
|
{t(item.label)}
|
||||||
|
</LayoutItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
open={Boolean(menuContextPosition)}
|
||||||
|
onClose={handleMenuContextClose}
|
||||||
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition={
|
||||||
|
menuContextPosition
|
||||||
|
? {
|
||||||
|
top: menuContextPosition.top,
|
||||||
|
left: menuContextPosition.left,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
transitionDuration={200}
|
||||||
|
slotProps={{
|
||||||
|
list: {
|
||||||
|
sx: { py: 0.5 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={menuUnlocked ? handleLockMenu : handleUnlockMenu}
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
{menuUnlocked ? t("Lock menu order") : t("Unlock menu order")}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
<div className="the-traffic">
|
<div className="the-traffic">
|
||||||
<LayoutTraffic />
|
<LayoutTraffic />
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const AppDataProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addWindowListener = (eventName: string, handler: EventListener) => {
|
const addWindowListener = (eventName: string, handler: EventListener) => {
|
||||||
|
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
|
||||||
window.addEventListener(eventName, handler);
|
window.addEventListener(eventName, handler);
|
||||||
return () => window.removeEventListener(eventName, handler);
|
return () => window.removeEventListener(eventName, handler);
|
||||||
};
|
};
|
||||||
|
|||||||
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@@ -797,6 +797,7 @@ interface IVergeConfig {
|
|||||||
enable_memory_usage?: boolean;
|
enable_memory_usage?: boolean;
|
||||||
enable_group_icon?: boolean;
|
enable_group_icon?: boolean;
|
||||||
menu_icon?: "monochrome" | "colorful" | "disable";
|
menu_icon?: "monochrome" | "colorful" | "disable";
|
||||||
|
menu_order?: string[];
|
||||||
tray_icon?: "monochrome" | "colorful";
|
tray_icon?: "monochrome" | "colorful";
|
||||||
common_tray_icon?: boolean;
|
common_tray_icon?: boolean;
|
||||||
sysproxy_tray_icon?: boolean;
|
sysproxy_tray_icon?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user