refactor: optimize keyboard keys and refactor keyboard shortcut

This commit is contained in:
wj-xiao
2025-12-22 15:56:31 +08:00
parent 758449cdc0
commit 052e0263e6
20 changed files with 760 additions and 407 deletions

View File

@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-scroll-area": "^1.2.10",
"antd": "^5.22.2",
"clsx": "^2.1.1",
"i18next": "^24.0.5",

179
browser/pnpm-lock.yaml generated
View File

@@ -24,6 +24,9 @@ importers:
.:
dependencies:
'@radix-ui/react-scroll-area':
specifier: ^1.2.10
version: 1.2.10(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd:
specifier: ^5.22.2
version: 5.22.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -552,9 +555,15 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -564,6 +573,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
@@ -573,6 +591,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
@@ -586,6 +613,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
@@ -656,6 +692,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@@ -669,6 +718,32 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-scroll-area@1.2.10':
resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@@ -678,6 +753,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@@ -687,6 +771,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
@@ -714,6 +807,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@rc-component/async-validator@5.0.4':
resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==}
engines: {node: '>=14.x'}
@@ -2974,20 +3076,36 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-context@1.1.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -3010,6 +3128,12 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-direction@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -3067,6 +3191,16 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3076,6 +3210,32 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3083,12 +3243,25 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-slot@1.2.3(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3109,6 +3282,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@rc-component/async-validator@5.0.4':
dependencies:
'@babel/runtime': 7.28.4

View File

@@ -4,7 +4,7 @@ import { device } from '@/libs/device';
import { Modifiers } from '@/libs/device/keyboard.ts';
import { KeyboardCodes } from '@/libs/keyboard';
const MAX_SIMULTANEOUS_KEYS = 4;
const MAX_SIMULTANEOUS_KEYS = 6;
const ModifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
export const Keyboard = () => {
@@ -44,7 +44,7 @@ export const Keyboard = () => {
}
}
await sendKeyData(event);
await sendKeyData();
}
// release button
@@ -61,7 +61,7 @@ export const Keyboard = () => {
}
}
await sendKeyData(event);
await sendKeyData();
}
// release all keys when page loses focus
@@ -86,11 +86,13 @@ export const Keyboard = () => {
}
// send keyboard data
async function sendKeyData(event: KeyboardEvent) {
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
async function sendKeyData() {
const modifiers = new Modifiers();
pressedModifiersRef.current.forEach((code) => {
modifiers.setModifier(code);
});
const keys = [
0x00,
0x00,
...Array.from(pressedKeysRef.current),
...new Array(MAX_SIMULTANEOUS_KEYS - pressedKeysRef.current.size).fill(0x00)
];

View File

@@ -3,18 +3,18 @@ import { Popover } from 'antd';
import { KeyboardIcon } from 'lucide-react';
import { Paste } from './paste.tsx';
import { Shortcuts } from './shortcuts';
import { VirtualKeyboard } from './virtual-keyboard.tsx';
import { KeyboardShortcutsMenu } from './shortcuts-menu.tsx';
export const Keyboard = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const content = (
<>
<div className="flex flex-col space-y-0.5">
<Paste />
<VirtualKeyboard />
<KeyboardShortcutsMenu />
</>
<Shortcuts />
</div>
);
return (

View File

@@ -39,7 +39,7 @@ export const Paste = () => {
}
async function send(modifiers: Modifiers, code: number) {
const keys = [0x00, 0x00, code, 0x00, 0x00, 0x00];
const keys = [code, 0x00, 0x00, 0x00, 0x00, 0x00];
await device.sendKeyboardData(modifiers, keys);
await device.sendKeyboardData(new Modifiers(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
@@ -47,7 +47,7 @@ export const Paste = () => {
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50"
className="flex h-[32px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/50"
onClick={paste}
>
<ClipboardIcon size={18} />

View File

@@ -1,159 +0,0 @@
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<boolean>(false);
const [isCapturingShortcut, setIsCapturingShortcut] = useState<boolean>(false);
const [shortcut, setShortcut] = useState<ShortcutProps | null>(null);
const [shortcutLabel, setShortcutLabel] = useState<string>('');
const [shortcutFixedLabel, setShortcutFixedLabel] = useState<string>('');
const pressedModifiersRef = useRef<Set<string>>(new Set());
const inputRef = useRef<InputRef>(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 (
<>
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={() => {
setIsModalOpen(true);
}}
>
<span>{t('keyboard.shortcut.custom')}</span>
</div>
<Modal
width={400}
title={t('keyboard.shortcut.custom')}
open={isModalOpen}
onOk={handleModelDone}
onCancel={cleanShortcut}
okText={t('keyboard.shortcut.save')}
cancelText={t('keyboard.shortcut.cancel')}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div className="flex">
{t('keyboard.shortcut.captureTips')}
<a onClick={handleFullscreen}>{t('keyboard.shortcut.enterFullScreen')}</a>
</div>
<Input
ref={inputRef}
placeholder={t('keyboard.shortcut.capture')}
value={shortcutFixedLabel}
prefix={<KeyboardIcon size={16} />}
onFocus={() => {
setIsCapturingShortcut(true);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyDown);
}}
onBlur={() => {
setIsCapturingShortcut(false);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyDown);
}}
/>
<Input
placeholder={shortcutFixedLabel || t('keyboard.shortcut.label')}
value={shortcutLabel}
prefix={<SquarePenIcon size={16} />}
onChange={(e) => setShortcutLabel(e.target.value)}
/>
</Space>
</Modal>
</>
);
};

View File

@@ -1,42 +0,0 @@
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'
type ShortcutPropsWithChildren = ShortcutProps & PropsWithChildren
export const Shortcut = ({ label, modifiers = {}, keyCode , children}: ShortcutPropsWithChildren) => {
const [isLoading, setIsLoading] = useState(false);
async function handleClick(): Promise<void> {
if (isLoading) return;
setIsLoading(true);
try {
const mods = new Modifiers();
Object.assign(mods, modifiers);
await send(mods, KeyboardCodes.get(keyCode)!);
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
}
async function send(mods: Modifiers, code: number) {
const keys = [0x00, 0x00, code, 0x00, 0x00, 0x00];
await device.sendKeyboardData(mods, keys);
await device.sendKeyboardData(new Modifiers(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={handleClick}
>
<span className="w-full">{label}</span>
{children}
</div>
);
};

View File

@@ -1,99 +0,0 @@
import { useEffect, useState } from 'react';
import { Button, Divider, Popover } from 'antd';
import { SendHorizonal, Trash } from 'lucide-react';
import { useTranslation } from 'react-i18next';
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<ShortcutProps[]>([]);
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 (
<Popover
content={
<div className="flex flex-col gap-1">
{storedShortcuts.map((shortcut, index) => (
<>
<Shortcut
key={index}
label={shortcut.label}
modifiers={shortcut.modifiers}
keyCode={shortcut.keyCode}
>
<Button
className="ml-auto"
type="text"
danger
icon={<Trash size={16} />}
onClick={() => {removeShortcut(index)}}
/>
</Shortcut>
</>
))}
<Divider style={{ margin: '5px 0 5px 0' }} />
<KeyboardShortcutCustom
addShortcut={addShortcut}/>
</div>
}
trigger="click"
placement="rightTop"
align={{ offset: [14, 0] }}
open={open}
onOpenChange={setOpen}
arrow={false}
>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
<SendHorizonal size={18} />
<span>{t('keyboard.shortcut.title')}</span>
</div>
</Popover>
);
};

View File

@@ -0,0 +1,96 @@
import { useEffect, useState } from 'react';
import { Divider, Popover } from 'antd';
import { CommandIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ScrollArea } from '@/components/ui/scroll-area';
import * as storage from '@/libs/storage';
import { Recorder } from './recorder.tsx';
import { Shortcut } from './shortcut.tsx';
import type { Shortcut as ShortcutInterface } from './types.ts';
export const Shortcuts = () => {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [customShortcuts, setCustomShortcuts] = useState<ShortcutInterface[]>([]);
const defaultShortcuts: ShortcutInterface[] = [
{
keys: [
{ code: 'MetaLeft', label: 'Win', isModifier: true },
{ code: 'Tab', label: 'Tab', isModifier: false }
]
},
{
keys: [
{ code: 'ControlLeft', label: 'Ctrl', isModifier: true },
{ code: 'AltLeft', label: 'Alt', isModifier: true },
{ code: 'Delete', label: '⌫', isModifier: false }
]
}
];
useEffect(() => {
const shortcuts = storage.getShortcuts();
if (!shortcuts) return;
setCustomShortcuts(JSON.parse(shortcuts));
}, []);
function addShortcut(shortcut: ShortcutInterface) {
const shortcuts = [...customShortcuts, shortcut];
setCustomShortcuts(shortcuts);
storage.setShortcuts(JSON.stringify(shortcuts));
}
function delShortcut(index: number) {
const shortcuts = customShortcuts.filter((_, i) => i !== index);
setCustomShortcuts(shortcuts);
storage.setShortcuts(JSON.stringify(shortcuts));
}
const content = (
<ScrollArea className="max-w-[400px] [&>[data-radix-scroll-area-viewport]]:max-h-[350px]">
{/* custom shortcuts */}
{customShortcuts.length > 0 && (
<>
{customShortcuts.map((shortcut, index) => (
<Shortcut key={index} shortcut={shortcut}></Shortcut>
))}
<Divider style={{ margin: '5px 0 5px 0' }} />
</>
)}
{/* default shortcuts */}
{defaultShortcuts.map((shortcut, index) => (
<Shortcut key={index} shortcut={shortcut}></Shortcut>
))}
<Divider style={{ margin: '5px 0 5px 0' }} />
<Recorder shortcuts={customShortcuts} addShortcut={addShortcut} delShortcut={delShortcut} />
</ScrollArea>
);
return (
<Popover
content={content}
trigger="hover"
placement="rightTop"
align={{ offset: [14, 0] }}
open={isOpen}
onOpenChange={setIsOpen}
arrow={false}
>
<div className="flex h-[32px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/50">
<div className="flex size-[18px] items-center justify-center">
<CommandIcon size={16} />
</div>
<span>{t('keyboard.shortcut.title')}</span>
</div>
</Popover>
);
};

View File

@@ -0,0 +1,256 @@
import { useEffect, useRef, useState } from 'react';
import { Button, Divider, Input, InputRef, Modal } from 'antd';
import { useSetAtom } from 'jotai';
import { KeyboardIcon, Trash2Icon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Kbd, KbdGroup } from '@/components/ui/kbd.tsx';
import { ScrollArea } from '@/components/ui/scroll-area.tsx';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { modifierKeys } from '@/libs/device/keyboard.ts';
import { KeyInfo, Shortcut } from './types.ts';
const MAX_KEYS = 6;
const SpecialKeyMap: Record<string, string> = {
Space: 'Space',
Backspace: '⌫',
Enter: '↵',
Tab: 'Tab',
CapsLock: 'Caps',
Escape: 'Esc',
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Delete: 'Del',
Insert: 'Ins',
Home: 'Home',
End: 'End',
PageUp: 'PgUp',
PageDown: 'PgDn'
};
const PunctuationMap: Record<string, string> = {
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: "'",
Backquote: '`',
Comma: ',',
Period: '.',
Slash: '/'
};
interface RecorderProps {
shortcuts: Shortcut[];
addShortcut: (shortcut: Shortcut) => void;
delShortcut: (index: number) => void;
}
export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps) => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [shortcutLabel, setShortcutLabel] = useState('');
const inputRef = useRef<InputRef | null>(null);
const recordedKeysRef = useRef<KeyInfo[]>([]);
useEffect(() => {
setIsKeyboardEnable(!isModalOpen);
if (!isModalOpen) return;
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 100);
return () => {
clearTimeout(timer);
};
}, [isModalOpen]);
useEffect(() => {
if (!isRecording) return;
const handleKeyDown = (event: KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
const { key, code } = event;
const isRecorded = recordedKeysRef.current.some((k) => k.code === code);
if (isRecorded) return;
if (recordedKeysRef.current.length >= MAX_KEYS) return;
const keyInfo: KeyInfo = {
code: code,
label: formatKeyDisplay(key, code),
isModifier: modifierKeys.has(key)
};
setShortcutLabel((prev) => (prev ? `${prev} + ${keyInfo.label}` : keyInfo.label));
recordedKeysRef.current.push(keyInfo);
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isRecording]);
const formatKeyDisplay = (key: string, code: string): string => {
if (modifierKeys.has(key)) {
if (code.startsWith('Control')) {
return 'Ctrl';
}
if (code.startsWith('Shift')) {
return 'Shift';
}
if (code.startsWith('Alt')) {
return 'Alt';
}
if (code.startsWith('Meta')) {
return 'Win';
}
}
if (code.startsWith('Digit')) {
return code.replace('Digit', '');
}
if (code.startsWith('Key')) {
return code.replace('Key', '');
}
if (code.startsWith('Numpad')) {
const numpadKey = code.replace('Numpad', '');
return `Num${numpadKey}`;
}
if (code.startsWith('F') && /^F\d+$/.test(code)) {
return code; // F1, F2, etc.
}
if (SpecialKeyMap[code]) {
return SpecialKeyMap[code];
}
if (PunctuationMap[code]) {
return PunctuationMap[code];
}
return key.toUpperCase();
};
function saveShortcut() {
if (recordedKeysRef.current.length > 0) {
addShortcut({ keys: recordedKeysRef.current });
}
clearShortcut();
}
function clearShortcut() {
setShortcutLabel('');
recordedKeysRef.current = [];
}
function closeModal() {
clearShortcut();
setIsModalOpen(false);
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock
navigator.keyboard?.lock();
} else {
document.exitFullscreen();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/unlock
navigator.keyboard?.unlock();
}
}
return (
<>
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={() => setIsModalOpen(true)}
>
<span>{t('keyboard.shortcut.custom')}</span>
</div>
<Modal
width={500}
title={t('keyboard.shortcut.title')}
keyboard={false}
footer={null}
open={isModalOpen}
onCancel={closeModal}
>
<div>
<span className="text-neutral-500">{t('keyboard.shortcut.captureTips')}</span>
<a className="px-1 text-blue-500/80" onClick={toggleFullscreen}>
{t('keyboard.shortcut.enterFullScreen')}
</a>
</div>
<div className="flex items-center space-x-0.5 py-6">
<Input
ref={inputRef}
placeholder={t('keyboard.shortcut.capture')}
prefix={<KeyboardIcon size={16} className="text-neutral-500" />}
value={shortcutLabel}
onFocus={() => setIsRecording(true)}
onBlur={() => setIsRecording(false)}
/>
<Button onClick={clearShortcut}>{t('keyboard.shortcut.clear')}</Button>
</div>
<div className="flex w-full justify-center pb-3">
<Button type="primary" className="min-w-24" onClick={saveShortcut}>
{t('keyboard.shortcut.save')}
</Button>
</div>
{shortcuts.length > 0 && (
<>
<Divider />
<ScrollArea className="[&>[data-radix-scroll-area-viewport]]:max-h-[300px]">
{shortcuts.map((shortcut, index) => (
<div
key={index}
className="flex items-center justify-between rounded p-2 hover:bg-neutral-700/50"
>
<div className="flex items-center space-x-1">
{shortcut.keys.map((key, index) => (
<KbdGroup key={index}>
<Kbd>{key.label}</Kbd>
</KbdGroup>
))}
</div>
<div
className="flex size-[20px] cursor-pointer items-center justify-center rounded-sm text-neutral-500 hover:text-red-500"
onClick={() => delShortcut(index)}
>
<Trash2Icon size={16} />
</div>
</div>
))}
</ScrollArea>
</>
)}
</Modal>
</>
);
};

View File

@@ -0,0 +1,65 @@
import { useState } from 'react';
import { Kbd, KbdGroup } from '@/components/ui/kbd.tsx';
import { device } from '@/libs/device';
import { Modifiers } from '@/libs/device/keyboard.ts';
import { KeyboardCodes } from '@/libs/keyboard';
import type { Shortcut as ShortcutInterface } from './types.ts';
type ShortcutProps = {
shortcut: ShortcutInterface;
};
export const Shortcut = ({ shortcut }: ShortcutProps) => {
const [isLoading, setIsLoading] = useState(false);
async function send() {
const modifiers = new Modifiers();
const codes: number[] = [];
shortcut.keys.forEach((key) => {
if (key.isModifier) {
modifiers.setModifier(key.code);
} else {
const code = KeyboardCodes.get(key.code);
if (code) {
codes.push(code);
}
}
});
if (codes.length < 6) {
codes.push(...new Array(6 - codes.length).fill(0x00));
}
await device.sendKeyboardData(modifiers, codes);
await device.sendKeyboardData(new Modifiers(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
async function handleClick(): Promise<void> {
if (isLoading) return;
setIsLoading(true);
try {
await send();
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
}
}
return (
<div
className="flex h-[32px] w-full cursor-pointer items-center space-x-1 rounded px-3 hover:bg-neutral-700/30"
onClick={handleClick}
>
{shortcut.keys.map((key, index) => (
<KbdGroup key={index}>
<Kbd>{key.label}</Kbd>
</KbdGroup>
))}
</div>
);
};

View File

@@ -0,0 +1,9 @@
export interface KeyInfo {
code: string;
label: string;
isModifier: boolean;
}
export interface Shortcut {
keys: KeyInfo[];
}

View File

@@ -8,10 +8,14 @@ export const VirtualKeyboard = () => {
const { t } = useTranslation();
const setIsKeyboardOpen = useSetAtom(isKeyboardOpenAtom);
function toggleKeyboard() {
setIsKeyboardOpen((isKeyboardOpen) => !isKeyboardOpen);
}
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50"
onClick={() => setIsKeyboardOpen(true)}
className="flex h-[32px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/50"
onClick={toggleKeyboard}
>
<KeyboardIcon size={18} />
<span>{t('keyboard.virtualKeyboard')}</span>

View File

@@ -0,0 +1,28 @@
import clsx from 'clsx';
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot="kbd"
className={clsx(
'pointer-events-none inline-flex h-5 w-fit min-w-9 select-none items-center justify-center gap-1 rounded border-b border-b-neutral-600 bg-neutral-700/70 px-1 font-sans text-xs font-medium text-neutral-300',
"[&_svg:not([class*='size-'])]:size-3",
'[[data-slot=tooltip-content]_&]:text-background [[data-slot=tooltip-content]_&]:bg-background/10',
className
)}
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<kbd
data-slot="kbd-group"
className={clsx('inline-flex items-center gap-1', className)}
{...props}
/>
);
}
export { Kbd, KbdGroup };

View File

@@ -0,0 +1,53 @@
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import clsx from 'clsx';
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={clsx('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px]"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={clsx(
'flex touch-none select-none p-px transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full bg-neutral-500/30"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -64,7 +64,7 @@ export const VirtualKeyboard = ({ isBigScreen }: KeyboardProps) => {
}
const modifiers = getModifiers();
const keys = [0x00, 0x00, code, 0x00, 0x00, 0x00];
const keys = [code, 0x00, 0x00, 0x00, 0x00, 0x00];
await device.sendKeyboardData(modifiers, keys);
}
@@ -80,35 +80,8 @@ export const VirtualKeyboard = ({ isBigScreen }: KeyboardProps) => {
const modifiers = new Modifiers();
activeModifierKeys.forEach((modifierKey) => {
const specialKey = specialKeyMap.get(modifierKey)!;
switch (specialKey) {
case 'ControlLeft':
modifiers.leftCtrl = true;
break;
case 'ControlRight':
modifiers.rightCtrl = true;
break;
case 'ShiftLeft':
modifiers.leftShift = true;
break;
case 'ShiftRight':
modifiers.rightShift = true;
break;
case 'AltLeft':
modifiers.leftAlt = true;
break;
case 'AltRight':
modifiers.rightAlt = true;
break;
case 'MetaLeft':
modifiers.leftWindows = true;
break;
case 'MetaRight':
modifiers.rightWindows = true;
break;
default:
break;
}
const code = specialKeyMap.get(modifierKey)!;
modifiers.setModifier(code);
});
return modifiers;

View File

@@ -39,18 +39,15 @@ const en = {
paste: 'Paste',
virtualKeyboard: 'Keyboard',
shortcut: {
title: 'Shortcut',
custom: 'Custom Shortcut',
title: 'Shortcuts',
custom: 'Custom',
capture: 'Click here to capture shortcut',
label: 'Label',
cancel: 'Cancel',
clear: 'Clear',
save: 'Save',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: 'Enter Full Screen',
captureTips: 'Capturing the Meta key separately requires operation in full-screen mode.'
},
captureTips:
'Capturing system-level keys (such as the Windows key) requires full-screen permission.',
enterFullScreen: 'Toggle full-screen mode.'
}
},
mouse: {
cursor: {

View File

@@ -38,17 +38,13 @@ const zh = {
virtualKeyboard: '虚拟键盘',
shortcut: {
title: '快捷键',
custom: '自定义快捷键',
custom: '自定义',
capture: '点击此处捕获快捷键',
label: '备注',
cancel: '取消',
clear: '清空',
save: '保存',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
enterFullScreen: '进入全屏',
captureTips: '单独捕获 Meta 键需要在全屏下操作,'
},
captureTips: '捕获系统级按键(如 Windows 键)需要全屏权限。',
enterFullScreen: '切换全屏模式。'
}
},
mouse: {
cursor: {

View File

@@ -1,11 +1,5 @@
import { setBit } from './utils.ts';
interface ShortcutProps {
label: string;
modifiers: Partial<Modifiers>;
keyCode: string;
}
const modifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
class Modifiers {
@@ -31,31 +25,34 @@ class Modifiers {
return b;
}
public static getModifiers(event: KeyboardEvent, pressedModifiers: Set<string>) {
const modifiers = new Modifiers();
if (event.ctrlKey) {
modifiers.leftCtrl = pressedModifiers.has('ControlLeft');
modifiers.rightCtrl = pressedModifiers.has('ControlRight');
public setModifier(code: string) {
switch (code) {
case 'ControlLeft':
this.leftCtrl = true;
return;
case 'ControlRight':
this.rightCtrl = true;
return;
case 'ShiftLeft':
this.leftShift = true;
return;
case 'ShiftRight':
this.rightShift = true;
return;
case 'AltLeft':
this.leftAlt = true;
return;
case 'AltRight':
this.rightAlt = true;
return;
case 'MetaLeft':
this.leftWindows = true;
return;
case 'MetaRight':
this.rightWindows = true;
return;
}
if (event.shiftKey) {
modifiers.leftShift = pressedModifiers.has('ShiftLeft');
modifiers.rightShift = pressedModifiers.has('ShiftRight');
}
if (event.altKey) {
modifiers.leftAlt = pressedModifiers.has('AltLeft');
modifiers.rightAlt = pressedModifiers.has('AltRight');
}
if (event.metaKey) {
modifiers.leftWindows = pressedModifiers.has('MetaLeft');
modifiers.rightWindows = pressedModifiers.has('MetaRight');
}
if (event.getModifierState('AltGraph')) {
modifiers.leftCtrl = true;
modifiers.rightAlt = true;
}
return modifiers;
}
}
export { type ShortcutProps, Modifiers, modifierKeys };
export { Modifiers, modifierKeys };

View File

@@ -1,18 +1,17 @@
import type { Resolution } from '@/types';
import { ShortcutProps } from '@/libs/device/keyboard.ts';
const LANGUAGE_KEY = 'nanokvm-usb-language';
const VIDEO_DEVICE_ID_KEY = 'nanokvm-usb-video-device-id';
const VIDEO_RESOLUTION_KEY = 'nanokvm-usb-video-resolution';
const CUSTOM_RESOLUTION_KEY = 'nanokvm-usb-custom-resolution';
const VIDEO_SCALE_KEY = 'nanokvm-usb-video-scale'
const VIDEO_SCALE_KEY = 'nanokvm-usb-video-scale';
const IS_MENU_OPEN_KEY = 'nanokvm-is-menu-open';
const MOUSE_STYLE_KEY = 'nanokvm-usb-mouse-style';
const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode';
const MOUSE_SCROLL_DIRECTION_KEY = 'nanokvm-usb-mouse-scroll-direction';
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-usb-mouse-scroll-interval';
const SHORTCUTS_KEY = 'nanokvm-usb-shortcuts';
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode';
const KEYBOARD_SHORTCUT_KEY = 'nanokvm-usb-keyboard-shortcut';
export function getLanguage() {
return localStorage.getItem(LANGUAGE_KEY);
@@ -61,15 +60,15 @@ export function removeCustomResolutions() {
}
export function getVideoScale(): number | null {
const scale = localStorage.getItem(VIDEO_SCALE_KEY)
const scale = localStorage.getItem(VIDEO_SCALE_KEY);
if (scale && Number(scale)) {
return Number(scale)
return Number(scale);
}
return null
return null;
}
export function setVideoScale(scale: number): void {
localStorage.setItem(VIDEO_SCALE_KEY, String(scale))
localStorage.setItem(VIDEO_SCALE_KEY, String(scale));
}
export function getIsMenuOpen(): boolean {
@@ -124,14 +123,12 @@ export function setMouseScrollInterval(interval: number): void {
localStorage.setItem(MOUSE_SCROLL_INTERVAL_KEY, String(interval));
}
export function getShortcuts(): ShortcutProps[] {
const shortcuts = localStorage.getItem(SHORTCUTS_KEY);
if (!shortcuts) return [];
return window.JSON.parse(shortcuts);
export function getShortcuts(): string | null {
return localStorage.getItem(KEYBOARD_SHORTCUT_KEY);
}
export function setShortcuts(shortcuts: ShortcutProps[]): void {
localStorage.setItem(SHORTCUTS_KEY, window.JSON.stringify(shortcuts));
export function setShortcuts(shortcuts: string): void {
localStorage.setItem(KEYBOARD_SHORTCUT_KEY, shortcuts);
}
export function getMouseJigglerMode(): 'enable' | 'disable' {