diff --git a/browser/src/components/keyboard/index.tsx b/browser/src/components/keyboard/index.tsx index 7ca90db..b1227ff 100644 --- a/browser/src/components/keyboard/index.tsx +++ b/browser/src/components/keyboard/index.tsx @@ -87,7 +87,7 @@ export const Keyboard = () => { // send keyboard data async function sendKeyData(event: KeyboardEvent) { - const modifiers = getModifiers(event); + const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current); const keys = [ 0x00, 0x00, @@ -98,32 +98,5 @@ export const Keyboard = () => { await device.sendKeyboardData(modifiers, keys); } - function getModifiers(event: KeyboardEvent) { - const modifiers = new Modifiers(); - - if (event.ctrlKey) { - modifiers.leftCtrl = pressedModifiersRef.current.has('ControlLeft'); - modifiers.rightCtrl = pressedModifiersRef.current.has('ControlRight'); - } - if (event.shiftKey) { - modifiers.leftShift = pressedModifiersRef.current.has('ShiftLeft'); - modifiers.rightShift = pressedModifiersRef.current.has('ShiftRight'); - } - if (event.altKey) { - modifiers.leftAlt = pressedModifiersRef.current.has('AltLeft'); - modifiers.rightAlt = pressedModifiersRef.current.has('AltRight'); - } - if (event.metaKey) { - modifiers.leftWindows = pressedModifiersRef.current.has('MetaLeft'); - modifiers.rightWindows = pressedModifiersRef.current.has('MetaRight'); - } - if (event.getModifierState('AltGraph')) { - modifiers.leftCtrl = true; - modifiers.rightAlt = true; - } - - return modifiers; - } - return <>; }; diff --git a/browser/src/components/menu/keyboard/shortcut-custom.tsx b/browser/src/components/menu/keyboard/shortcut-custom.tsx new file mode 100644 index 0000000..6a775a3 --- /dev/null +++ b/browser/src/components/menu/keyboard/shortcut-custom.tsx @@ -0,0 +1,159 @@ +import { useEffect, useRef, useState } from 'react'; +import { Input, InputRef, Modal, Space } from 'antd'; +import { useSetAtom } from 'jotai'; +import { KeyboardIcon, SquarePenIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts'; +import { modifierKeys, Modifiers, ShortcutProps } from '@/libs/device/keyboard.ts'; + +interface KeyboardShortcutCustomProps { + addShortcut: (shortcut: ShortcutProps) => void; +} + +export const KeyboardShortcutCustom = ({ addShortcut }: KeyboardShortcutCustomProps) => { + const { t } = useTranslation(); + const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isCapturingShortcut, setIsCapturingShortcut] = useState(false); + const [shortcut, setShortcut] = useState(null); + const [shortcutLabel, setShortcutLabel] = useState(''); + const [shortcutFixedLabel, setShortcutFixedLabel] = useState(''); + const pressedModifiersRef = useRef>(new Set()); + const inputRef = useRef(null); + + function getPressedShortcutString(event: KeyboardEvent) { + let pressedKey: string = ''; + pressedKey += event.ctrlKey ? 'Ctrl + ' : ''; + pressedKey += event.shiftKey ? 'Shift + ' : ''; + pressedKey += event.altKey ? 'Alt + ' : ''; + pressedKey += event.metaKey ? 'Meta + ' : ''; + if (!modifierKeys.has(event.key)) { + if (event.code.startsWith('Key') || event.code.startsWith('Digit')) { + pressedKey += event.code.slice(-1); + } else { + pressedKey += event.key; + } + } + return pressedKey; + } + + function handleModelDone() { + if (shortcut == null) { + cleanShortcut(); + return; + } + addShortcut({ + modifiers: shortcut.modifiers, + label: shortcutLabel == '' ? shortcutFixedLabel : shortcutLabel, + keyCode: shortcut.keyCode + }); + cleanShortcut(); + } + + function cleanShortcut() { + setIsCapturingShortcut(false); + setIsModalOpen(false); + setShortcut(null); + setShortcutLabel(''); + setShortcutFixedLabel(''); + } + + async function handleKeyDown(event: KeyboardEvent) { + event.preventDefault(); + event.stopPropagation(); + + const label = getPressedShortcutString(event); + setShortcutFixedLabel(label); + + if (modifierKeys.has(event.key)) { + if (event.type == 'keydown') { + pressedModifiersRef.current.add(event.code); + } else { + pressedModifiersRef.current.delete(event.code); + } + } else { + setIsCapturingShortcut(false); + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyDown); + const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current); + const ret: ShortcutProps = { + modifiers: modifiers, + label: label, + keyCode: event.code + }; + setShortcut(ret); + + } + } + + function handleFullscreen() { + if (!document.fullscreenElement) { + const element = document.documentElement; + element.requestFullscreen().then(); + // @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock + navigator.keyboard?.lock(); + } + } + + useEffect(() => { + setIsKeyboardEnable(!isModalOpen); + }, [isModalOpen]); + + useEffect(() => { + if (inputRef.current && !isCapturingShortcut) { + inputRef.current.blur(); + } + }, [isCapturingShortcut]); + + return ( + <> +
{ + setIsModalOpen(true); + }} + > + {t('keyboard.shortcut.custom')} +
+ + +
+ {t('keyboard.shortcut.captureTips')} + {t('keyboard.shortcut.enterFullScreen')} +
+ } + onFocus={() => { + setIsCapturingShortcut(true); + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyDown); + }} + onBlur={() => { + setIsCapturingShortcut(false); + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyDown); + }} + /> + + } + onChange={(e) => setShortcutLabel(e.target.value)} + /> + +
+
+ + ); +}; diff --git a/browser/src/components/menu/keyboard/shortcut.tsx b/browser/src/components/menu/keyboard/shortcut.tsx index 95a0f6f..a38666d 100644 --- a/browser/src/components/menu/keyboard/shortcut.tsx +++ b/browser/src/components/menu/keyboard/shortcut.tsx @@ -1,17 +1,12 @@ -import { useState } from 'react'; -import { SendHorizonal } from 'lucide-react'; - +import { useState, PropsWithChildren } from 'react'; import { device } from '@/libs/device'; import { Modifiers } from '@/libs/device/keyboard.ts'; import { KeyboardCodes } from '@/libs/keyboard'; +import { ShortcutProps } from '@/libs/device/keyboard.ts' -interface ShortcutProps { - label: string; - modifiers?: Partial; - keyCode: string; -} +type ShortcutPropsWithChildren = ShortcutProps & PropsWithChildren -export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => { +export const Shortcut = ({ label, modifiers = {}, keyCode , children}: ShortcutPropsWithChildren) => { const [isLoading, setIsLoading] = useState(false); async function handleClick(): Promise { @@ -40,8 +35,8 @@ export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => { className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60" onClick={handleClick} > - - {label} + {label} + {children} ); }; diff --git a/browser/src/components/menu/keyboard/shortcuts-menu.tsx b/browser/src/components/menu/keyboard/shortcuts-menu.tsx index 6f960cf..1020204 100644 --- a/browser/src/components/menu/keyboard/shortcuts-menu.tsx +++ b/browser/src/components/menu/keyboard/shortcuts-menu.tsx @@ -1,42 +1,86 @@ -import { useState } from 'react'; -import { SendHorizonal } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Button, Divider, Popover } from 'antd'; +import { SendHorizonal, Trash } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { Popover } from 'antd'; - +import { ShortcutProps } from '@/libs/device/keyboard.ts'; import { Shortcut } from './shortcut.tsx'; +import { KeyboardShortcutCustom } from './shortcut-custom.tsx'; +import { getShortcuts, setShortcuts } from '@/libs/storage'; export const KeyboardShortcutsMenu = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const [storedShortcuts, setStoredShortcuts] = useState([]); + const predefinedShortcuts : ShortcutProps[] = [ + { + label: t('keyboard.shortcut.ctrlAltDel'), + modifiers: { leftCtrl: true, leftAlt: true }, + keyCode: 'Delete', + }, + { + label: t('keyboard.shortcut.ctrlD'), + modifiers: { leftCtrl: true }, + keyCode: 'KeyD', + }, + { + label: t('keyboard.shortcut.winTab'), + modifiers: { leftWindows: true }, + keyCode: 'Tab', + }, + ] + + const addShortcut = (shortcut: ShortcutProps) => { + if (shortcut == null) return; + const shortcuts = getShortcuts(); + shortcuts.push(shortcut); + setStoredShortcuts(shortcuts); + setShortcuts(shortcuts); + } + + const removeShortcut = (indexToRemove: number) => { + const newShortcuts = storedShortcuts.filter( + (_, index) => index !== indexToRemove + ); + setStoredShortcuts(newShortcuts); + setShortcuts(newShortcuts); + } + + useEffect(() => { + const shortcuts = getShortcuts(); + if (shortcuts.length === 0) { + predefinedShortcuts.forEach(shortcut => { + addShortcut(shortcut); + }) + } else { + setStoredShortcuts(shortcuts); + } + }, []); return ( - {[ - { - label: t('keyboard.ctrlAltDel'), - modifiers: { leftCtrl: true, leftAlt: true }, - keyCode: 'Delete', - }, - { - label: t('keyboard.ctrlD'), - modifiers: { leftCtrl: true }, - keyCode: 'KeyD', - }, - { - label: t('keyboard.winTab'), - modifiers: { leftWindows: true }, - keyCode: 'Tab', - }, - ].map((shortcut) => ( - + {storedShortcuts.map((shortcut, index) => ( + <> + +