Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 111 additions & 25 deletions browser/src/components/menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,54 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { Divider } from 'antd';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import { ChevronRightIcon, GripVerticalIcon, XIcon } from 'lucide-react';
import {
ChevronRightIcon,
GripVerticalIcon,
XIcon,
ArrowRightLeftIcon,
} from 'lucide-react';
import Draggable from 'react-draggable';

import { serialStateAtom } from '@/jotai/device.ts';
import { menuConfigAtom, serialStateAtom } from '@/jotai/device.ts';
import {
type MenuItemId,
type SubMenuItemId,
isSubMenuItem,
SERIAL_REQUIRED_ITEMS,
getSubMenuItemMeta,
} from '@/libs/menu-config';
import * as storage from '@/libs/storage';

import { Audio } from './audio';
import { Fullscreen } from './fullscreen';
import { Keyboard } from './keyboard';
import { Mouse } from './mouse';
import { PromotedSubMenuItem } from './promoted-items';
import { Recorder } from './recorder';
import { SerialPort } from './serial-port';
import { Settings } from './settings';
import { Video } from './video';

// Map MenuItemId to its component
const MENU_ITEM_COMPONENTS: Record<MenuItemId, React.FC> = {
video: Video,
audio: Audio,
serialPort: SerialPort,
keyboard: Keyboard,
mouse: Mouse,
recorder: Recorder,
settings: Settings,
fullscreen: Fullscreen,
};

export const Menu = () => {
const serialState = useAtomValue(serialStateAtom);
const menuConfig = useAtomValue(menuConfigAtom);

const [isMenuOpen, setIsMenuOpen] = useState(true);
const [orientation, setOrientation] = useState<'horizontal' | 'vertical'>(
storage.getMenuOrientation()
);
const [menuBounds, setMenuBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });

const nodeRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -52,15 +81,71 @@ export const Menu = () => {

useEffect(() => {
handleResize();
}, [isMenuOpen, serialState, handleResize]);
}, [isMenuOpen, serialState, menuConfig, orientation, handleResize]);

function toggleMenu() {
const isOpen = !isMenuOpen;

setIsMenuOpen(isOpen);
storage.setIsMenuOpen(isOpen);
}

function toggleOrientation() {
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
setOrientation(newOrientation);
storage.setMenuOrientation(newOrientation);
}

// Build the list of rendered menu items with dividers between serial/non-serial groups
const renderMenuItems = () => {
const elements: React.ReactNode[] = [];
let lastRequiredSerial: boolean | null = null;

for (const itemId of menuConfig.visibleItems) {
let requiresSerial: boolean;
let element: React.ReactNode;

if (isSubMenuItem(itemId)) {
// Promoted submenu item
const meta = getSubMenuItemMeta(itemId as SubMenuItemId);
requiresSerial = meta?.requiresSerial ?? false;

if (requiresSerial && serialState !== 'connected') continue;

element = <PromotedSubMenuItem key={itemId} itemId={itemId as SubMenuItemId} />;
} else {
const menuItemId = itemId as MenuItemId;
requiresSerial = SERIAL_REQUIRED_ITEMS.includes(menuItemId);

if (requiresSerial && serialState !== 'connected') continue;

const Component = MENU_ITEM_COMPONENTS[menuItemId];
if (!Component) continue;

element = <Component key={menuItemId} />;
}

// Add divider at serial/non-serial transitions
if (lastRequiredSerial !== null && lastRequiredSerial !== requiresSerial) {
const dividerType = orientation === 'horizontal' ? 'vertical' : 'horizontal';
elements.push(
<Divider
key={`div-${itemId}`}
type={dividerType}
className={orientation === 'horizontal' ? 'px-0.5' : ''}
style={orientation === 'vertical' ? { margin: '2px 0' } : undefined}
/>
);
}

elements.push(element);
lastRequiredSerial = requiresSerial;
}

return elements;
};

const isVertical = orientation === 'vertical';

