1- import { ReactElement , useEffect , useState } from 'react'
1+ import { ReactElement , useCallback , useEffect , useRef , useState } from 'react'
22import { Divider } from 'antd'
33import clsx from 'clsx'
4- import { MenuIcon , XIcon } from 'lucide-react'
4+ import { ChevronRightIcon , GripVerticalIcon , XIcon } from 'lucide-react'
5+ import Draggable from 'react-draggable'
56
67import * as storage from '@renderer/libs/storage'
78
@@ -14,55 +15,135 @@ import { Settings } from './settings'
1415import { Video } from './video'
1516
1617export const Menu = ( ) : ReactElement => {
17- const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
18+ const [ isMenuOpen , setIsMenuOpen ] = useState ( true )
19+ const [ menuBounds , setMenuBounds ] = useState ( { left : 0 , right : 0 , top : 0 , bottom : 0 } )
20+ const [ position , setPosition ] = useState ( { x : 0 , y : 0 } )
21+ const [ initialized , setInitialized ] = useState ( false )
22+
23+ const nodeRef = useRef < HTMLDivElement | null > ( null )
24+
25+ const handleResize = useCallback ( ( ) => {
26+ if ( ! nodeRef . current ) return
27+
28+ const { innerWidth, innerHeight } = window
29+ const { offsetWidth, offsetHeight } = nodeRef . current
30+
31+ setMenuBounds ( {
32+ left : 0 ,
33+ top : 0 ,
34+ right : innerWidth - offsetWidth ,
35+ bottom : innerHeight - offsetHeight
36+ } )
37+ } , [ ] )
38+
39+ const initializePosition = useCallback ( ( ) => {
40+ if ( ! nodeRef . current || initialized ) return
41+
42+ const { innerWidth } = window
43+
44+ requestAnimationFrame ( ( ) => {
45+ if ( ! nodeRef . current ) return
46+ const width = nodeRef . current . offsetWidth
47+ setPosition ( {
48+ x : ( innerWidth - width ) / 2 ,
49+ y : 10
50+ } )
51+ setInitialized ( true )
52+ } )
53+ } , [ initialized ] )
1854
1955 useEffect ( ( ) => {
2056 const isOpen = storage . getIsMenuOpen ( )
2157 setIsMenuOpen ( isOpen )
22- } , [ ] )
58+
59+ window . addEventListener ( 'resize' , handleResize )
60+
61+ const observer = new ResizeObserver ( ( ) => {
62+ handleResize ( )
63+ } )
64+
65+ if ( nodeRef . current ) {
66+ observer . observe ( nodeRef . current )
67+ }
68+
69+ return ( ) => {
70+ window . removeEventListener ( 'resize' , handleResize )
71+ observer . disconnect ( )
72+ }
73+ } , [ handleResize ] )
74+
75+ useEffect ( ( ) => {
76+ handleResize ( )
77+ initializePosition ( )
78+ } , [ isMenuOpen , handleResize , initializePosition ] )
2379
2480 function toggleMenu ( ) : void {
2581 setIsMenuOpen ( ! isMenuOpen )
2682 }
2783
2884 return (
29- < div className = "fixed top-[10px] left-1/2 z-[1000] -translate-x-1/2" >
30- < div className = "sticky top-[10px]" >
31- < div
32- className = { clsx (
33- 'h-[34px] items-center justify-between rounded bg-neutral-800/70 px-2' ,
34- isMenuOpen ? 'flex' : 'hidden'
35- ) }
36- >
37- < Video />
38- < SerialPort />
39- < Divider type = "vertical" className = "px-[2px]" />
85+ < Draggable
86+ nodeRef = { nodeRef }
87+ bounds = { menuBounds }
88+ handle = "strong"
89+ position = { position }
90+ onDrag = { ( _ , data ) => setPosition ( { x : data . x , y : data . y } ) }
91+ >
92+ < div ref = { nodeRef } className = "fixed top-0 left-0 z-[1000]" >
93+ { /* Menubar */ }
94+ < div className = "sticky top-[10px] flex w-full justify-center" >
95+ < div
96+ className = { clsx (
97+ 'h-[34px] items-center justify-between rounded bg-neutral-800/70 pr-2 pl-1 transition-all duration-300' ,
98+ isMenuOpen ? 'flex' : 'hidden'
99+ ) }
100+ >
101+ < strong >
102+ < div className = "flex h-[28px] cursor-move items-center justify-center pl-1 text-neutral-500 select-none" >
103+ < GripVerticalIcon size = { 18 } />
104+ </ div >
105+ </ strong >
106+ < Divider type = "vertical" />
40107
41- < Keyboard />
42- < Mouse />
43- < Recorder />
108+ < Video />
109+ < SerialPort />
110+ < Divider type = "vertical" className = "px-0.5" />
44111
45- < Divider type = "vertical" className = "px-[2px]" />
112+ < Keyboard />
113+ < Mouse />
114+ < Recorder />
46115
47- < Fullscreen />
48- < Settings />
49- < div
50- className = "flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70"
51- onClick = { toggleMenu }
52- >
53- < XIcon size = { 18 } />
54- </ div >
55- </ div >
116+ < Divider type = "vertical" className = "px-0.5" />
56117
57- { ! isMenuOpen && (
58- < div
59- 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"
60- onClick = { toggleMenu }
61- >
62- < MenuIcon size = { 18 } />
118+ < Fullscreen />
119+ < Settings />
120+ < div
121+ className = "flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70"
122+ onClick = { toggleMenu }
123+ >
124+ < XIcon size = { 18 } />
125+ </ div >
63126 </ div >
64- ) }
127+
128+ { /* Menubar expand button */ }
129+ { ! isMenuOpen && (
130+ < div className = "flex items-center rounded-lg bg-neutral-800/50 p-1" >
131+ < strong >
132+ < div className = "flex size-[26px] cursor-move items-center justify-center text-neutral-500 select-none" >
133+ < GripVerticalIcon size = { 18 } />
134+ </ div >
135+ </ strong >
136+ < Divider type = "vertical" style = { { margin : '0 4px' } } />
137+ < div
138+ className = "flex size-[26px] cursor-pointer items-center justify-center rounded text-neutral-500 hover:bg-neutral-800/60 hover:text-white"
139+ onClick = { toggleMenu }
140+ >
141+ < ChevronRightIcon size = { 18 } />
142+ </ div >
143+ </ div >
144+ ) }
145+ </ div >
65146 </ div >
66- </ div >
147+ </ Draggable >
67148 )
68149}
0 commit comments