ui: update menu bar

This commit is contained in:
wj-xiao
2026-01-08 14:45:50 +08:00
parent 0e00760474
commit 6ef8bb74e3
7 changed files with 160 additions and 94 deletions

View File

@@ -214,7 +214,7 @@ const zh = {
languageDesc: '选择界面语言',
menuBar: '菜单栏',
menuBarDesc: '是否在菜单栏中显示图标',
webTitle: '网标题',
webTitle: '网标题',
webTitleDesc: '自定义网站页面标题'
},
device: {

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Divider, Modal, Segmented } from 'antd';
import { Divider, Modal, Segmented, Tooltip } from 'antd';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { DiscIcon, HardDriveIcon } from 'lucide-react';
@@ -61,15 +61,17 @@ export const Image = () => {
return (
<>
<div
className={clsx(
'flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700',
isMounted ? 'text-blue-500' : 'text-neutral-300 hover:text-white'
)}
onClick={() => toggleModal(true)}
>
<DiscIcon size={18} />
</div>
<Tooltip title={t('image.title')} placement="bottom" mouseEnterDelay={0.6}>
<div
className={clsx(
'flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700',
isMounted ? 'text-blue-500' : 'text-neutral-300 hover:text-white'
)}
onClick={() => toggleModal(true)}
>
<DiscIcon size={18} />
</div>
</Tooltip>
<Modal open={isModalOpen} footer={null} onCancel={() => toggleModal(false)}>
<div className="flex items-center space-x-1">

View File

@@ -1,10 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { Divider, Tooltip } from 'antd';
import { Divider } from 'antd';
import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai';
import { MenuIcon, XIcon } from 'lucide-react';
import { GripVerticalIcon } from 'lucide-react';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { useTranslation } from 'react-i18next';
import { getMenuDisabledItems } from '@/lib/localstorage.ts';
import { menuDisabledItemsAtom, submenuOpenCountAtom } from '@/jotai/settings.ts';
@@ -14,6 +13,7 @@ import { Fullscreen } from './fullscreen';
import { Image } from './image';
import { Keyboard } from './keyboard';
import { Mouse } from './mouse';
import { Collapse, Expand } from './operations';
import { Power } from './power';
import { Screen } from './screen';
import { Script } from './script';
@@ -21,39 +21,29 @@ import { Settings } from './settings';
import { Terminal } from './terminal';
import { Wol } from './wol';
const HIDE_TIMEOUT = 8000;
export const Menu = () => {
const { t } = useTranslation();
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
const submenuOpenCount = useAtomValue(submenuOpenCountAtom);
const [isMenuExpanded, setIsMenuExpanded] = useState(true);
const [isMenuMoved, setIsMenuMoved] = useState(false);
const [isMenuHovered, setIsMenuHovered] = useState(false);
const [isMenuHidden, setIsMenuHidden] = useState(false);
const [bounds, setBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });
const [menuBounds, setMenuBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });
const nodeRef = useRef<HTMLDivElement | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// init menu
useEffect(() => {
const items = getMenuDisabledItems();
setMenuDisabledItems(items);
// react-draggable bounds
const handleResize = () => {
if (!nodeRef.current) return;
const elementRect = nodeRef.current.getBoundingClientRect();
const width = (window.innerWidth - elementRect.width) / 2;
setBounds({
left: -width,
top: -10,
right: width,
bottom: window.innerHeight - elementRect.height - 10
});
};
startCountdown();
handleResize();
window.addEventListener('resize', handleResize);
return () => {
@@ -62,24 +52,47 @@ export const Menu = () => {
};
}, []);
// handle menu bounds
useEffect(() => {
if (submenuOpenCount === 0) {
startCountdown();
} else {
stopCountdown();
}
}, [submenuOpenCount]);
handleResize();
}, [menuDisabledItems, isMenuExpanded]);
function startCountdown() {
if (submenuOpenCount > 0) {
// handle hide timer
useEffect(() => {
if (submenuOpenCount === 0 && !isMenuHovered) {
startCountdown();
return;
}
stopCountdown();
}, [submenuOpenCount, isMenuHovered]);
function handleResize() {
if (!nodeRef.current) return;
const elementRect = nodeRef.current.getBoundingClientRect();
const width = (window.innerWidth - elementRect.width) / 2;
setMenuBounds({
left: -width,
top: -10,
right: width,
bottom: window.innerHeight - elementRect.height - 10
});
}
function startCountdown() {
if (submenuOpenCount > 0 || !isMenuExpanded || isMenuMoved) {
return;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
setIsMenuHidden(true);
}, 5000);
}, HIDE_TIMEOUT);
}
function stopCountdown() {
@@ -89,68 +102,77 @@ export const Menu = () => {
}
}
function handleDraggableStop(_e: DraggableEvent, data: DraggableData) {
if (data.x !== 0 || data.y !== 0) {
setIsMenuMoved(true);
function handleMoved(_e: DraggableEvent, data: DraggableData) {
if (data.x === 0 && data.y === 0) return;
if (isMenuMoved) return;
setIsMenuMoved(true);
}
function handleHovered(hovered: boolean) {
setIsMenuHovered(hovered);
if (hovered) {
setIsMenuHidden(false);
}
}
function handleMouseEnter() {
stopCountdown();
setIsMenuHidden(false);
function isEnabled(item: string) {
return !menuDisabledItems.includes(item);
}
return (
<Draggable
nodeRef={nodeRef}
bounds={bounds}
bounds={menuBounds}
handle="strong"
positionOffset={{ x: '-50%', y: '0%' }}
onStop={handleDraggableStop}
onStop={handleMoved}
>
<div
ref={nodeRef}
className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2"
onMouseEnter={handleMouseEnter}
onMouseLeave={startCountdown}
onMouseEnter={() => handleHovered(true)}
onMouseLeave={() => handleHovered(false)}
onBlur={() => handleHovered(false)}
>
{/* Trigger area for auto-show when hidden */}
{isMenuExpanded && (
<div className="absolute -top-[10px] left-0 right-0 h-[60px] w-full bg-transparent" />
<div className="absolute -top-[10px] left-0 right-0 h-[46px] w-full bg-transparent" />
)}
{/* Menubar */}
<div className="sticky top-[10px] flex w-full justify-center">
<div
className={clsx(
'h-[36px] items-center rounded bg-neutral-800/80 transition-all duration-300',
'h-[36px] items-center rounded bg-neutral-800/80 pl-1 pr-2 transition-all duration-300',
isMenuExpanded ? 'flex' : 'hidden',
isMenuExpanded && !isMenuMoved && isMenuHidden
? '-translate-y-[110%] opacity-80'
: 'translate-y-0 opacity-100'
isMenuHidden ? '-translate-y-[110%] opacity-80' : 'translate-y-0 opacity-100'
)}
>
<strong>
<div className="hidden h-[30px] cursor-move select-none items-center px-3 sm:flex">
<img src="/sipeed.ico" width={18} height={18} draggable={false} alt="sipeed" />
<div className="flex h-[30px] cursor-move select-none items-center justify-center pl-1 text-neutral-500">
<GripVerticalIcon size={18} />
</div>
</strong>
<Divider type="vertical" />
<Screen />
<Keyboard />
<Mouse />
<Divider type="vertical" />
{!menuDisabledItems.includes('image') && <Image />}
{!menuDisabledItems.includes('download') && <DownloadImage />}
{!menuDisabledItems.includes('script') && <Script />}
{!menuDisabledItems.includes('terminal') && <Terminal />}
{!menuDisabledItems.includes('wol') && <Wol />}
{isEnabled('image') && <Image />}
{isEnabled('download') && <DownloadImage />}
{isEnabled('script') && <Script />}
{isEnabled('terminal') && <Terminal />}
{isEnabled('wol') && <Wol />}
{['image', 'script', 'terminal', 'wol', 'download'].some(
(key) => !menuDisabledItems.includes(key)
) && <Divider type="vertical" />}
{['image', 'download', 'script', 'terminal', 'wol'].some(isEnabled) && (
<Divider type="vertical" />
)}
{!menuDisabledItems.includes('power') && (
{isEnabled('power') && (
<>
<Power />
<Divider type="vertical" />
@@ -158,29 +180,13 @@ export const Menu = () => {
)}
<Settings />
<Fullscreen />
<Tooltip title={t('menu.collapse')} placement="bottom" mouseEnterDelay={0.6}>
<div
className="mr-1 flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/80 hover:text-white"
onClick={() => setIsMenuExpanded((expanded) => !expanded)}
>
<XIcon size={20} />
</div>
</Tooltip>
{isEnabled('fullscreen') && <Fullscreen />}
{isEnabled('collapse') && <Collapse toggleMenu={setIsMenuExpanded} />}
</div>
</div>
{!isMenuExpanded && (
<Tooltip title={t('menu.expand')} placement="bottom" mouseEnterDelay={0.6}>
<div
className="flex h-[30px] w-[32px] cursor-pointer items-center justify-center rounded bg-neutral-800/50 text-white/50 hover:bg-neutral-700 hover:text-white"
onClick={() => setIsMenuExpanded((expanded) => !expanded)}
>
<MenuIcon size={20} />
</div>
</Tooltip>
)}
{/* Menubar expand button */}
{!isMenuExpanded && <Expand toggleMenu={setIsMenuExpanded} />}
</div>
</Draggable>
);

View File

@@ -0,0 +1,22 @@
import { Tooltip } from 'antd';
import { XIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface CollapseProps {
toggleMenu(expanded: boolean): void;
}
export const Collapse = ({ toggleMenu }: CollapseProps) => {
const { t } = useTranslation();
return (
<Tooltip title={t('menu.collapse')} placement="bottom" mouseEnterDelay={0.6}>
<div
className="flex size-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/80 hover:text-white"
onClick={() => toggleMenu(false)}
>
<XIcon size={18} />
</div>
</Tooltip>
);
};

View File

@@ -0,0 +1,27 @@
import { Divider } from 'antd';
import { ChevronRightIcon, GripVerticalIcon } from 'lucide-react';
interface ExpandProps {
toggleMenu: (expanded: boolean) => void;
}
export const Expand = ({ toggleMenu }: ExpandProps) => {
return (
<div className="flex items-center rounded-lg bg-neutral-800/50 p-1">
<strong>
<div className="flex size-[26px] cursor-move select-none items-center justify-center text-neutral-500">
<GripVerticalIcon size={18} />
</div>
</strong>
<Divider type="vertical" style={{ margin: '0 4px' }} />
<div
className="flex size-[26px] cursor-pointer items-center justify-center rounded text-neutral-500 hover:bg-neutral-800/60 hover:text-white"
onClick={() => toggleMenu(true)}
>
<ChevronRightIcon size={18} />
</div>
</div>
);
};

View File

@@ -0,0 +1,2 @@
export { Collapse } from './collapse.tsx';
export { Expand } from './expand.tsx';

View File

@@ -4,9 +4,11 @@ import {
DiscIcon,
DownloadIcon,
FileJsonIcon,
MaximizeIcon,
NetworkIcon,
PowerIcon,
TerminalSquareIcon
TerminalSquareIcon,
XIcon
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -19,12 +21,14 @@ export const MenuBar = () => {
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
const items = [
{ key: 'image', icon: <DiscIcon size={16} className="text-neutral-400" /> },
{ key: 'download', icon: <DownloadIcon size={16} className="text-neutral-400" /> },
{ key: 'script', icon: <FileJsonIcon size={16} className="text-neutral-400" /> },
{ key: 'terminal', icon: <TerminalSquareIcon size={16} className="text-neutral-400" /> },
{ key: 'wol', icon: <NetworkIcon size={16} className="text-neutral-400" /> },
{ key: 'power', icon: <PowerIcon size={16} className="text-neutral-400" /> }
{ key: 'image', icon: <DiscIcon size={16} /> },
{ key: 'download', icon: <DownloadIcon size={16} /> },
{ key: 'script', icon: <FileJsonIcon size={16} /> },
{ key: 'terminal', icon: <TerminalSquareIcon size={16} /> },
{ key: 'wol', icon: <NetworkIcon size={16} /> },
{ key: 'power', icon: <PowerIcon size={16} /> },
{ key: 'fullscreen', icon: <MaximizeIcon size={16} />, label: 'fullscreen.toggle' },
{ key: 'collapse', icon: <XIcon size={16} />, label: 'menu.collapse' }
];
function updateItems(key: string) {
@@ -48,10 +52,13 @@ export const MenuBar = () => {
<div className="mt-8 flex flex-col space-y-5">
{items.map((item) => (
<div key={item.key} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 text-neutral-400">
{item.icon}
<span>{t(`${item.key}.title`)}</span>
<span className="text-neutral-300">
{item.label ? t(item.label) : t(`${item.key}.title`)}
</span>
</div>
<Switch
value={!menuDisabledItems.includes(item.key)}
onChange={() => updateItems(item.key)}