return (
<Draggable
nodeRef={nodeRef}
Expand All @@ -76,37 +161,38 @@ export const Menu = () => {
<div className="sticky top-[10px] flex w-full justify-center">
<div
className={clsx(
'h-[34px] items-center justify-between space-x-1.5 rounded bg-neutral-800/70 px-2',
isMenuOpen ? 'flex' : 'hidden'
'items-center justify-between rounded bg-neutral-800/70 px-2',
isMenuOpen
? isVertical
? 'flex flex-col space-y-1.5 py-2'
: 'flex h-[34px] space-x-1.5'
: 'hidden'
)}
>
<strong>
<div className="flex h-[28px] cursor-move select-none items-center justify-center text-neutral-400">
<GripVerticalIcon size={18} />
</div>
</strong>
<Divider type="vertical" />

<Video />
<Audio />
<Divider
type={isVertical ? 'horizontal' : 'vertical'}
style={isVertical ? { margin: '2px 0' } : undefined}
/>

{serialState === 'connected' && (
<>
<SerialPort />
{renderMenuItems()}

<Divider type="vertical" className="px-0.5" />
<Divider
type={isVertical ? 'horizontal' : 'vertical'}
style={isVertical ? { margin: '2px 0' } : undefined}
/>

<Keyboard />
<Mouse />
</>
)}

<Recorder />

<Divider type="vertical" className="px-0.5" />

<Settings />
<Fullscreen />
<div
className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-neutral-400 hover:bg-neutral-700/70 hover:text-white"
onClick={toggleOrientation}
title={isVertical ? 'Switch to horizontal' : 'Switch to vertical'}
>
<ArrowRightLeftIcon size={16} />
</div>
<div
className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/70"
onClick={toggleMenu}
Expand Down
18 changes: 15 additions & 3 deletions browser/src/components/menu/keyboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { useState } from 'react';
import { Popover } from 'antd';
import { useAtomValue } from 'jotai';
import { KeyboardIcon } from 'lucide-react';

import { menuConfigAtom } from '@/jotai/device';
import type { KeyboardSubItemId } from '@/libs/menu-config';

import { Paste } from './paste.tsx';
import { Shortcuts } from './shortcuts';
import { VirtualKeyboard } from './virtual-keyboard.tsx';

const KEYBOARD_SUB_COMPONENTS: Record<KeyboardSubItemId, React.FC> = {
'keyboard.paste': Paste,
'keyboard.virtualKeyboard': VirtualKeyboard,
'keyboard.shortcuts': Shortcuts,
};

export const Keyboard = () => {
const menuConfig = useAtomValue(menuConfigAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

const content = (
<div className="flex flex-col space-y-0.5">
<Paste />
<VirtualKeyboard />
<Shortcuts />
{menuConfig.subMenus.keyboard.map((itemId) => {
const Component = KEYBOARD_SUB_COMPONENTS[itemId];
return Component ? <Component key={itemId} /> : null;
})}
</div>
);

Expand Down
26 changes: 17 additions & 9 deletions browser/src/components/menu/mouse/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useEffect, useState } from 'react';
import { Divider, Popover } from 'antd';
import { useAtom, useSetAtom } from 'jotai';
import { Popover } from 'antd';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { MouseIcon } from 'lucide-react';

import { menuConfigAtom } from '@/jotai/device';
import {
mouseJigglerModeAtom,
mouseModeAtom,
mouseStyleAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.ts';
import type { MouseSubItemId } from '@/libs/menu-config';
import { mouseJiggler } from '@/libs/mouse-jiggler';
import * as storage from '@/libs/storage';

Expand All @@ -19,7 +21,16 @@ import { Mode } from './mode.tsx';
import { Speed } from './speed.tsx';
import { Style } from './style.tsx';

const MOUSE_SUB_COMPONENTS: Record<MouseSubItemId, React.FC> = {
'mouse.style': Style,
'mouse.mode': Mode,
'mouse.direction': Direction,
'mouse.speed': Speed,
'mouse.jiggler': Jiggler,
};

export const Mouse = () => {
const menuConfig = useAtomValue(menuConfigAtom);
const [mouseStyle, setMouseStyle] = useAtom(mouseStyleAtom);
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
const setScrollDirection = useSetAtom(scrollDirectionAtom);
Expand Down Expand Up @@ -60,13 +71,10 @@ export const Mouse = () => {

const content = (
<div className="flex flex-col space-y-0.5">
<Style />
<Mode />
<Direction />
<Speed />

<Divider style={{ margin: '5px 0 5px 0' }} />
<Jiggler />
{menuConfig.subMenus.mouse.map((itemId) => {
const Component = MOUSE_SUB_COMPONENTS[itemId];
return Component ? <Component key={itemId} /> : null;
})}
</div>
);

Expand Down
Loading