feat(desktop): menu bar supports dragging

This commit is contained in:
wj-xiao
2026-01-13 16:37:29 +08:00
parent 3635d0cdf9
commit 3094a8bbbd
3 changed files with 137 additions and 39 deletions

View File

@@ -32,6 +32,7 @@
"i18next": "^24.2.2",
"jotai": "^2.12.1",
"lucide-react": "^0.476.0",
"react-draggable": "^4.5.0",
"react-i18next": "^15.4.1",
"react-responsive": "^10.0.0",
"react-simple-keyboard": "^3.8.49",

16
desktop/pnpm-lock.yaml generated
View File

@@ -64,6 +64,9 @@ importers:
lucide-react:
specifier: ^0.476.0
version: 0.476.0(react@18.3.1)
react-draggable:
specifier: ^4.5.0
version: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-i18next:
specifier: ^15.4.1
version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)
@@ -3290,6 +3293,12 @@ packages:
peerDependencies:
react: ^18.3.1
react-draggable@4.5.0:
resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==}
peerDependencies:
react: '>= 16.3.0'
react-dom: '>= 16.3.0'
react-i18next@15.7.4:
resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==}
peerDependencies:
@@ -7300,6 +7309,13 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-draggable@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.28.4

View File

@@ -1,7 +1,8 @@
import { ReactElement, useEffect, useState } from 'react'
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
import { Divider } from 'antd'
import clsx from 'clsx'
import { MenuIcon, XIcon } from 'lucide-react'
import { ChevronRightIcon, GripVerticalIcon, XIcon } from 'lucide-react'
import Draggable from 'react-draggable'
import * as storage from '@renderer/libs/storage'
@@ -14,55 +15,135 @@ import { Settings } from './settings'
import { Video } from './video'
export const Menu = (): ReactElement => {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(true)
const [menuBounds, setMenuBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 })
const [position, setPosition] = useState({ x: 0, y: 0 })
const [initialized, setInitialized] = useState(false)
const nodeRef = useRef<HTMLDivElement | null>(null)
const handleResize = useCallback(() => {
if (!nodeRef.current) return
const { innerWidth, innerHeight } = window
const { offsetWidth, offsetHeight } = nodeRef.current
setMenuBounds({
left: 0,
top: 0,
right: innerWidth - offsetWidth,
bottom: innerHeight - offsetHeight
})
}, [])
const initializePosition = useCallback(() => {
if (!nodeRef.current || initialized) return
const { innerWidth } = window
requestAnimationFrame(() => {
if (!nodeRef.current) return
const width = nodeRef.current.offsetWidth
setPosition({
x: (innerWidth - width) / 2,
y: 10
})
setInitialized(true)
})
}, [initialized])
useEffect(() => {
const isOpen = storage.getIsMenuOpen()
setIsMenuOpen(isOpen)
}, [])
window.addEventListener('resize', handleResize)
const observer = new ResizeObserver(() => {
handleResize()
})
if (nodeRef.current) {
observer.observe(nodeRef.current)
}
return () => {
window.removeEventListener('resize', handleResize)
observer.disconnect()
}
}, [handleResize])
useEffect(() => {
handleResize()
initializePosition()
}, [isMenuOpen, handleResize, initializePosition])
function toggleMenu(): void {
setIsMenuOpen(!isMenuOpen)
}
return (
<div className="fixed top-[10px] left-1/2 z-[1000] -translate-x-1/2">
<div className="sticky top-[10px]">
<div
className={clsx(
'h-[34px] items-center justify-between rounded bg-neutral-800/70 px-2',
isMenuOpen ? 'flex' : 'hidden'
<Draggable
nodeRef={nodeRef}
bounds={menuBounds}
handle="strong"
position={position}
onDrag={(_, data) => setPosition({ x: data.x, y: data.y })}
>
<div ref={nodeRef} className="fixed top-0 left-0 z-[1000]">
{/* Menubar */}
<div className="sticky top-[10px] flex w-full justify-center">
<div
className={clsx(
'h-[34px] items-center justify-between rounded bg-neutral-800/70 pr-2 pl-1 transition-all duration-300',
isMenuOpen ? 'flex' : 'hidden'
)}
>
<strong>
<div className="flex h-[28px] cursor-move items-center justify-center pl-1 text-neutral-500 select-none">
<GripVerticalIcon size={18} />
</div>
</strong>
<Divider type="vertical" />
<Video />
<SerialPort />
<Divider type="vertical" className="px-0.5" />
<Keyboard />
<Mouse />
<Recorder />
<Divider type="vertical" className="px-0.5" />
<Fullscreen />
<Settings />
<div
className="flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70"
onClick={toggleMenu}
>
<XIcon size={18} />
</div>
</div>
{/* Menubar expand button */}
{!isMenuOpen && (
<div className="flex items-center rounded-lg bg-neutral-800/50 p-1">
<strong>
<div className="flex size-[26px] cursor-move items-center justify-center text-neutral-500 select-none">
<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}
>
<ChevronRightIcon size={18} />
</div>
</div>
)}
>
<Video />
<SerialPort />
<Divider type="vertical" className="px-[2px]" />
<Keyboard />
<Mouse />
<Recorder />
<Divider type="vertical" className="px-[2px]" />
<Fullscreen />
<Settings />
<div
className="flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70"
onClick={toggleMenu}
>
<XIcon size={18} />
</div>
</div>
{!isMenuOpen && (
<div
className="flex h-[30px] w-[35px] cursor-pointer items-center justify-center rounded bg-neutral-800/50 text-white/70 hover:bg-neutral-800 hover:text-white"
onClick={toggleMenu}
>
<MenuIcon size={18} />
</div>
)}
</div>
</div>
</Draggable>
)
}