mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-11-10 01:14:16 -06:00
refactor: optimize keyboard keys and refactor keyboard shortcut
This commit is contained in:
@@ -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
179
browser/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
96
browser/src/components/menu/keyboard/shortcuts/index.tsx
Normal file
96
browser/src/components/menu/keyboard/shortcuts/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
256
browser/src/components/menu/keyboard/shortcuts/recorder.tsx
Normal file
256
browser/src/components/menu/keyboard/shortcuts/recorder.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
65
browser/src/components/menu/keyboard/shortcuts/shortcut.tsx
Normal file
65
browser/src/components/menu/keyboard/shortcuts/shortcut.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
9
browser/src/components/menu/keyboard/shortcuts/types.ts
Normal file
9
browser/src/components/menu/keyboard/shortcuts/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface KeyInfo {
|
||||
code: string;
|
||||
label: string;
|
||||
isModifier: boolean;
|
||||
}
|
||||
|
||||
export interface Shortcut {
|
||||
keys: KeyInfo[];
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
28
browser/src/components/ui/kbd.tsx
Normal file
28
browser/src/components/ui/kbd.tsx
Normal 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 };
|
||||
53
browser/src/components/ui/scroll-area.tsx
Normal file
53
browser/src/components/ui/scroll-area.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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' {
|
||||
|
||||
Reference in New Issue
Block a user