Skip to content

Commit 3094a8b

Browse files
committed
feat(desktop): menu bar supports dragging
1 parent 3635d0c commit 3094a8b

3 files changed

Lines changed: 134 additions & 36 deletions

File tree

desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"i18next": "^24.2.2",
3333
"jotai": "^2.12.1",
3434
"lucide-react": "^0.476.0",
35+
"react-draggable": "^4.5.0",
3536
"react-i18next": "^15.4.1",
3637
"react-responsive": "^10.0.0",
3738
"react-simple-keyboard": "^3.8.49",

desktop/pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 117 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { ReactElement, useEffect, useState } from 'react'
1+
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
22
import { Divider } from 'antd'
33
import 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

67
import * as storage from '@renderer/libs/storage'
78

@@ -14,55 +15,135 @@ import { Settings } from './settings'
1415
import { Video } from './video'
1516

1617
export 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

Comments
 (0)