mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-11-09 17:04:27 -06:00
feat(desktop): menu bar supports dragging
This commit is contained in:
@@ -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
16
desktop/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user