mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-09-10 21:49:56 -05:00
refactor: refactor keyboard, mouse and menu bar
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
27
browser/eslint.config.js
Normal file
27
browser/eslint.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import js from '@eslint/js';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'node_modules'] },
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooksPlugin,
|
||||
'react-refresh': reactRefreshPlugin
|
||||
},
|
||||
rules: {
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -7,6 +7,7 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,14 +16,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^24.0.5",
|
||||
"jotai": "^2.10.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-i18next": "^15.1.4",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-simple-keyboard": "^3.8.28",
|
||||
"vaul": "^1.1.1",
|
||||
"yocto-queue": "^1.1.1"
|
||||
"vaul": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
@@ -39,6 +40,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^15.12.0",
|
||||
"minimatch": "^9.0.5",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
|
||||
5504
browser/pnpm-lock.yaml
generated
5504
browser/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
overrides:
|
||||
minimatch: ^9.0.0
|
||||
'@babel/helpers@<7.26.10': '>=7.26.10'
|
||||
'@babel/runtime@<7.26.10': '>=7.26.10'
|
||||
'@eslint/plugin-kit@<0.3.4': '>=0.3.4'
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #000;
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
}
|
||||
|
||||
.simple-keyboard-main.simple-keyboard .hg-button {
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.simple-keyboard-main.simple-keyboard .hg-row:first-child {
|
||||
@@ -51,7 +53,9 @@
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.controlArrows {
|
||||
@@ -76,7 +80,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.hg-button.hg-functionBtn.hg-button-space {
|
||||
@@ -90,16 +96,20 @@
|
||||
.simple-keyboard .hg-highlight {
|
||||
background: rgb(37 99 235);
|
||||
border-bottom: 1px solid #2563eb;
|
||||
box-shadow: 0 1px 3px 0 rgb(37 99 235 / 0.1), 0 1px 2px -1px rgb(37 99 235 / 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(37 99 235 / 0.1),
|
||||
0 1px 2px -1px rgb(37 99 235 / 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.simple-keyboard .hg-button.hg-double{
|
||||
.simple-keyboard .hg-button.hg-double {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.keyboard-header {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
@@ -1,103 +1,173 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard';
|
||||
import { getOperatingSystem } from '@/libs/browser';
|
||||
import { device } from '@/libs/device';
|
||||
import { Modifiers } from '@/libs/device/keyboard.ts';
|
||||
import { KeyboardCodes } from '@/libs/keyboard';
|
||||
import { KeyboardReport } from '@/libs/keyboard/keyboard.ts';
|
||||
import { isModifier } from '@/libs/keyboard/keymap.ts';
|
||||
|
||||
const MAX_SIMULTANEOUS_KEYS = 6;
|
||||
const ModifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
|
||||
interface AltGrState {
|
||||
active: boolean;
|
||||
ctrlLeftTimestamp: number;
|
||||
}
|
||||
|
||||
const ALTGR_THRESHOLD_MS = 10;
|
||||
|
||||
export const Keyboard = () => {
|
||||
const pressedKeysRef = useRef<Set<number>>(new Set());
|
||||
const pressedModifiersRef = useRef<Set<string>>(new Set());
|
||||
const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom);
|
||||
|
||||
const keyboardRef = useRef(new KeyboardReport());
|
||||
const pressedKeys = useRef(new Set<string>());
|
||||
const altGrState = useRef<AltGrState | null>(null);
|
||||
const isComposing = useRef(false);
|
||||
|
||||
// listen keyboard events
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('blur', releaseAllKeys);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
window.removeEventListener('blur', releaseAllKeys);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// press button
|
||||
async function handleKeyDown(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (ModifierKeys.has(event.key)) {
|
||||
pressedModifiersRef.current.add(event.code);
|
||||
} else {
|
||||
const keyCode = KeyboardCodes.get(event.code);
|
||||
if (
|
||||
keyCode !== undefined &&
|
||||
!pressedKeysRef.current.has(keyCode) &&
|
||||
pressedKeysRef.current.size < MAX_SIMULTANEOUS_KEYS
|
||||
) {
|
||||
pressedKeysRef.current.add(keyCode);
|
||||
}
|
||||
if (getOperatingSystem() === 'Windows' && !altGrState.current) {
|
||||
altGrState.current = { active: false, ctrlLeftTimestamp: 0 };
|
||||
}
|
||||
|
||||
await sendKeyData();
|
||||
}
|
||||
|
||||
// release button
|
||||
async function handleKeyUp(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (ModifierKeys.has(event.key)) {
|
||||
pressedModifiersRef.current.delete(event.code);
|
||||
} else {
|
||||
const commonKeyCode = KeyboardCodes.get(event.code);
|
||||
if (commonKeyCode !== undefined && pressedKeysRef.current.has(commonKeyCode)) {
|
||||
pressedKeysRef.current.delete(commonKeyCode);
|
||||
}
|
||||
}
|
||||
|
||||
await sendKeyData();
|
||||
}
|
||||
|
||||
// release all keys when page loses focus
|
||||
async function releaseAllKeys() {
|
||||
if (pressedKeysRef.current.size === 0 && pressedModifiersRef.current.size === 0) {
|
||||
if (!isKeyboardEnabled) {
|
||||
releaseKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
const modifiers = new Modifiers();
|
||||
const keys = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
await device.sendKeyboardData(modifiers, keys);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
document.addEventListener('compositionstart', handleCompositionStart);
|
||||
document.addEventListener('compositionend', handleCompositionEnd);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
pressedKeysRef.current.clear();
|
||||
pressedModifiersRef.current.clear();
|
||||
}
|
||||
// Key down event
|
||||
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
||||
if (!isKeyboardEnabled) return;
|
||||
|
||||
// release all keys when page is hidden
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
releaseAllKeys();
|
||||
// Skip during IME composition
|
||||
if (isComposing.current || event.isComposing) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const code = event.code;
|
||||
if (pressedKeys.current.has(code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When AltGr is pressed, browsers send ControlLeft followed immediately by AltRight
|
||||
if (altGrState.current) {
|
||||
if (code === 'ControlLeft') {
|
||||
altGrState.current.ctrlLeftTimestamp = event.timeStamp;
|
||||
} else if (code === 'AltRight') {
|
||||
const timeDiff = event.timeStamp - altGrState.current.ctrlLeftTimestamp;
|
||||
if (timeDiff < ALTGR_THRESHOLD_MS && pressedKeys.current.has('ControlLeft')) {
|
||||
pressedKeys.current.delete('ControlLeft');
|
||||
handleKeyEvent({ type: 'keyup', code: 'ControlLeft' });
|
||||
altGrState.current.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.add(code);
|
||||
await handleKeyEvent({ type: 'keydown', code });
|
||||
}
|
||||
}
|
||||
|
||||
// send keyboard data
|
||||
async function sendKeyData() {
|
||||
const modifiers = new Modifiers();
|
||||
pressedModifiersRef.current.forEach((code) => {
|
||||
modifiers.setModifier(code);
|
||||
});
|
||||
// Key up event
|
||||
async function handleKeyUp(event: KeyboardEvent): Promise<void> {
|
||||
if (!isKeyboardEnabled) return;
|
||||
|
||||
const keys = [
|
||||
...Array.from(pressedKeysRef.current),
|
||||
...new Array(MAX_SIMULTANEOUS_KEYS - pressedKeysRef.current.size).fill(0x00)
|
||||
];
|
||||
if (isComposing.current || event.isComposing) return;
|
||||
|
||||
await device.sendKeyboardData(modifiers, keys);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const code = event.code;
|
||||
|
||||
// Handle AltGr state for Windows
|
||||
if (altGrState.current?.active) {
|
||||
if (code === 'ControlLeft') return;
|
||||
|
||||
if (code === 'AltRight') {
|
||||
altGrState.current.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible with macOS's command key combinations
|
||||
if (code === 'MetaLeft' || code === 'MetaRight') {
|
||||
const keysToRelease: string[] = [];
|
||||
pressedKeys.current.forEach((pressedCode) => {
|
||||
if (!isModifier(pressedCode)) {
|
||||
keysToRelease.push(pressedCode);
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of keysToRelease) {
|
||||
await handleKeyEvent({ type: 'keyup', code: key });
|
||||
pressedKeys.current.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.delete(code);
|
||||
await handleKeyEvent({ type: 'keyup', code });
|
||||
}
|
||||
|
||||
// Composition start event
|
||||
function handleCompositionStart(): void {
|
||||
isComposing.current = true;
|
||||
}
|
||||
|
||||
// Composition end event
|
||||
function handleCompositionEnd(): void {
|
||||
isComposing.current = false;
|
||||
}
|
||||
|
||||
// Release all keys when window loses focus
|
||||
async function handleBlur(): Promise<void> {
|
||||
await releaseKeys();
|
||||
}
|
||||
|
||||
// Release all keys before window closes
|
||||
async function handleVisibilityChange(): Promise<void> {
|
||||
if (document.hidden) {
|
||||
await releaseKeys();
|
||||
}
|
||||
}
|
||||
|
||||
// Release all keys
|
||||
async function releaseKeys(): Promise<void> {
|
||||
for (const code of pressedKeys.current) {
|
||||
await handleKeyEvent({ type: 'keyup', code });
|
||||
}
|
||||
|
||||
pressedKeys.current.clear();
|
||||
|
||||
// Reset AltGr state
|
||||
if (altGrState.current) {
|
||||
altGrState.current.active = false;
|
||||
altGrState.current.ctrlLeftTimestamp = 0;
|
||||
}
|
||||
|
||||
const report = keyboardRef.current.reset();
|
||||
await device.sendKeyboardData(report);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
document.removeEventListener('compositionstart', handleCompositionStart);
|
||||
document.removeEventListener('compositionend', handleCompositionEnd);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
releaseKeys();
|
||||
};
|
||||
}, [isKeyboardEnabled]);
|
||||
|
||||
// Keyboard handler
|
||||
async function handleKeyEvent(event: { type: 'keydown' | 'keyup'; code: string }): Promise<void> {
|
||||
const kb = keyboardRef.current;
|
||||
const report = event.type === 'keydown' ? kb.keyDown(event.code) : kb.keyUp(event.code);
|
||||
await device.sendKeyboardData(report);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Divider } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { MenuIcon, XIcon } from 'lucide-react';
|
||||
import { ChevronRightIcon, GripVerticalIcon, XIcon } from 'lucide-react';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import { serialStateAtom } from '@/jotai/device.ts';
|
||||
import * as storage from '@/libs/storage';
|
||||
@@ -18,12 +19,67 @@ import { Video } from './video';
|
||||
export const Menu = () => {
|
||||
const serialState = useAtomValue(serialStateAtom);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [menuBounds, setMenuBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const nodeRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const { offsetWidth, offsetHeight } = nodeRef.current;
|
||||
|
||||
setMenuBounds({
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: innerWidth - offsetWidth,
|
||||
bottom: innerHeight - offsetHeight
|
||||
});
|
||||
}, []);
|
||||
|
||||
const initializePosition = useCallback(() => {
|
||||
if (!nodeRef.current || initialized) return;
|
||||
|
||||
const { innerWidth } = window;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!nodeRef.current) return;
|
||||
const width = nodeRef.current.offsetWidth;
|
||||
setPosition({
|
||||
x: (innerWidth - width) / 2,
|
||||
y: 10
|
||||
});
|
||||
setInitialized(true);
|
||||
});
|
||||
}, [initialized]);
|
||||
|
||||
useEffect(() => {
|
||||
const isOpen = storage.getIsMenuOpen();
|
||||
setIsMenuOpen(isOpen);
|
||||
}, []);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
if (nodeRef.current) {
|
||||
observer.observe(nodeRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
initializePosition();
|
||||
}, [isMenuOpen, handleResize, initializePosition]);
|
||||
|
||||
function toggleMenu() {
|
||||
const isOpen = !isMenuOpen;
|
||||
@@ -33,52 +89,75 @@ export const Menu = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2">
|
||||
<div className="sticky top-[10px]">
|
||||
<div
|
||||
className={clsx(
|
||||
'h-[34px] items-center justify-between space-x-1.5 rounded bg-neutral-800/70 px-3',
|
||||
isMenuOpen ? 'flex' : 'hidden'
|
||||
)}
|
||||
>
|
||||
<Video />
|
||||
|
||||
{serialState === 'connected' && (
|
||||
<>
|
||||
<SerialPort />
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Keyboard />
|
||||
<Mouse />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Recorder />
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Settings />
|
||||
<Fullscreen />
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
bounds={menuBounds}
|
||||
handle="strong"
|
||||
position={position}
|
||||
onDrag={(_, data) => setPosition({ x: data.x, y: data.y })}
|
||||
>
|
||||
<div ref={nodeRef} className="fixed left-0 top-0 z-[1000]">
|
||||
{/* Menubar */}
|
||||
<div className="sticky top-[10px] flex w-full justify-center">
|
||||
<div
|
||||
className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/70 hover:text-white"
|
||||
onClick={toggleMenu}
|
||||
className={clsx(
|
||||
'h-[34px] items-center justify-between space-x-1.5 rounded bg-neutral-800/70 px-2',
|
||||
isMenuOpen ? 'flex' : 'hidden'
|
||||
)}
|
||||
>
|
||||
<XIcon size={18} />
|
||||
<strong>
|
||||
<div className="flex h-[28px] cursor-move select-none items-center justify-center text-neutral-500">
|
||||
<GripVerticalIcon size={18} />
|
||||
</div>
|
||||
</strong>
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Video />
|
||||
|
||||
{serialState === 'connected' && (
|
||||
<>
|
||||
<SerialPort />
|
||||
|
||||
<Divider type="vertical" className="px-0.5" />
|
||||
|
||||
<Keyboard />
|
||||
<Mouse />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Recorder />
|
||||
|
||||
<Divider type="vertical" className="px-0.5" />
|
||||
|
||||
<Settings />
|
||||
<Fullscreen />
|
||||
<div
|
||||
className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/70"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<XIcon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menubar expand button */}
|
||||
{!isMenuOpen && (
|
||||
<div className="flex items-center rounded-lg bg-neutral-800/50 p-1">
|
||||
<strong>
|
||||
<div className="flex size-[26px] cursor-move select-none items-center justify-center text-neutral-500">
|
||||
<GripVerticalIcon size={18} />
|
||||
</div>
|
||||
</strong>
|
||||
<Divider type="vertical" style={{ margin: '0 4px' }} />
|
||||
<div
|
||||
className="flex size-[26px] cursor-pointer items-center justify-center rounded text-neutral-500 hover:bg-neutral-700/70 hover:text-white"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<ChevronRightIcon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMenuOpen && (
|
||||
<div
|
||||
className="flex h-[30px] w-[35px] cursor-pointer items-center justify-center rounded bg-neutral-800/50 text-white/50 hover:bg-neutral-800 hover:text-white"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<MenuIcon size={18} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,14 +3,14 @@ import { ClipboardIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { device } from '@/libs/device';
|
||||
import { Modifiers } from '@/libs/device/keyboard.ts';
|
||||
import { CharCodes, ShiftChars } from '@/libs/keyboard';
|
||||
import { CharCodes, ShiftChars } from '@/libs/keyboard/charCodes.ts';
|
||||
import { getModifierBit } from '@/libs/keyboard/keymap.ts';
|
||||
|
||||
export const Paste = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function paste() {
|
||||
async function paste(): Promise<void> {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -21,15 +21,17 @@ export const Paste = () => {
|
||||
for (const char of text) {
|
||||
const ascii = char.charCodeAt(0);
|
||||
|
||||
const code = CharCodes.get(ascii);
|
||||
const code = CharCodes[ascii];
|
||||
if (!code) continue;
|
||||
|
||||
const modifiers = new Modifiers();
|
||||
if ((ascii >= 65 && ascii <= 90) || ShiftChars.has(ascii)) {
|
||||
modifiers.leftShift = true;
|
||||
let modifier = 0;
|
||||
if ((ascii >= 65 && ascii <= 90) || ShiftChars[ascii]) {
|
||||
modifier |= getModifierBit('ShiftLeft');
|
||||
}
|
||||
|
||||
await send(modifiers, code);
|
||||
await send(modifier, code);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await send(0, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@@ -38,11 +40,9 @@ export const Paste = () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function send(modifiers: Modifiers, code: number) {
|
||||
const keys = [code, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
await device.sendKeyboardData(modifiers, keys);
|
||||
|
||||
await device.sendKeyboardData(new Modifiers(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
async function send(modifier: number, code: number): Promise<void> {
|
||||
const keys = [modifier, 0, code, 0, 0, 0, 0, 0];
|
||||
await device.sendKeyboardData(keys);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,21 +14,21 @@ export const Shortcuts = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [customShortcuts, setCustomShortcuts] = useState<ShortcutInterface[]>([]);
|
||||
|
||||
const defaultShortcuts: ShortcutInterface[] = [
|
||||
{
|
||||
keys: [
|
||||
{ code: 'MetaLeft', label: 'Win', isModifier: true },
|
||||
{ code: 'Tab', label: 'Tab', isModifier: false }
|
||||
{ code: 'MetaLeft', label: 'Win' },
|
||||
{ code: 'Tab', label: 'Tab' }
|
||||
]
|
||||
},
|
||||
{
|
||||
keys: [
|
||||
{ code: 'ControlLeft', label: 'Ctrl', isModifier: true },
|
||||
{ code: 'AltLeft', label: 'Alt', isModifier: true },
|
||||
{ code: 'Delete', label: '⌫', isModifier: false }
|
||||
{ code: 'ControlLeft', label: 'Ctrl' },
|
||||
{ code: 'AltLeft', label: 'Alt' },
|
||||
{ code: 'Delete', label: '⌫' }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -39,18 +39,29 @@ export const Shortcuts = () => {
|
||||
setCustomShortcuts(JSON.parse(shortcuts));
|
||||
}, []);
|
||||
|
||||
function addShortcut(shortcut: ShortcutInterface) {
|
||||
function addShortcut(shortcut: ShortcutInterface): void {
|
||||
const shortcuts = [...customShortcuts, shortcut];
|
||||
setCustomShortcuts(shortcuts);
|
||||
storage.setShortcuts(JSON.stringify(shortcuts));
|
||||
}
|
||||
|
||||
function delShortcut(index: number) {
|
||||
function delShortcut(index: number): void {
|
||||
const shortcuts = customShortcuts.filter((_, i) => i !== index);
|
||||
setCustomShortcuts(shortcuts);
|
||||
storage.setShortcuts(JSON.stringify(shortcuts));
|
||||
}
|
||||
|
||||
function handleOpenChange(open: boolean): void {
|
||||
if (open) {
|
||||
setIsOpen(true);
|
||||
return;
|
||||
}
|
||||
if (isRecording) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<ScrollArea className="max-w-[400px] [&>[data-radix-scroll-area-viewport]]:max-h-[350px]">
|
||||
{/* custom shortcuts */}
|
||||
@@ -71,7 +82,12 @@ export const Shortcuts = () => {
|
||||
|
||||
<Divider style={{ margin: '5px 0 5px 0' }} />
|
||||
|
||||
<Recorder shortcuts={customShortcuts} addShortcut={addShortcut} delShortcut={delShortcut} />
|
||||
<Recorder
|
||||
shortcuts={customShortcuts}
|
||||
addShortcut={addShortcut}
|
||||
delShortcut={delShortcut}
|
||||
setIsRecording={setIsRecording}
|
||||
/>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
@@ -82,7 +98,7 @@ export const Shortcuts = () => {
|
||||
placement="rightTop"
|
||||
align={{ offset: [14, 0] }}
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
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">
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 { isModifier } from '@/libs/keyboard/keymap.ts';
|
||||
|
||||
import { KeyInfo, Shortcut } from './types.ts';
|
||||
|
||||
@@ -50,14 +50,20 @@ interface RecorderProps {
|
||||
shortcuts: Shortcut[];
|
||||
addShortcut: (shortcut: Shortcut) => void;
|
||||
delShortcut: (index: number) => void;
|
||||
setIsRecording: (isRecording: boolean) => void;
|
||||
}
|
||||
|
||||
export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps) => {
|
||||
export const Recorder = ({
|
||||
shortcuts,
|
||||
addShortcut,
|
||||
delShortcut,
|
||||
setIsRecording
|
||||
}: RecorderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [shortcutLabel, setShortcutLabel] = useState('');
|
||||
|
||||
const inputRef = useRef<InputRef | null>(null);
|
||||
@@ -65,6 +71,7 @@ export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps)
|
||||
|
||||
useEffect(() => {
|
||||
setIsKeyboardEnable(!isModalOpen);
|
||||
setIsRecording(isModalOpen);
|
||||
|
||||
if (!isModalOpen) return;
|
||||
|
||||
@@ -78,9 +85,9 @@ export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps)
|
||||
}, [isModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRecording) return;
|
||||
if (!isFocused) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -93,8 +100,7 @@ export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps)
|
||||
|
||||
const keyInfo: KeyInfo = {
|
||||
code: code,
|
||||
label: formatKeyDisplay(key, code),
|
||||
isModifier: modifierKeys.has(key)
|
||||
label: formatKeyDisplay(key, code)
|
||||
};
|
||||
|
||||
setShortcutLabel((prev) => (prev ? `${prev} + ${keyInfo.label}` : keyInfo.label));
|
||||
@@ -106,10 +112,10 @@ export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isRecording]);
|
||||
}, [isFocused]);
|
||||
|
||||
const formatKeyDisplay = (key: string, code: string): string => {
|
||||
if (modifierKeys.has(key)) {
|
||||
if (isModifier(code)) {
|
||||
if (code.startsWith('Control')) {
|
||||
return 'Ctrl';
|
||||
}
|
||||
@@ -208,8 +214,8 @@ export const Recorder = ({ shortcuts, addShortcut, delShortcut }: RecorderProps)
|
||||
placeholder={t('keyboard.shortcut.capture')}
|
||||
prefix={<KeyboardIcon size={16} className="text-neutral-500" />}
|
||||
value={shortcutLabel}
|
||||
onFocus={() => setIsRecording(true)}
|
||||
onBlur={() => setIsRecording(false)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
|
||||
<Button onClick={clearShortcut}>{t('keyboard.shortcut.clear')}</Button>
|
||||
|
||||
@@ -2,8 +2,7 @@ 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 { KeyboardReport } from '@/libs/keyboard/keyboard.ts';
|
||||
|
||||
import type { Shortcut as ShortcutInterface } from './types.ts';
|
||||
|
||||
@@ -14,35 +13,12 @@ type ShortcutProps = {
|
||||
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();
|
||||
await sendShortcut();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
@@ -50,6 +26,18 @@ export const Shortcut = ({ shortcut }: ShortcutProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendShortcut(): Promise<void> {
|
||||
const keyboard = new KeyboardReport();
|
||||
|
||||
for (const key of shortcut.keys) {
|
||||
const report = keyboard.keyDown(key.code);
|
||||
await device.sendKeyboardData(report);
|
||||
}
|
||||
|
||||
const report = keyboard.reset();
|
||||
await device.sendKeyboardData(report);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[32px] w-full cursor-pointer items-center space-x-1 rounded px-3 hover:bg-neutral-700/30"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface KeyInfo {
|
||||
code: string;
|
||||
label: string;
|
||||
isModifier: boolean;
|
||||
}
|
||||
|
||||
export interface Shortcut {
|
||||
|
||||
@@ -1,164 +1,175 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { resolutionAtom, videoRotationAtom } from '@/jotai/device.ts';
|
||||
import { createInitialTouchState, createTouchHandlers } from '@/components/mouse/touchpad.ts';
|
||||
import { MouseAbsoluteEvent } from '@/components/mouse/types.ts';
|
||||
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
|
||||
import { device } from '@/libs/device';
|
||||
import { Key } from '@/libs/device/mouse.ts';
|
||||
import { MouseAbsoluteRelative } from '@/libs/mouse';
|
||||
import { mouseJiggler } from '@/libs/mouse-jiggler';
|
||||
|
||||
export const Absolute = () => {
|
||||
const resolution = useAtomValue(resolutionAtom);
|
||||
const videoRotation = useAtomValue(videoRotationAtom);
|
||||
const isBigScreen = useMediaQuery({ minWidth: 650 });
|
||||
|
||||
const scrollDirection = useAtomValue(scrollDirectionAtom);
|
||||
const scrollInterval = useAtomValue(scrollIntervalAtom);
|
||||
|
||||
const keyRef = useRef<Key>(new Key());
|
||||
const mouseRef = useRef(new MouseAbsoluteRelative());
|
||||
const lastPosRef = useRef({ x: 0.5, y: 0.5 });
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
const touchStateRef = useRef(createInitialTouchState());
|
||||
|
||||
// listen mouse events
|
||||
useEffect(() => {
|
||||
const canvas = document.getElementById('video');
|
||||
if (!canvas) return;
|
||||
const screen = document.getElementById('video') as HTMLVideoElement;
|
||||
if (!screen) return;
|
||||
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
canvas.addEventListener('mouseup', handleMouseUp);
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('wheel', handleWheel);
|
||||
canvas.addEventListener('click', disableEvent);
|
||||
canvas.addEventListener('contextmenu', disableEvent);
|
||||
// Add mouse event listeners
|
||||
screen.addEventListener('mousedown', handleMouseDown);
|
||||
screen.addEventListener('mouseup', handleMouseUp);
|
||||
screen.addEventListener('mousemove', handleMouseMove);
|
||||
screen.addEventListener('wheel', handleWheel);
|
||||
screen.addEventListener('click', disableEvent);
|
||||
screen.addEventListener('contextmenu', disableEvent);
|
||||
|
||||
// press button
|
||||
async function handleMouseDown(event: any) {
|
||||
disableEvent(event);
|
||||
// Create touch handlers
|
||||
const touchState = touchStateRef.current;
|
||||
const touchHandlers = createTouchHandlers(touchState, {
|
||||
scrollDirection,
|
||||
scrollInterval,
|
||||
getCoordinate,
|
||||
handleMouseEvent,
|
||||
disableEvent
|
||||
});
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
keyRef.current.left = true;
|
||||
break;
|
||||
case 1:
|
||||
keyRef.current.mid = true;
|
||||
break;
|
||||
case 2:
|
||||
keyRef.current.right = true;
|
||||
break;
|
||||
default:
|
||||
console.log(`unknown button ${event.button}`);
|
||||
return;
|
||||
// Add touch event listeners (only on big screens)
|
||||
if (isBigScreen) {
|
||||
screen.addEventListener('touchstart', touchHandlers.handleTouchStart);
|
||||
screen.addEventListener('touchmove', touchHandlers.handleTouchMove);
|
||||
screen.addEventListener('touchend', touchHandlers.handleTouchEnd);
|
||||
screen.addEventListener('touchcancel', touchHandlers.handleTouchCancel);
|
||||
}
|
||||
|
||||
// Mouse down event
|
||||
function handleMouseDown(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
handleMouseEvent({ type: 'mousedown', button: e.button });
|
||||
}
|
||||
|
||||
// Mouse up event
|
||||
function handleMouseUp(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
handleMouseEvent({ type: 'mouseup', button: e.button });
|
||||
}
|
||||
|
||||
// Mouse move event
|
||||
function handleMouseMove(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
const { x, y } = getCoordinate(e);
|
||||
handleMouseEvent({ type: 'move', x, y });
|
||||
}
|
||||
|
||||
// Mouse wheel event
|
||||
function handleWheel(e: WheelEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (Math.floor(e.deltaY) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await send(event);
|
||||
}
|
||||
|
||||
// release button
|
||||
async function handleMouseUp(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
keyRef.current.left = false;
|
||||
break;
|
||||
case 1:
|
||||
keyRef.current.mid = false;
|
||||
break;
|
||||
case 2:
|
||||
keyRef.current.right = false;
|
||||
break;
|
||||
default:
|
||||
console.log(`unknown button ${event.button}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await send(event);
|
||||
}
|
||||
|
||||
// mouse move
|
||||
async function handleMouseMove(event: any) {
|
||||
disableEvent(event);
|
||||
await send(event);
|
||||
|
||||
mouseJiggler.moveEventCallback();
|
||||
}
|
||||
|
||||
// mouse scroll
|
||||
async function handleWheel(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastScrollTimeRef.current < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = Math.floor(event.deltaY);
|
||||
if (delta === 0) return;
|
||||
|
||||
await send(event, delta > 0 ? -1 * scrollDirection : scrollDirection);
|
||||
|
||||
const deltaY = (e.deltaY > 0 ? 1 : -1) * scrollDirection;
|
||||
handleMouseEvent({ type: 'wheel', deltaY });
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
async function send(event: MouseEvent, scroll: number = 0) {
|
||||
const { x, y } = getCorrectedCoords(event.clientX, event.clientY);
|
||||
await device.sendMouseAbsoluteData(keyRef.current, 1, 1, x, y, scroll);
|
||||
}
|
||||
// Calculate mouse coordinate
|
||||
function getCoordinate(event: { clientX: number; clientY: number }): { x: number; y: number } {
|
||||
const rect = screen.getBoundingClientRect();
|
||||
|
||||
function getCorrectedCoords(clientX: number, clientY: number) {
|
||||
if (!canvas) {
|
||||
return { x: 0, y: 0 };
|
||||
const clientX = event.clientX;
|
||||
const clientY = event.clientY;
|
||||
|
||||
if (!screen.videoWidth || !screen.videoHeight) {
|
||||
const x = (clientX - rect.left) / rect.width;
|
||||
const y = (clientY - rect.top) / rect.height;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const videoElement = canvas as HTMLVideoElement;
|
||||
const videoRatio = screen.videoWidth / screen.videoHeight;
|
||||
const elementRatio = rect.width / rect.height;
|
||||
|
||||
const videoWidth = videoElement.videoWidth || 1920;
|
||||
const videoHeight = videoElement.videoHeight || 1080;
|
||||
let renderedWidth = rect.width;
|
||||
let renderedHeight = rect.height;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
const screenVideoRatio =
|
||||
videoRotation === 90 || videoRotation === 270
|
||||
? videoHeight / videoWidth
|
||||
: videoWidth / videoHeight;
|
||||
|
||||
const containerWidth = rect.width;
|
||||
const containerHeight = rect.height;
|
||||
const containerRatio = containerWidth / containerHeight;
|
||||
|
||||
let renderedWidth: number;
|
||||
let renderedHeight: number;
|
||||
|
||||
if (screenVideoRatio > containerRatio) {
|
||||
renderedWidth = containerWidth;
|
||||
renderedHeight = containerWidth / screenVideoRatio;
|
||||
if (videoRatio > elementRatio) {
|
||||
renderedHeight = rect.width / videoRatio;
|
||||
offsetY = (rect.height - renderedHeight) / 2;
|
||||
} else {
|
||||
renderedHeight = containerHeight;
|
||||
renderedWidth = containerHeight * screenVideoRatio;
|
||||
renderedWidth = rect.height * videoRatio;
|
||||
offsetX = (rect.width - renderedWidth) / 2;
|
||||
}
|
||||
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const relX = clientX - centerX;
|
||||
const relY = clientY - centerY;
|
||||
|
||||
let x = relX / renderedWidth + 0.5;
|
||||
let y = relY / renderedHeight + 0.5;
|
||||
|
||||
x = Math.max(0, Math.min(1, x));
|
||||
y = Math.max(0, Math.min(1, y));
|
||||
|
||||
const x = (clientX - rect.left - offsetX) / renderedWidth;
|
||||
const y = (clientY - rect.top - offsetY) / renderedHeight;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
canvas.removeEventListener('click', disableEvent);
|
||||
canvas.removeEventListener('contextmenu', disableEvent);
|
||||
};
|
||||
}, [resolution, scrollDirection, scrollInterval, videoRotation]);
|
||||
screen.removeEventListener('mousedown', handleMouseDown);
|
||||
screen.removeEventListener('mouseup', handleMouseUp);
|
||||
screen.removeEventListener('mousemove', handleMouseMove);
|
||||
screen.removeEventListener('wheel', handleWheel);
|
||||
screen.removeEventListener('click', disableEvent);
|
||||
screen.removeEventListener('contextmenu', disableEvent);
|
||||
screen.removeEventListener('touchstart', touchHandlers.handleTouchStart);
|
||||
screen.removeEventListener('touchmove', touchHandlers.handleTouchMove);
|
||||
screen.removeEventListener('touchend', touchHandlers.handleTouchEnd);
|
||||
screen.removeEventListener('touchcancel', touchHandlers.handleTouchCancel);
|
||||
|
||||
// disable default events
|
||||
function disableEvent(event: any) {
|
||||
touchHandlers.cleanup();
|
||||
};
|
||||
}, [isBigScreen, scrollDirection, scrollInterval]);
|
||||
|
||||
// Mouse event handler
|
||||
async function handleMouseEvent(event: MouseAbsoluteEvent): Promise<void> {
|
||||
let report: number[];
|
||||
const mouse = mouseRef.current;
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
mouse.buttonDown(event.button);
|
||||
report = mouse.buildButtonReport(lastPosRef.current.x, lastPosRef.current.y);
|
||||
break;
|
||||
case 'mouseup':
|
||||
mouse.buttonUp(event.button);
|
||||
report = mouse.buildButtonReport(lastPosRef.current.x, lastPosRef.current.y);
|
||||
break;
|
||||
case 'wheel':
|
||||
report = mouse.buildReport(lastPosRef.current.x, lastPosRef.current.y, event.deltaY);
|
||||
break;
|
||||
case 'move':
|
||||
report = mouse.buildReport(event.x, event.y);
|
||||
lastPosRef.current = { x: event.x, y: event.y };
|
||||
break;
|
||||
default:
|
||||
report = mouse.buildReport(lastPosRef.current.x, lastPosRef.current.y);
|
||||
break;
|
||||
}
|
||||
|
||||
await device.sendMouseData([0x02, ...report]);
|
||||
|
||||
mouseJiggler.moveEventCallback();
|
||||
}
|
||||
|
||||
// Disable default events
|
||||
function disableEvent(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -3,27 +3,144 @@ import { message } from 'antd';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { resolutionAtom } from '@/jotai/device.ts';
|
||||
import { MouseRelativeEvent } from '@/components/mouse/types.ts';
|
||||
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
|
||||
import { device } from '@/libs/device';
|
||||
import { Key } from '@/libs/device/mouse.ts';
|
||||
import { MouseReportRelative } from '@/libs/mouse';
|
||||
import { mouseJiggler } from '@/libs/mouse-jiggler';
|
||||
|
||||
export const Relative = () => {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const resolution = useAtomValue(resolutionAtom);
|
||||
const scrollDirection = useAtomValue(scrollDirectionAtom);
|
||||
const scrollInterval = useAtomValue(scrollIntervalAtom);
|
||||
|
||||
const mouseRef = useRef(new MouseReportRelative());
|
||||
const isLockedRef = useRef(false);
|
||||
const keyRef = useRef<Key>(new Key());
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
|
||||
showMessage();
|
||||
|
||||
useEffect(() => {
|
||||
const screen = document.getElementById('video');
|
||||
if (!screen) return;
|
||||
|
||||
screen.addEventListener('click', handleClick);
|
||||
screen.addEventListener('mousedown', handleMouseDown);
|
||||
screen.addEventListener('mouseup', handleMouseUp);
|
||||
screen.addEventListener('mousemove', handleMouseMove);
|
||||
screen.addEventListener('wheel', handleMouseWheel);
|
||||
screen.addEventListener('contextmenu', disableEvent);
|
||||
document.addEventListener('pointerlockchange', handlePointerLockChange);
|
||||
|
||||
// Click to request pointer lock
|
||||
function handleClick(event: MouseEvent): void {
|
||||
disableEvent(event);
|
||||
|
||||
if (!isLockedRef.current) {
|
||||
screen?.requestPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse down event
|
||||
function handleMouseDown(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
handleMouseEvent({ type: 'mousedown', button: e.button });
|
||||
}
|
||||
|
||||
// Mouse up event
|
||||
function handleMouseUp(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
handleMouseEvent({ type: 'mouseup', button: e.button });
|
||||
}
|
||||
|
||||
// Mouse move event
|
||||
function handleMouseMove(e: MouseEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
const x = e.movementX || 0;
|
||||
const y = e.movementY || 0;
|
||||
if (x === 0 && y === 0) return;
|
||||
|
||||
const deltaX = Math.abs(x * window.devicePixelRatio) < 10 ? x * 2 : x;
|
||||
const deltaY = Math.abs(y * window.devicePixelRatio) < 10 ? y * 2 : y;
|
||||
|
||||
handleMouseEvent({ type: 'move', deltaX, deltaY });
|
||||
}
|
||||
|
||||
// Mouse wheel event
|
||||
function handleMouseWheel(e: WheelEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (Math.floor(e.deltaY) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastScrollTimeRef.current < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = (e.deltaY > 0 ? 1 : -1) * scrollDirection;
|
||||
handleMouseEvent({ type: 'wheel', deltaY });
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
// Pointer lock state change
|
||||
function handlePointerLockChange(): void {
|
||||
isLockedRef.current = document.pointerLockElement === screen;
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
// Exit pointer lock when component unmounts
|
||||
if (document.pointerLockElement === screen) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
|
||||
screen.removeEventListener('click', handleClick);
|
||||
screen.removeEventListener('mousedown', handleMouseDown);
|
||||
screen.removeEventListener('mouseup', handleMouseUp);
|
||||
screen.removeEventListener('mousemove', handleMouseMove);
|
||||
screen.removeEventListener('wheel', handleMouseWheel);
|
||||
screen.removeEventListener('contextmenu', disableEvent);
|
||||
document.removeEventListener('pointerlockchange', handlePointerLockChange);
|
||||
};
|
||||
}, [scrollDirection, scrollInterval]);
|
||||
|
||||
// Mouse handler
|
||||
async function handleMouseEvent(event: MouseRelativeEvent): Promise<void> {
|
||||
let report: number[];
|
||||
const mouse = mouseRef.current;
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
mouse.buttonDown(event.button);
|
||||
report = mouse.buildButtonReport();
|
||||
break;
|
||||
case 'mouseup':
|
||||
mouse.buttonUp(event.button);
|
||||
report = mouse.buildButtonReport();
|
||||
break;
|
||||
case 'wheel':
|
||||
report = mouse.buildReport(0, 0, event.deltaY);
|
||||
break;
|
||||
case 'move':
|
||||
report = mouse.buildReport(event.deltaX, event.deltaY);
|
||||
break;
|
||||
default:
|
||||
report = mouse.buildReport(0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
await device.sendMouseData([0x01, ...report]);
|
||||
|
||||
mouseJiggler.moveEventCallback();
|
||||
}
|
||||
|
||||
function showMessage(): void {
|
||||
messageApi.open({
|
||||
key: 'relative',
|
||||
key: 'requestPointer',
|
||||
type: 'info',
|
||||
content: t('mouse.requestPointer'),
|
||||
duration: 3,
|
||||
@@ -31,124 +148,9 @@ export const Relative = () => {
|
||||
marginTop: '40vh'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// listen mouse events
|
||||
useEffect(() => {
|
||||
const canvas = document.getElementById('video');
|
||||
if (!canvas) return;
|
||||
|
||||
document.addEventListener('pointerlockchange', handlePointerLockChange);
|
||||
canvas.addEventListener('click', handleClick);
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
canvas.addEventListener('mouseup', handleMouseUp);
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('wheel', handleWheel);
|
||||
canvas.addEventListener('contextmenu', disableEvent);
|
||||
|
||||
function handleClick(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
if (!isLockedRef.current) {
|
||||
canvas!.requestPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerLockChange() {
|
||||
isLockedRef.current = document.pointerLockElement === canvas;
|
||||
}
|
||||
|
||||
// press button
|
||||
async function handleMouseDown(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
keyRef.current.left = true;
|
||||
break;
|
||||
case 1:
|
||||
keyRef.current.mid = true;
|
||||
break;
|
||||
case 2:
|
||||
keyRef.current.right = true;
|
||||
break;
|
||||
default:
|
||||
console.log(`unknown button ${event.button}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await send(0, 0, 0);
|
||||
}
|
||||
|
||||
// release button
|
||||
async function handleMouseUp(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
keyRef.current.left = false;
|
||||
break;
|
||||
case 1:
|
||||
keyRef.current.mid = false;
|
||||
break;
|
||||
case 2:
|
||||
keyRef.current.right = false;
|
||||
break;
|
||||
default:
|
||||
console.log(`unknown button ${event.button}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await send(0, 0, 0);
|
||||
}
|
||||
|
||||
// mouse move
|
||||
async function handleMouseMove(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
const x = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
|
||||
const y = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
|
||||
if (x === 0 && y === 0) return;
|
||||
|
||||
await send(Math.abs(x) < 10 ? x * 2 : x, Math.abs(y) < 10 ? y * 2 : y, 0);
|
||||
|
||||
mouseJiggler.moveEventCallback();
|
||||
}
|
||||
|
||||
// mouse scroll
|
||||
async function handleWheel(event: any) {
|
||||
disableEvent(event);
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastScrollTimeRef.current < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = Math.floor(event.deltaY);
|
||||
if (delta === 0) return;
|
||||
|
||||
await send(0, 0, delta > 0 ? -1 * scrollDirection : scrollDirection);
|
||||
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('pointerlockchange', handlePointerLockChange);
|
||||
canvas.removeEventListener('click', handleClick);
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
canvas.removeEventListener('contextmenu', disableEvent);
|
||||
};
|
||||
}, [resolution, scrollDirection, scrollInterval]);
|
||||
|
||||
async function send(x: number, y: number, scroll: number) {
|
||||
await device.sendMouseRelativeData(keyRef.current, x, y, scroll);
|
||||
}
|
||||
|
||||
// disable default events
|
||||
function disableEvent(event: any) {
|
||||
function disableEvent(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
205
browser/src/components/mouse/touchpad.ts
Normal file
205
browser/src/components/mouse/touchpad.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { MouseButton } from './types';
|
||||
import type { MouseAbsoluteEvent } from './types';
|
||||
|
||||
// Touch event thresholds
|
||||
const TAP_THRESHOLD = 8;
|
||||
const DRAG_THRESHOLD = 10;
|
||||
const VELOCITY_THRESHOLD = 0.3;
|
||||
const LONG_PRESS_DELAY = 800;
|
||||
|
||||
export interface TouchHandlerOptions {
|
||||
scrollDirection: number;
|
||||
scrollInterval: number;
|
||||
getCoordinate: (event: { clientX: number; clientY: number }) => { x: number; y: number };
|
||||
handleMouseEvent: (event: MouseAbsoluteEvent) => void;
|
||||
disableEvent: (event: Event) => void;
|
||||
}
|
||||
|
||||
export interface TouchState {
|
||||
touchStartTime: number;
|
||||
lastTouchY: number;
|
||||
longPressTimer: ReturnType<typeof setTimeout> | null;
|
||||
isLongPress: boolean;
|
||||
hasMove: boolean;
|
||||
isDragging: boolean;
|
||||
pressedButton: MouseButton | null;
|
||||
touchStartPos: { x: number; y: number };
|
||||
lastScrollTime: number;
|
||||
}
|
||||
|
||||
export function createInitialTouchState(): TouchState {
|
||||
return {
|
||||
touchStartTime: 0,
|
||||
lastTouchY: 0,
|
||||
longPressTimer: null,
|
||||
isLongPress: false,
|
||||
hasMove: false,
|
||||
isDragging: false,
|
||||
pressedButton: null,
|
||||
touchStartPos: { x: 0, y: 0 },
|
||||
lastScrollTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function createTouchHandlers(state: TouchState, options: TouchHandlerOptions) {
|
||||
const { scrollDirection, scrollInterval, getCoordinate, handleMouseEvent, disableEvent } =
|
||||
options;
|
||||
|
||||
function handleTouchStart(e: TouchEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (e.touches.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
|
||||
// Reset states
|
||||
state.touchStartTime = Date.now();
|
||||
state.lastTouchY = touch.clientY;
|
||||
state.isLongPress = false;
|
||||
state.hasMove = false;
|
||||
state.isDragging = false;
|
||||
state.pressedButton = null;
|
||||
state.touchStartPos = { x: touch.clientX, y: touch.clientY };
|
||||
|
||||
if (state.longPressTimer) {
|
||||
clearTimeout(state.longPressTimer);
|
||||
}
|
||||
|
||||
const { x, y } = getCoordinate(touch);
|
||||
handleMouseEvent({ type: 'move', x, y });
|
||||
|
||||
if (e.touches.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start long press timer
|
||||
state.longPressTimer = setTimeout(() => {
|
||||
state.isLongPress = true;
|
||||
state.pressedButton = MouseButton.Right;
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
|
||||
handleMouseEvent({ type: 'mousedown', button: MouseButton.Right });
|
||||
}, LONG_PRESS_DELAY);
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (e.touches.length === 0) {
|
||||
return;
|
||||
}
|
||||
const touch = e.touches[0];
|
||||
|
||||
// Handle two-finger scroll first
|
||||
if (e.touches.length > 1) {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - state.lastScrollTime < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = (touch.clientY - state.lastTouchY > 0 ? 1 : -1) * scrollDirection;
|
||||
handleMouseEvent({ type: 'wheel', deltaY });
|
||||
|
||||
state.lastTouchY = touch.clientY;
|
||||
state.lastScrollTime = currentTime;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = Math.abs(touch.clientX - state.touchStartPos.x);
|
||||
const deltaY = Math.abs(touch.clientY - state.touchStartPos.y);
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
const timeDelta = Date.now() - state.touchStartTime;
|
||||
const velocity = timeDelta > 0 ? distance / timeDelta : 0;
|
||||
|
||||
const shouldStartDrag =
|
||||
distance > DRAG_THRESHOLD || (distance > TAP_THRESHOLD && velocity > VELOCITY_THRESHOLD);
|
||||
|
||||
if (shouldStartDrag && !state.isDragging && !state.isLongPress) {
|
||||
if (!state.hasMove) {
|
||||
state.hasMove = true;
|
||||
}
|
||||
|
||||
if (state.longPressTimer) {
|
||||
clearTimeout(state.longPressTimer);
|
||||
state.longPressTimer = null;
|
||||
}
|
||||
|
||||
if (state.pressedButton === null) {
|
||||
state.isDragging = true;
|
||||
state.pressedButton = MouseButton.Left;
|
||||
handleMouseEvent({ type: 'mousedown', button: MouseButton.Left });
|
||||
}
|
||||
}
|
||||
|
||||
if (distance > TAP_THRESHOLD && !state.hasMove) {
|
||||
state.hasMove = true;
|
||||
}
|
||||
|
||||
if (state.isDragging || state.isLongPress) {
|
||||
const { x, y } = getCoordinate(touch);
|
||||
handleMouseEvent({ type: 'move', x, y });
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(e: TouchEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (state.longPressTimer) {
|
||||
clearTimeout(state.longPressTimer);
|
||||
state.longPressTimer = null;
|
||||
}
|
||||
|
||||
if (!state.hasMove && !state.isLongPress) {
|
||||
handleMouseEvent({ type: 'mousedown', button: MouseButton.Left });
|
||||
setTimeout(() => {
|
||||
handleMouseEvent({ type: 'mouseup', button: MouseButton.Left });
|
||||
}, 50);
|
||||
} else if (state.pressedButton !== null) {
|
||||
handleMouseEvent({ type: 'mouseup', button: state.pressedButton });
|
||||
}
|
||||
|
||||
resetTouchState();
|
||||
}
|
||||
|
||||
function handleTouchCancel(e: TouchEvent): void {
|
||||
disableEvent(e);
|
||||
|
||||
if (state.longPressTimer) {
|
||||
clearTimeout(state.longPressTimer);
|
||||
state.longPressTimer = null;
|
||||
}
|
||||
|
||||
if (state.pressedButton !== null) {
|
||||
handleMouseEvent({ type: 'mouseup', button: state.pressedButton });
|
||||
}
|
||||
|
||||
resetTouchState();
|
||||
}
|
||||
|
||||
function resetTouchState(): void {
|
||||
state.isLongPress = false;
|
||||
state.hasMove = false;
|
||||
state.isDragging = false;
|
||||
state.pressedButton = null;
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
if (state.longPressTimer) {
|
||||
clearTimeout(state.longPressTimer);
|
||||
state.longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
handleTouchCancel,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
33
browser/src/components/mouse/types.ts
Normal file
33
browser/src/components/mouse/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export enum MouseButton {
|
||||
Left = 0,
|
||||
Middle = 1,
|
||||
Right = 2,
|
||||
Back = 3,
|
||||
Forward = 4
|
||||
}
|
||||
|
||||
interface MouseMoveAbsoluteEvent {
|
||||
type: 'move';
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface MouseMoveRelativeEvent {
|
||||
type: 'move';
|
||||
deltaX: number;
|
||||
deltaY: number;
|
||||
}
|
||||
|
||||
interface MouseButtonEvent {
|
||||
type: 'mousedown' | 'mouseup';
|
||||
button: number;
|
||||
}
|
||||
|
||||
interface MouseWheelEvent {
|
||||
type: 'wheel';
|
||||
deltaY: number;
|
||||
}
|
||||
|
||||
export type MouseAbsoluteEvent = MouseMoveAbsoluteEvent | MouseButtonEvent | MouseWheelEvent;
|
||||
|
||||
export type MouseRelativeEvent = MouseMoveRelativeEvent | MouseButtonEvent | MouseWheelEvent;
|
||||
@@ -10,8 +10,7 @@ import '@/assets/keyboard.css';
|
||||
|
||||
import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
|
||||
import { device } from '@/libs/device';
|
||||
import { Modifiers } from '@/libs/device/keyboard.ts';
|
||||
import { KeyboardCodes } from '@/libs/keyboard';
|
||||
import { KeyboardReport } from '@/libs/keyboard/keyboard.ts';
|
||||
|
||||
import {
|
||||
doubleKeys,
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
keyboardControlPadOptions,
|
||||
keyboardOptions,
|
||||
modifierKeys,
|
||||
specialKeyMap
|
||||
specialKeys
|
||||
} from './keys.ts';
|
||||
|
||||
type KeyboardProps = {
|
||||
@@ -31,60 +30,56 @@ export const VirtualKeyboard = ({ isBigScreen }: KeyboardProps) => {
|
||||
|
||||
const [activeModifierKeys, setActiveModifierKeys] = useState<string[]>([]);
|
||||
|
||||
const keyboardRef = useRef<any>(null);
|
||||
const keyboardRef = useRef(new KeyboardReport());
|
||||
|
||||
async function onKeyPress(key: string) {
|
||||
if (modifierKeys.includes(key)) {
|
||||
if (activeModifierKeys.includes(key)) {
|
||||
await sendKeydown(key);
|
||||
await sendKeyup();
|
||||
} else {
|
||||
// Key down event
|
||||
async function onKeyPress(key: string): Promise<void> {
|
||||
if (modifierKeys[key]) {
|
||||
if (!activeModifierKeys.includes(key)) {
|
||||
// Save modifier key
|
||||
setActiveModifierKeys([...activeModifierKeys, key]);
|
||||
} else {
|
||||
// Press and release modifier keys
|
||||
for (const modifierKey of activeModifierKeys) {
|
||||
await handleKeyEvent({ type: 'keydown', key: modifierKey });
|
||||
}
|
||||
for (const modifierKey of activeModifierKeys) {
|
||||
await handleKeyEvent({ type: 'keyup', key: modifierKey });
|
||||
}
|
||||
setActiveModifierKeys([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await sendKeydown(key);
|
||||
for (const modifierKey of activeModifierKeys) {
|
||||
await handleKeyEvent({ type: 'keydown', key: modifierKey });
|
||||
}
|
||||
|
||||
await handleKeyEvent({ type: 'keydown', key });
|
||||
}
|
||||
|
||||
async function onKeyReleased(key: string) {
|
||||
if (modifierKeys.includes(key)) {
|
||||
// Key up event
|
||||
async function onKeyReleased(key: string): Promise<void> {
|
||||
// Skip modifier key
|
||||
if (modifierKeys[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendKeyup();
|
||||
}
|
||||
|
||||
async function sendKeydown(key: string) {
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
const code = KeyboardCodes.get(specialKey ? specialKey : key);
|
||||
if (!code) {
|
||||
console.log('unknown code: ', key);
|
||||
return;
|
||||
for (const modifierKey of activeModifierKeys) {
|
||||
await handleKeyEvent({ type: 'keyup', key: modifierKey });
|
||||
}
|
||||
|
||||
const modifiers = getModifiers();
|
||||
const keys = [code, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
await device.sendKeyboardData(modifiers, keys);
|
||||
}
|
||||
|
||||
async function sendKeyup() {
|
||||
const modifiers = new Modifiers();
|
||||
const keys = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
await device.sendKeyboardData(modifiers, keys);
|
||||
await handleKeyEvent({ type: 'keyup', key });
|
||||
|
||||
setActiveModifierKeys([]);
|
||||
}
|
||||
|
||||
function getModifiers() {
|
||||
const modifiers = new Modifiers();
|
||||
async function handleKeyEvent(event: { type: 'keydown' | 'keyup'; key: string }): Promise<void> {
|
||||
const code = specialKeys[event.key] ?? event.key;
|
||||
|
||||
activeModifierKeys.forEach((modifierKey) => {
|
||||
const code = specialKeyMap.get(modifierKey)!;
|
||||
modifiers.setModifier(code);
|
||||
});
|
||||
const kb = keyboardRef.current;
|
||||
const report = event.type === 'keydown' ? kb.keyDown(code) : kb.keyUp(code);
|
||||
|
||||
return modifiers;
|
||||
await device.sendKeyboardData(report);
|
||||
}
|
||||
|
||||
function getButtonTheme(): KeyboardButtonTheme[] {
|
||||
@@ -123,7 +118,6 @@ export const VirtualKeyboard = ({ isBigScreen }: KeyboardProps) => {
|
||||
{/* main keyboard */}
|
||||
<Keyboard
|
||||
buttonTheme={getButtonTheme()}
|
||||
keyboardRef={(r: any) => (keyboardRef.current = r)}
|
||||
onKeyPress={onKeyPress}
|
||||
onKeyReleased={onKeyReleased}
|
||||
layoutName="default"
|
||||
|
||||
@@ -127,51 +127,51 @@ export const keyboardArrowsOptions = {
|
||||
};
|
||||
|
||||
// keys require special mapping
|
||||
export const specialKeyMap = new Map([
|
||||
['{escape}', 'Escape'],
|
||||
['{backspace}', 'Backspace'],
|
||||
['{tab}', 'Tab'],
|
||||
['{capslock}', 'CapsLock'],
|
||||
['{enter}', 'Enter'],
|
||||
['{shiftleft}', 'ShiftLeft'],
|
||||
['{shiftright}', 'ShiftRight'],
|
||||
['{controlleft}', 'ControlLeft'],
|
||||
['{controlright}', 'ControlRight'],
|
||||
['{altleft}', 'AltLeft'],
|
||||
['{metaleft}', 'MetaLeft'],
|
||||
['{winleft}', 'WinLeft'],
|
||||
['{space}', 'Space'],
|
||||
['{metaright}', 'MetaRight'],
|
||||
['{winright}', 'WinRight'],
|
||||
['{altright}', 'AltRight'],
|
||||
['{prtscr}', 'PrintScreen'],
|
||||
['{scrolllock}', 'ScrollLock'],
|
||||
['{pause}', 'Pause'],
|
||||
['{insert}', 'Insert'],
|
||||
['{home}', 'Home'],
|
||||
['{pageup}', 'PageUp'],
|
||||
['{delete}', 'Delete'],
|
||||
['{end}', 'End'],
|
||||
['{pagedown}', 'PageDown'],
|
||||
['{arrowright}', 'ArrowRight'],
|
||||
['{arrowleft}', 'ArrowLeft'],
|
||||
['{arrowdown}', 'ArrowDown'],
|
||||
['{arrowup}', 'ArrowUp']
|
||||
]);
|
||||
export const specialKeys: Record<string, string> = {
|
||||
'{escape}': 'Escape',
|
||||
'{backspace}': 'Backspace',
|
||||
'{tab}': 'Tab',
|
||||
'{capslock}': 'CapsLock',
|
||||
'{enter}': 'Enter',
|
||||
'{shiftleft}': 'ShiftLeft',
|
||||
'{shiftright}': 'ShiftRight',
|
||||
'{controlleft}': 'ControlLeft',
|
||||
'{controlright}': 'ControlRight',
|
||||
'{altleft}': 'AltLeft',
|
||||
'{metaleft}': 'MetaLeft',
|
||||
'{winleft}': 'WinLeft',
|
||||
'{space}': 'Space',
|
||||
'{metaright}': 'MetaRight',
|
||||
'{winright}': 'WinRight',
|
||||
'{altright}': 'AltRight',
|
||||
'{prtscr}': 'PrintScreen',
|
||||
'{scrolllock}': 'ScrollLock',
|
||||
'{pause}': 'Pause',
|
||||
'{insert}': 'Insert',
|
||||
'{home}': 'Home',
|
||||
'{pageup}': 'PageUp',
|
||||
'{delete}': 'Delete',
|
||||
'{end}': 'End',
|
||||
'{pagedown}': 'PageDown',
|
||||
'{arrowright}': 'ArrowRight',
|
||||
'{arrowleft}': 'ArrowLeft',
|
||||
'{arrowdown}': 'ArrowDown',
|
||||
'{arrowup}': 'ArrowUp'
|
||||
};
|
||||
|
||||
// modifier keys
|
||||
export const modifierKeys = [
|
||||
'{shiftleft}',
|
||||
'{controlleft}',
|
||||
'{altleft}',
|
||||
'{metaleft}',
|
||||
'{winleft}',
|
||||
'{shiftright}',
|
||||
'{controlright}',
|
||||
'{altright}',
|
||||
'{metaright}',
|
||||
'{winright}'
|
||||
];
|
||||
export const modifierKeys: Record<string, string> = {
|
||||
'{shiftleft}': 'ShiftLeft',
|
||||
'{controlleft}': 'ControlLeft',
|
||||
'{altleft}': 'AltLeft',
|
||||
'{metaleft}': 'MetaLeft',
|
||||
'{winleft}': 'WinLeft',
|
||||
'{shiftright}': 'ShiftRight',
|
||||
'{controlright}': 'ControlRight',
|
||||
'{altright}': 'AltRight',
|
||||
'{metaright}': 'MetaRight',
|
||||
'{winright}': 'WinRight'
|
||||
};
|
||||
|
||||
// double line display buttons
|
||||
export const doubleKeys = [
|
||||
|
||||
@@ -1,72 +1,73 @@
|
||||
const be = {
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriële poort wordt niet ondersteund. Gebruik de desktop Chrome-browser om muis en klavier te gebruiken.',
|
||||
failed: 'Verbinding met seriële poort mislukt. Probeer opnieuw'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Wachten op toelating...',
|
||||
denied: 'Toelating geweigerd',
|
||||
authorize:
|
||||
'De externe desktop vereist cameratoegang. Geef toelating voor de camera in de browserinstellingen.',
|
||||
failed: 'Kan geen verbinding maken met de camera. Probeer opnieuw.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Kies USB-apparaat',
|
||||
selectVideo: 'Kies een video-invoerapparaat',
|
||||
selectSerial: 'Kies serieel apparaat'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serieel',
|
||||
keyboard: 'Klavier',
|
||||
mouse: 'Muis'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Toestel',
|
||||
custom: {
|
||||
title: 'Aangepaste resolutie',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
confirm: 'OK',
|
||||
cancel: 'Annuleren'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Plakken',
|
||||
virtualKeyboard: 'Virtueel klavier',
|
||||
shortcut: {
|
||||
title: 'Sneltoetsen',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Muisaanwijzer',
|
||||
pointer: 'Wijzer',
|
||||
grab: 'Hand',
|
||||
cell: 'Kruis',
|
||||
hide: 'Verborgen'
|
||||
},
|
||||
mode: 'Muismodus',
|
||||
absolute: 'Absolute modus',
|
||||
relative: 'Relatieve modus',
|
||||
direction: 'Scrollrichting',
|
||||
scrollUp: 'Omhoog scrollen',
|
||||
scrollDown: 'Omlaag scrollen',
|
||||
requestPointer: 'Gebruik relatieve modus. Klik op het bureaublad om de muisaanwijzer vast te nemen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Taal',
|
||||
document: 'Document',
|
||||
download: 'Download'
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriële poort wordt niet ondersteund. Gebruik de desktop Chrome-browser om muis en klavier te gebruiken.',
|
||||
failed: 'Verbinding met seriële poort mislukt. Probeer opnieuw'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Wachten op toelating...',
|
||||
denied: 'Toelating geweigerd',
|
||||
authorize:
|
||||
'De externe desktop vereist cameratoegang. Geef toelating voor de camera in de browserinstellingen.',
|
||||
failed: 'Kan geen verbinding maken met de camera. Probeer opnieuw.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Kies USB-apparaat',
|
||||
selectVideo: 'Kies een video-invoerapparaat',
|
||||
selectSerial: 'Kies serieel apparaat'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serieel',
|
||||
keyboard: 'Klavier',
|
||||
mouse: 'Muis'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Toestel',
|
||||
custom: {
|
||||
title: 'Aangepaste resolutie',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
confirm: 'OK',
|
||||
cancel: 'Annuleren'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Plakken',
|
||||
virtualKeyboard: 'Virtueel klavier',
|
||||
shortcut: {
|
||||
title: 'Sneltoetsen',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Muisaanwijzer',
|
||||
pointer: 'Wijzer',
|
||||
grab: 'Hand',
|
||||
cell: 'Kruis',
|
||||
hide: 'Verborgen'
|
||||
},
|
||||
mode: 'Muismodus',
|
||||
absolute: 'Absolute modus',
|
||||
relative: 'Relatieve modus',
|
||||
direction: 'Scrollrichting',
|
||||
scrollUp: 'Omhoog scrollen',
|
||||
scrollDown: 'Omlaag scrollen',
|
||||
requestPointer:
|
||||
'Gebruik relatieve modus. Klik op het bureaublad om de muisaanwijzer vast te nemen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Taal',
|
||||
document: 'Document',
|
||||
download: 'Download'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default be;
|
||||
export default be;
|
||||
|
||||
@@ -1,75 +1,76 @@
|
||||
const de = {
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriell nicht unterstützt. Bitte benutze den Desktop Chrome Browser um Maus und Tastatur zu verwenden.',
|
||||
failed: 'Verbindung zu Seriell fehlgeschlagen. Bitte erneut versuchen'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Warten auf Berechtigung...',
|
||||
denied: 'Berechtigung verweigert',
|
||||
authorize:
|
||||
'Der Remote-Desktop erfordert eine Kamerazulassung. Bitte autorisieren Sie die Kamera in den Browsereinstellungen.',
|
||||
failed: 'Kamera konnte nicht verbunden werden. Bitte versuche es erneut.'
|
||||
},
|
||||
modal: {
|
||||
title: 'USB-Gerät auswählen',
|
||||
selectVideo: 'Bitte wähle ein Video-Eingabegerät aus',
|
||||
selectSerial: 'Serielles Gerät auswählen'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Seriell',
|
||||
keyboard: 'Tastatur',
|
||||
mouse: 'Maus'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Auflösung',
|
||||
scale: 'Skalierung',
|
||||
customResolution: 'Benutzerdefiniert',
|
||||
device: 'Gerät',
|
||||
custom: {
|
||||
title: 'Benutzerdefinierte Aufösung',
|
||||
width: 'Breite',
|
||||
height: 'Höhe',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Abbrechen'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Einfügen',
|
||||
virtualKeyboard: 'Virtuelle Tastatur',
|
||||
shortcut: {
|
||||
title: 'Tastenkürzel',
|
||||
ctrlAltDel: 'Strg + Alt + Entfernen',
|
||||
ctrlD: 'Strg + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Mauszeiger',
|
||||
pointer: 'Zeiger',
|
||||
grab: 'Hand',
|
||||
cell: 'Kreuz',
|
||||
hide: 'Versteckt'
|
||||
},
|
||||
mode: 'Mausmodus',
|
||||
absolute: 'Absoluter Modus',
|
||||
relative: 'Relativer Mode',
|
||||
direction: 'Scrollrichtung',
|
||||
scrollUp: 'Hochscrollen',
|
||||
scrollDown: 'Runterscrollen',
|
||||
speed: 'Scrollgeschwindigkeit',
|
||||
fast: 'Schnell',
|
||||
slow: 'Langsam',
|
||||
requestPointer: 'Benutze relativen Modus. Bitte auf den Desktop klicken, um den Mauszeiger anzuzeigen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Sprache',
|
||||
document: 'Dokument',
|
||||
download: 'Download'
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriell nicht unterstützt. Bitte benutze den Desktop Chrome Browser um Maus und Tastatur zu verwenden.',
|
||||
failed: 'Verbindung zu Seriell fehlgeschlagen. Bitte erneut versuchen'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Warten auf Berechtigung...',
|
||||
denied: 'Berechtigung verweigert',
|
||||
authorize:
|
||||
'Der Remote-Desktop erfordert eine Kamerazulassung. Bitte autorisieren Sie die Kamera in den Browsereinstellungen.',
|
||||
failed: 'Kamera konnte nicht verbunden werden. Bitte versuche es erneut.'
|
||||
},
|
||||
modal: {
|
||||
title: 'USB-Gerät auswählen',
|
||||
selectVideo: 'Bitte wähle ein Video-Eingabegerät aus',
|
||||
selectSerial: 'Serielles Gerät auswählen'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Seriell',
|
||||
keyboard: 'Tastatur',
|
||||
mouse: 'Maus'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Auflösung',
|
||||
scale: 'Skalierung',
|
||||
customResolution: 'Benutzerdefiniert',
|
||||
device: 'Gerät',
|
||||
custom: {
|
||||
title: 'Benutzerdefinierte Aufösung',
|
||||
width: 'Breite',
|
||||
height: 'Höhe',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Abbrechen'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Einfügen',
|
||||
virtualKeyboard: 'Virtuelle Tastatur',
|
||||
shortcut: {
|
||||
title: 'Tastenkürzel',
|
||||
ctrlAltDel: 'Strg + Alt + Entfernen',
|
||||
ctrlD: 'Strg + D',
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Mauszeiger',
|
||||
pointer: 'Zeiger',
|
||||
grab: 'Hand',
|
||||
cell: 'Kreuz',
|
||||
hide: 'Versteckt'
|
||||
},
|
||||
mode: 'Mausmodus',
|
||||
absolute: 'Absoluter Modus',
|
||||
relative: 'Relativer Mode',
|
||||
direction: 'Scrollrichtung',
|
||||
scrollUp: 'Hochscrollen',
|
||||
scrollDown: 'Runterscrollen',
|
||||
speed: 'Scrollgeschwindigkeit',
|
||||
fast: 'Schnell',
|
||||
slow: 'Langsam',
|
||||
requestPointer:
|
||||
'Benutze relativen Modus. Bitte auf den Desktop klicken, um den Mauszeiger anzuzeigen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Sprache',
|
||||
document: 'Dokument',
|
||||
download: 'Download'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default de;
|
||||
export default de;
|
||||
|
||||
@@ -41,8 +41,8 @@ const ko = {
|
||||
shortcut: {
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
|
||||
@@ -1,72 +1,73 @@
|
||||
const nl = {
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriële poort wordt niet ondersteund. Gebruik de desktop Chrome-browser om muis en toetsenbord te gebruiken.',
|
||||
failed: 'Verbinding met seriële poort mislukt. Probeer het opnieuw'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Wachten op toestemming...',
|
||||
denied: 'Toestemming geweigerd',
|
||||
authorize:
|
||||
'De externe desktop vereist cameratoegang. Geef toestemming voor de camera in de browserinstellingen.',
|
||||
failed: 'Kan geen verbinding maken met de camera. Probeer het opnieuw.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Selecteer USB-apparaat',
|
||||
selectVideo: 'Selecteer een video-invoerapparaat',
|
||||
selectSerial: 'Selecteer serieel apparaat'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serieel',
|
||||
keyboard: 'Toetsenbord',
|
||||
mouse: 'Muis'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Apparaat',
|
||||
custom: {
|
||||
title: 'Aangepaste resolutie',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
confirm: 'OK',
|
||||
cancel: 'Annuleren'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Plakken',
|
||||
virtualKeyboard: 'Virtueel toetsenbord',
|
||||
shortcut: {
|
||||
title: 'Sneltoetsen',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Muiscursor',
|
||||
pointer: 'Aanwijzer',
|
||||
grab: 'Hand',
|
||||
cell: 'Kruis',
|
||||
hide: 'Verborgen'
|
||||
},
|
||||
mode: 'Muismodus',
|
||||
absolute: 'Absolute modus',
|
||||
relative: 'Relatieve modus',
|
||||
direction: 'Scrollrichting',
|
||||
scrollUp: 'Omhoog scrollen',
|
||||
scrollDown: 'Omlaag scrollen',
|
||||
requestPointer: 'Gebruik relatieve modus. Klik op het bureaublad om de muisaanwijzer vast te leggen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Taal',
|
||||
document: 'Document',
|
||||
download: 'Download'
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Seriële poort wordt niet ondersteund. Gebruik de desktop Chrome-browser om muis en toetsenbord te gebruiken.',
|
||||
failed: 'Verbinding met seriële poort mislukt. Probeer het opnieuw'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Wachten op toestemming...',
|
||||
denied: 'Toestemming geweigerd',
|
||||
authorize:
|
||||
'De externe desktop vereist cameratoegang. Geef toestemming voor de camera in de browserinstellingen.',
|
||||
failed: 'Kan geen verbinding maken met de camera. Probeer het opnieuw.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Selecteer USB-apparaat',
|
||||
selectVideo: 'Selecteer een video-invoerapparaat',
|
||||
selectSerial: 'Selecteer serieel apparaat'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serieel',
|
||||
keyboard: 'Toetsenbord',
|
||||
mouse: 'Muis'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Apparaat',
|
||||
custom: {
|
||||
title: 'Aangepaste resolutie',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
confirm: 'OK',
|
||||
cancel: 'Annuleren'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Plakken',
|
||||
virtualKeyboard: 'Virtueel toetsenbord',
|
||||
shortcut: {
|
||||
title: 'Sneltoetsen',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Muiscursor',
|
||||
pointer: 'Aanwijzer',
|
||||
grab: 'Hand',
|
||||
cell: 'Kruis',
|
||||
hide: 'Verborgen'
|
||||
},
|
||||
mode: 'Muismodus',
|
||||
absolute: 'Absolute modus',
|
||||
relative: 'Relatieve modus',
|
||||
direction: 'Scrollrichting',
|
||||
scrollUp: 'Omhoog scrollen',
|
||||
scrollDown: 'Omlaag scrollen',
|
||||
requestPointer:
|
||||
'Gebruik relatieve modus. Klik op het bureaublad om de muisaanwijzer vast te leggen.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Taal',
|
||||
document: 'Document',
|
||||
download: 'Download'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default nl;
|
||||
export default nl;
|
||||
|
||||
@@ -40,8 +40,8 @@ const pl = {
|
||||
shortcut: {
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
|
||||
@@ -40,8 +40,8 @@ const pt_br = {
|
||||
shortcut: {
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
@@ -60,7 +60,8 @@ const pt_br = {
|
||||
speed: 'Velocidade do scroll',
|
||||
fast: 'Rápido',
|
||||
slow: 'Lento',
|
||||
requestPointer: 'Usando modo relativo. Por favor, clique na área de trabalho para restaurar o ponteiro do mouse.'
|
||||
requestPointer:
|
||||
'Usando modo relativo. Por favor, clique na área de trabalho para restaurar o ponteiro do mouse.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Linguagem',
|
||||
|
||||
@@ -3,7 +3,8 @@ const ru = {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Подключение к последовательному порту не поддерживается. Чтобы пользоваться мышью и клавиатурой, используйте браузер Chrome для настольных компьютеров.',
|
||||
failed: 'Не удалось установить подключение к последовательному порту. Пожалуйста, попробуйте еще раз.'
|
||||
failed:
|
||||
'Не удалось установить подключение к последовательному порту. Пожалуйста, попробуйте еще раз.'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Ожидание доступа...',
|
||||
@@ -42,8 +43,8 @@ const ru = {
|
||||
title: 'Сочетания клавиш',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
@@ -62,7 +63,8 @@ const ru = {
|
||||
speed: 'Скорость прокрутки',
|
||||
fast: 'Быстро',
|
||||
slow: 'Медленно',
|
||||
requestPointer: 'Используется относительное позиционирование мыши. Чтобы захватить курсор, щелкните по видео на экране'
|
||||
requestPointer:
|
||||
'Используется относительное позиционирование мыши. Чтобы захватить курсор, щелкните по видео на экране'
|
||||
},
|
||||
settings: {
|
||||
language: 'Язык приложения',
|
||||
|
||||
@@ -38,8 +38,8 @@ const zh_tw = {
|
||||
shortcut: {
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete',
|
||||
ctrlD: 'Ctrl + D',
|
||||
winTab: 'Win + Tab',
|
||||
},
|
||||
winTab: 'Win + Tab'
|
||||
}
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const mouseStyleAtom = atom('cursor-default');
|
||||
export const mouseModeAtom = atom('absolute');
|
||||
|
||||
// mouse scroll direction: 1 or -1
|
||||
export const scrollDirectionAtom = atom(1);
|
||||
export const scrollDirectionAtom = atom(-1);
|
||||
|
||||
// mouse scroll interval (unit: ms)
|
||||
// mouse scroll interval (unit: ms)
|
||||
|
||||
56
browser/src/libs/browser/index.ts
Normal file
56
browser/src/libs/browser/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface BrowserInfo {
|
||||
os: string;
|
||||
isChrome: boolean;
|
||||
}
|
||||
|
||||
export function getOperatingSystem(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const platformSource =
|
||||
// @ts-expect-error check userAgentData.platform
|
||||
'userAgentData' in navigator ? navigator.userAgentData.platform : navigator.platform;
|
||||
|
||||
if (!platformSource) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const platform = platformSource.toLowerCase();
|
||||
|
||||
if (platform.startsWith('win')) return 'Windows';
|
||||
if (platform.startsWith('mac')) return 'macOS';
|
||||
if (platform.startsWith('linux')) return 'Linux';
|
||||
|
||||
return platformSource;
|
||||
}
|
||||
|
||||
export function isChromeBrowser(): boolean {
|
||||
if (typeof window === 'undefined' || !window.navigator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('userAgentData' in navigator) {
|
||||
// @ts-expect-error userAgentData.brands
|
||||
return navigator.userAgentData?.brands?.some(
|
||||
(brand: any) => brand.brand === 'Google Chrome' || brand.brand === 'Chromium'
|
||||
);
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
const vendor = navigator.vendor;
|
||||
|
||||
return (
|
||||
/Chrome|Chromium/.test(userAgent) &&
|
||||
/Google Inc/.test(vendor) &&
|
||||
!/Edg/.test(userAgent) &&
|
||||
!/OPR/.test(userAgent)
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrowserInfo(): BrowserInfo {
|
||||
return {
|
||||
os: getOperatingSystem(),
|
||||
isChrome: isChromeBrowser()
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Modifiers } from './keyboard.ts';
|
||||
import { Key as MouseKey, Mode as MouseMode } from './mouse.ts';
|
||||
import { CmdEvent, CmdPacket, InfoPacket } from './proto.ts';
|
||||
import { SerialPort } from './serial-port.ts';
|
||||
import { intToByte, intToLittleEndianList } from './utils.ts';
|
||||
|
||||
export class Device {
|
||||
addr: number;
|
||||
@@ -22,45 +19,16 @@ export class Device {
|
||||
return new InfoPacket(rspPacket.DATA);
|
||||
}
|
||||
|
||||
async sendKeyboardData(modifiers: Modifiers, keys: number[]) {
|
||||
if (keys.length !== 6) {
|
||||
throw new Error('keyboard keys length must be 6');
|
||||
}
|
||||
|
||||
const data = [modifiers.encode(), 0x00, ...keys];
|
||||
const cmdData = new CmdPacket(this.addr, CmdEvent.SEND_KB_GENERAL_DATA, data).encode();
|
||||
|
||||
async sendKeyboardData(report: number[]): Promise<void> {
|
||||
const cmdData = new CmdPacket(this.addr, CmdEvent.SEND_KB_GENERAL_DATA, report).encode();
|
||||
await this.serialPort.write(cmdData);
|
||||
}
|
||||
|
||||
async sendMouseAbsoluteData(
|
||||
key: MouseKey,
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
scroll: number
|
||||
) {
|
||||
const xAbs = width === 0 ? 0 : Math.floor((x * 4096) / width);
|
||||
const yAbs = width === 0 ? 0 : Math.floor((y * 4096) / height);
|
||||
async sendMouseData(report: number[]): Promise<void> {
|
||||
if (report.length === 0) return;
|
||||
|
||||
const data = [
|
||||
MouseMode.ABSOLUTE,
|
||||
key.encode(),
|
||||
...intToLittleEndianList(xAbs),
|
||||
...intToLittleEndianList(yAbs),
|
||||
scroll
|
||||
];
|
||||
const cmdData = new CmdPacket(this.addr, CmdEvent.SEND_MS_ABS_DATA, data).encode();
|
||||
await this.serialPort.write(cmdData);
|
||||
}
|
||||
|
||||
async sendMouseRelativeData(msKey: MouseKey, x: number, y: number, scroll: number) {
|
||||
const xByte = intToByte(x);
|
||||
const yByte = intToByte(y);
|
||||
|
||||
const data = [MouseMode.RELATIVE, msKey.encode(), xByte, yByte, scroll];
|
||||
const cmdData = new CmdPacket(this.addr, CmdEvent.SEND_MS_REL_DATA, data).encode();
|
||||
const cmdEvent = report[0] === 0x01 ? CmdEvent.SEND_MS_REL_DATA : CmdEvent.SEND_MS_ABS_DATA;
|
||||
const cmdData = new CmdPacket(this.addr, cmdEvent, report).encode();
|
||||
await this.serialPort.write(cmdData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { setBit } from './utils.ts';
|
||||
|
||||
const modifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']);
|
||||
|
||||
class Modifiers {
|
||||
public rightWindows: boolean = false;
|
||||
public rightAlt: boolean = false;
|
||||
public rightShift: boolean = false;
|
||||
public rightCtrl: boolean = false;
|
||||
public leftWindows: boolean = false;
|
||||
public leftAlt: boolean = false;
|
||||
public leftShift: boolean = false;
|
||||
public leftCtrl: boolean = false;
|
||||
|
||||
public encode(): number {
|
||||
let b = 0x00;
|
||||
b = setBit(b, 0, this.leftCtrl);
|
||||
b = setBit(b, 1, this.leftShift);
|
||||
b = setBit(b, 2, this.leftAlt);
|
||||
b = setBit(b, 3, this.leftWindows);
|
||||
b = setBit(b, 4, this.rightCtrl);
|
||||
b = setBit(b, 5, this.rightShift);
|
||||
b = setBit(b, 6, this.rightAlt);
|
||||
b = setBit(b, 7, this.rightWindows);
|
||||
return b;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Modifiers, modifierKeys };
|
||||
@@ -1,26 +0,0 @@
|
||||
import { setBit } from './utils.ts';
|
||||
|
||||
export enum Mode {
|
||||
RELATIVE = 0x01,
|
||||
ABSOLUTE = 0x02
|
||||
}
|
||||
|
||||
export class Key {
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
mid: boolean;
|
||||
|
||||
constructor(left: boolean = false, right: boolean = false, mid: boolean = false) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.mid = mid;
|
||||
}
|
||||
|
||||
public encode(): number {
|
||||
let b = 0x00;
|
||||
b = setBit(b, 0, this.left);
|
||||
b = setBit(b, 1, this.right);
|
||||
b = setBit(b, 2, this.mid);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getBit } from './utils.ts';
|
||||
|
||||
export enum CmdEvent {
|
||||
GET_INFO = 0x01,
|
||||
SEND_KB_GENERAL_DATA = 0x02,
|
||||
@@ -132,3 +130,7 @@ export class InfoPacket {
|
||||
this.SCROLL_LOCK = getBit(data[2], 2) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getBit(number: number, bitPosition: number): number {
|
||||
return (number >> bitPosition) & 1;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class SerialPort {
|
||||
}
|
||||
|
||||
this.instance = port;
|
||||
await this.instance.open({baudRate});
|
||||
await this.instance.open({ baudRate });
|
||||
|
||||
this.reader = this.instance.readable!.getReader();
|
||||
this.writer = this.instance.writable!.getWriter();
|
||||
@@ -48,7 +48,7 @@ export class SerialPort {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {value, done} = await this.reader.read();
|
||||
const { value, done } = await this.reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export function getBit(number: number, bitPosition: number): number {
|
||||
return (number >> bitPosition) & 1;
|
||||
}
|
||||
|
||||
export function setBit(number: number, bitPosition: number, value: boolean): number {
|
||||
if (value) {
|
||||
return number | (1 << bitPosition);
|
||||
} else {
|
||||
return number & ~(1 << bitPosition);
|
||||
}
|
||||
}
|
||||
|
||||
export function intToByte(value: number): number {
|
||||
if (value < -128 || value > 127) {
|
||||
throw new Error('value must be in range -128 to 127 for a signed byte');
|
||||
}
|
||||
return (value + 256) % 256;
|
||||
}
|
||||
|
||||
export function intToLittleEndianList(number: number): number[] {
|
||||
const byteList: number[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
byteList.push((number >> (i * 8)) & 0xff);
|
||||
}
|
||||
return byteList;
|
||||
}
|
||||
@@ -1,107 +1,127 @@
|
||||
export const CharCodes: Map<number, number> = new Map([
|
||||
[48, 0x27], // 0
|
||||
[49, 0x1e], // 1
|
||||
[50, 0x1f], // 2
|
||||
[51, 0x20], // 3
|
||||
[52, 0x21], // 4
|
||||
[53, 0x22], // 5
|
||||
[54, 0x23], // 6
|
||||
[55, 0x24], // 7
|
||||
[56, 0x25], // 8
|
||||
[57, 0x26], // 9
|
||||
export const CharCodes: Record<number, number> = {
|
||||
48: 0x27, // 0
|
||||
49: 0x1e, // 1
|
||||
50: 0x1f, // 2
|
||||
51: 0x20, // 3
|
||||
52: 0x21, // 4
|
||||
53: 0x22, // 5
|
||||
54: 0x23, // 6
|
||||
55: 0x24, // 7
|
||||
56: 0x25, // 8
|
||||
57: 0x26, // 9
|
||||
|
||||
[65, 0x04], // A
|
||||
[66, 0x05], // B
|
||||
[67, 0x06], // C
|
||||
[68, 0x07], // D
|
||||
[69, 0x08], // E
|
||||
[70, 0x09], // F
|
||||
[71, 0x0a], // G
|
||||
[72, 0x0b], // H
|
||||
[73, 0x0c], // I
|
||||
[74, 0x0d], // J
|
||||
[75, 0x0e], // K
|
||||
[76, 0x0f], // L
|
||||
[77, 0x10], // M
|
||||
[78, 0x11], // N
|
||||
[79, 0x12], // O
|
||||
[80, 0x13], // P
|
||||
[81, 0x14], // Q
|
||||
[82, 0x15], // R
|
||||
[83, 0x16], // S
|
||||
[84, 0x17], // T
|
||||
[85, 0x18], // U
|
||||
[86, 0x19], // V
|
||||
[87, 0x1a], // W
|
||||
[88, 0x1b], // X
|
||||
[89, 0x1c], // Y
|
||||
[90, 0x1d], // Z
|
||||
65: 0x04, // A
|
||||
66: 0x05, // B
|
||||
67: 0x06, // C
|
||||
68: 0x07, // D
|
||||
69: 0x08, // E
|
||||
70: 0x09, // F
|
||||
71: 0x0a, // G
|
||||
72: 0x0b, // H
|
||||
73: 0x0c, // I
|
||||
74: 0x0d, // J
|
||||
75: 0x0e, // K
|
||||
76: 0x0f, // L
|
||||
77: 0x10, // M
|
||||
78: 0x11, // N
|
||||
79: 0x12, // O
|
||||
80: 0x13, // P
|
||||
81: 0x14, // Q
|
||||
82: 0x15, // R
|
||||
83: 0x16, // S
|
||||
84: 0x17, // T
|
||||
85: 0x18, // U
|
||||
86: 0x19, // V
|
||||
87: 0x1a, // W
|
||||
88: 0x1b, // X
|
||||
89: 0x1c, // Y
|
||||
90: 0x1d, // Z
|
||||
|
||||
[97, 0x04], // a
|
||||
[98, 0x05], // b
|
||||
[99, 0x06], // c
|
||||
[100, 0x07], // d
|
||||
[101, 0x08], // e
|
||||
[102, 0x09], // f
|
||||
[103, 0x0a], // g
|
||||
[104, 0x0b], // h
|
||||
[105, 0x0c], // i
|
||||
[106, 0x0d], // j
|
||||
[107, 0x0e], // k
|
||||
[108, 0x0f], // l
|
||||
[109, 0x10], // m
|
||||
[110, 0x11], // n
|
||||
[111, 0x12], // o
|
||||
[112, 0x13], // p
|
||||
[113, 0x14], // q
|
||||
[114, 0x15], // r
|
||||
[115, 0x16], // s
|
||||
[116, 0x17], // t
|
||||
[117, 0x18], // u
|
||||
[118, 0x19], // v
|
||||
[119, 0x1a], // w
|
||||
[120, 0x1b], // x
|
||||
[121, 0x1c], // y
|
||||
[122, 0x1d], // z
|
||||
97: 0x04, // a
|
||||
98: 0x05, // b
|
||||
99: 0x06, // c
|
||||
100: 0x07, // d
|
||||
101: 0x08, // e
|
||||
102: 0x09, // f
|
||||
103: 0x0a, // g
|
||||
104: 0x0b, // h
|
||||
105: 0x0c, // i
|
||||
106: 0x0d, // j
|
||||
107: 0x0e, // k
|
||||
108: 0x0f, // l
|
||||
109: 0x10, // m
|
||||
110: 0x11, // n
|
||||
111: 0x12, // o
|
||||
112: 0x13, // p
|
||||
113: 0x14, // q
|
||||
114: 0x15, // r
|
||||
115: 0x16, // s
|
||||
116: 0x17, // t
|
||||
117: 0x18, // u
|
||||
118: 0x19, // v
|
||||
119: 0x1a, // w
|
||||
120: 0x1b, // x
|
||||
121: 0x1c, // y
|
||||
122: 0x1d, // z
|
||||
|
||||
[32, 0x2c], // Space
|
||||
[33, 0x1e], // !
|
||||
[34, 0x34], // "
|
||||
[35, 0x20], // #
|
||||
[36, 0x21], // $
|
||||
[37, 0x22], // %
|
||||
[38, 0x24], // &
|
||||
[39, 0x34], // '
|
||||
[40, 0x26], // (
|
||||
[41, 0x27], // )
|
||||
[42, 0x25], // *
|
||||
[43, 0x2e], // +
|
||||
[44, 0x36], // ,
|
||||
[45, 0x2d], // -
|
||||
[46, 0x37], // .
|
||||
[47, 0x38], // /
|
||||
32: 0x2c, // Space
|
||||
33: 0x1e, // !
|
||||
34: 0x34, // "
|
||||
35: 0x20, // #
|
||||
36: 0x21, // $
|
||||
37: 0x22, // %
|
||||
38: 0x24, // &
|
||||
39: 0x34, // '
|
||||
40: 0x26, // (
|
||||
41: 0x27, // )
|
||||
42: 0x25, // *
|
||||
43: 0x2e, // +
|
||||
44: 0x36, // ,
|
||||
45: 0x2d, // -
|
||||
46: 0x37, // .
|
||||
47: 0x38, // /
|
||||
|
||||
[9, 43], // Tab
|
||||
[10, 40], // Enter
|
||||
[58, 51], // :
|
||||
[59, 51], // ;
|
||||
[60, 54], // <
|
||||
[61, 46], // =
|
||||
[62, 55], // >
|
||||
[63, 56], // ?
|
||||
[64, 31], // @
|
||||
[91, 47], // [
|
||||
[92, 49], // \
|
||||
[93, 48], // ]
|
||||
[94, 35], // ^
|
||||
[95, 45], // _
|
||||
[96, 53], // `
|
||||
[123, 47], // {
|
||||
[124, 49], // |
|
||||
[125, 48], // }
|
||||
[126, 53] // ~
|
||||
]);
|
||||
9: 43, // Tab
|
||||
10: 40, // Enter
|
||||
58: 51, // :
|
||||
59: 51, // ;
|
||||
60: 54, // <
|
||||
61: 46, // =
|
||||
62: 55, // >
|
||||
63: 56, // ?
|
||||
64: 31, // @
|
||||
91: 47, // [
|
||||
92: 49, // \
|
||||
93: 48, // ]
|
||||
94: 35, // ^
|
||||
95: 45, // _
|
||||
96: 53, // `
|
||||
123: 47, // {
|
||||
124: 49, // |
|
||||
125: 48, // }
|
||||
126: 53 // ~
|
||||
};
|
||||
|
||||
export const ShiftChars: Set<number> = new Set([
|
||||
33, 64, 35, 36, 37, 94, 38, 42, 40, 41, 95, 43, 123, 124, 125, 58, 34, 126, 60, 62, 63
|
||||
]);
|
||||
export const ShiftChars: Record<number, boolean> = {
|
||||
33: true, // !
|
||||
64: true, // @
|
||||
35: true, // #
|
||||
36: true, // $
|
||||
37: true, // %
|
||||
94: true, // ^
|
||||
38: true, // &
|
||||
42: true, // *
|
||||
40: true, // (
|
||||
41: true, // )
|
||||
95: true, // _
|
||||
43: true, // +
|
||||
123: true, // {
|
||||
124: true, // |
|
||||
125: true, // }
|
||||
58: true, // :
|
||||
34: true, // "
|
||||
126: true, // ~
|
||||
60: true, // <
|
||||
62: true, // >
|
||||
63: true // ?
|
||||
};
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './keyboardCodes.ts';
|
||||
export * from './charCodes.ts';
|
||||
63
browser/src/libs/keyboard/keyboard.ts
Normal file
63
browser/src/libs/keyboard/keyboard.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { getKeycode, getModifierBit, isModifier } from './keymap';
|
||||
|
||||
const MAX_KEYS = 6;
|
||||
|
||||
export class KeyboardReport {
|
||||
private modifier: number = 0;
|
||||
private pressedKeys: Map<string, number> = new Map();
|
||||
|
||||
keyDown(code: string): number[] {
|
||||
if (isModifier(code)) {
|
||||
this.modifier |= getModifierBit(code);
|
||||
} else {
|
||||
const keycode = getKeycode(code);
|
||||
if (keycode !== undefined && this.pressedKeys.size < MAX_KEYS) {
|
||||
this.pressedKeys.set(code, keycode);
|
||||
}
|
||||
}
|
||||
return this.buildReport();
|
||||
}
|
||||
|
||||
keyUp(code: string): number[] {
|
||||
if (isModifier(code)) {
|
||||
this.modifier &= ~getModifierBit(code);
|
||||
} else {
|
||||
this.pressedKeys.delete(code);
|
||||
}
|
||||
return this.buildReport();
|
||||
}
|
||||
|
||||
reset(): number[] {
|
||||
this.modifier = 0;
|
||||
this.pressedKeys.clear();
|
||||
return this.buildReport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the 8-byte HID keyboard report
|
||||
* Byte 0: Modifier keys bitmap
|
||||
* Byte 1: Reserved (0x00)
|
||||
* Bytes 2-7: Up to 6 keycodes
|
||||
*/
|
||||
private buildReport(): number[] {
|
||||
const report = [this.modifier, 0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
let i = 2;
|
||||
for (const keycode of this.pressedKeys.values()) {
|
||||
if (i >= 8) break;
|
||||
report[i++] = keycode;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
getModifier(): number {
|
||||
return this.modifier;
|
||||
}
|
||||
|
||||
getPressedKeyCount(): number {
|
||||
return this.pressedKeys.size;
|
||||
}
|
||||
}
|
||||
|
||||
export const keyboard = new KeyboardReport();
|
||||
@@ -1,241 +0,0 @@
|
||||
export const KeyboardCodes: Map<string, number> = new Map([
|
||||
// Letters
|
||||
['KeyA', 4],
|
||||
['KeyB', 5],
|
||||
['KeyC', 6],
|
||||
['KeyD', 7],
|
||||
['KeyE', 8],
|
||||
['KeyF', 9],
|
||||
['KeyG', 10],
|
||||
['KeyH', 11],
|
||||
['KeyI', 12],
|
||||
['KeyJ', 13],
|
||||
['KeyK', 14],
|
||||
['KeyL', 15],
|
||||
['KeyM', 16],
|
||||
['KeyN', 17],
|
||||
['KeyO', 18],
|
||||
['KeyP', 19],
|
||||
['KeyQ', 20],
|
||||
['KeyR', 21],
|
||||
['KeyS', 22],
|
||||
['KeyT', 23],
|
||||
['KeyU', 24],
|
||||
['KeyV', 25],
|
||||
['KeyW', 26],
|
||||
['KeyX', 27],
|
||||
['KeyY', 28],
|
||||
['KeyZ', 29],
|
||||
|
||||
// Numbers
|
||||
['Digit1', 30],
|
||||
['Digit2', 31],
|
||||
['Digit3', 32],
|
||||
['Digit4', 33],
|
||||
['Digit5', 34],
|
||||
['Digit6', 35],
|
||||
['Digit7', 36],
|
||||
['Digit8', 37],
|
||||
['Digit9', 38],
|
||||
['Digit0', 39],
|
||||
|
||||
// Special keys
|
||||
['Enter', 40],
|
||||
['Escape', 41],
|
||||
['Backspace', 42],
|
||||
['Tab', 43],
|
||||
['Space', 44],
|
||||
['Minus', 45],
|
||||
['Equal', 46],
|
||||
['BracketLeft', 47],
|
||||
['BracketRight', 48],
|
||||
['Backslash', 49],
|
||||
['Semicolon', 51],
|
||||
['Quote', 52],
|
||||
['Backquote', 53],
|
||||
['Comma', 54],
|
||||
['Period', 55],
|
||||
['Slash', 56],
|
||||
['CapsLock', 57],
|
||||
|
||||
// ISO keyboard specific
|
||||
['IntlHash', 50],
|
||||
|
||||
// Function keys
|
||||
['F1', 58],
|
||||
['F2', 59],
|
||||
['F3', 60],
|
||||
['F4', 61],
|
||||
['F5', 62],
|
||||
['F6', 63],
|
||||
['F7', 64],
|
||||
['F8', 65],
|
||||
['F9', 66],
|
||||
['F10', 67],
|
||||
['F11', 68],
|
||||
['F12', 69],
|
||||
|
||||
// Control keys
|
||||
['PrintScreen', 70],
|
||||
['ScrollLock', 71],
|
||||
['Pause', 72],
|
||||
['Insert', 73],
|
||||
['Home', 74],
|
||||
['PageUp', 75],
|
||||
['Delete', 76],
|
||||
['End', 77],
|
||||
['PageDown', 78],
|
||||
|
||||
// Arrow keys
|
||||
['ArrowRight', 79],
|
||||
['ArrowLeft', 80],
|
||||
['ArrowDown', 81],
|
||||
['ArrowUp', 82],
|
||||
|
||||
// Numpad
|
||||
['NumLock', 83],
|
||||
['NumpadDivide', 84],
|
||||
['NumpadMultiply', 85],
|
||||
['NumpadSubtract', 86],
|
||||
['NumpadAdd', 87],
|
||||
['NumpadEnter', 88],
|
||||
['Numpad1', 89],
|
||||
['Numpad2', 90],
|
||||
['Numpad3', 91],
|
||||
['Numpad4', 92],
|
||||
['Numpad5', 93],
|
||||
['Numpad6', 94],
|
||||
['Numpad7', 95],
|
||||
['Numpad8', 96],
|
||||
['Numpad9', 97],
|
||||
['Numpad0', 98],
|
||||
['NumpadDecimal', 99],
|
||||
|
||||
// International / Non-US keyboard keys
|
||||
['IntlBackslash', 100],
|
||||
['ContextMenu', 101],
|
||||
['Power', 102],
|
||||
['NumpadEqual', 103],
|
||||
|
||||
// Extended function keys
|
||||
['F13', 104],
|
||||
['F14', 105],
|
||||
['F15', 106],
|
||||
['F16', 107],
|
||||
['F17', 108],
|
||||
['F18', 109],
|
||||
['F19', 110],
|
||||
['F20', 111],
|
||||
['F21', 112],
|
||||
['F22', 113],
|
||||
['F23', 114],
|
||||
['F24', 115],
|
||||
|
||||
// System / Edit keys
|
||||
['Execute', 116],
|
||||
['Help', 117],
|
||||
['Props', 118],
|
||||
['Select', 119],
|
||||
['Stop', 120],
|
||||
['Again', 121],
|
||||
['Undo', 122],
|
||||
['Cut', 123],
|
||||
['Copy', 124],
|
||||
['Paste', 125],
|
||||
['Find', 126],
|
||||
|
||||
// Media / Volume keys
|
||||
['AudioVolumeMute', 127],
|
||||
['AudioVolumeUp', 128],
|
||||
['AudioVolumeDown', 129],
|
||||
['VolumeMute', 127],
|
||||
['VolumeUp', 128],
|
||||
['VolumeDown', 129],
|
||||
|
||||
// Locking keys (for keyboards with physical lock keys)
|
||||
['LockingCapsLock', 130],
|
||||
['LockingNumLock', 131],
|
||||
['LockingScrollLock', 132],
|
||||
|
||||
// Numpad additional
|
||||
['NumpadComma', 133],
|
||||
['NumpadEqual2', 134],
|
||||
|
||||
// International keys - Japanese
|
||||
['IntlRo', 135],
|
||||
['KanaMode', 136],
|
||||
['IntlYen', 137],
|
||||
['Convert', 138],
|
||||
['NonConvert', 139],
|
||||
|
||||
// International keys - Additional Japanese
|
||||
['International6', 140],
|
||||
['International7', 141],
|
||||
['International8', 142],
|
||||
['International9', 143],
|
||||
|
||||
// Language keys - Korean/Japanese/Chinese
|
||||
['Lang1', 144],
|
||||
['Lang2', 145],
|
||||
['Lang3', 146],
|
||||
['Lang4', 147],
|
||||
['Lang5', 148],
|
||||
['Lang6', 149],
|
||||
['Lang7', 150],
|
||||
['Lang8', 151],
|
||||
['Lang9', 152],
|
||||
|
||||
// Numpad extended
|
||||
['NumpadParenLeft', 182],
|
||||
['NumpadParenRight', 183],
|
||||
['NumpadBackspace', 187],
|
||||
['NumpadMemoryStore', 208],
|
||||
['NumpadMemoryRecall', 209],
|
||||
['NumpadMemoryClear', 210],
|
||||
['NumpadMemoryAdd', 211],
|
||||
['NumpadMemorySubtract', 212],
|
||||
['NumpadClear', 216],
|
||||
['NumpadClearEntry', 217],
|
||||
|
||||
// Modifier keys
|
||||
['ControlLeft', 224],
|
||||
['ShiftLeft', 225],
|
||||
['AltLeft', 226],
|
||||
['MetaLeft', 227],
|
||||
['WinLeft', 227],
|
||||
['ControlRight', 228],
|
||||
['ShiftRight', 229],
|
||||
['AltRight', 230],
|
||||
['MetaRight', 231],
|
||||
['WinRight', 231],
|
||||
|
||||
// Media keys
|
||||
['MediaPlayPause', 232],
|
||||
['MediaStop', 233],
|
||||
['MediaTrackPrevious', 234],
|
||||
['MediaTrackNext', 235],
|
||||
['Eject', 236],
|
||||
['MediaSelect', 237],
|
||||
|
||||
// Application launch keys
|
||||
['LaunchMail', 238],
|
||||
['LaunchApp1', 239],
|
||||
['LaunchApp2', 240],
|
||||
|
||||
// Additional browser/system keys
|
||||
['BrowserSearch', 240],
|
||||
['BrowserHome', 241],
|
||||
['BrowserBack', 242],
|
||||
['BrowserForward', 243],
|
||||
['BrowserStop', 244],
|
||||
['BrowserRefresh', 245],
|
||||
['BrowserFavorites', 246],
|
||||
|
||||
// Sleep/Wake keys
|
||||
['Sleep', 248],
|
||||
['Wake', 249],
|
||||
|
||||
// Accessibility keys
|
||||
['MediaRewind', 250],
|
||||
['MediaFastForward', 251]
|
||||
]);
|
||||
281
browser/src/libs/keyboard/keymap.ts
Normal file
281
browser/src/libs/keyboard/keymap.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
// Modifier key bit positions
|
||||
export const ModifierBits = {
|
||||
LeftCtrl: 1 << 0,
|
||||
LeftShift: 1 << 1,
|
||||
LeftAlt: 1 << 2,
|
||||
LeftMeta: 1 << 3,
|
||||
RightCtrl: 1 << 4,
|
||||
RightShift: 1 << 5,
|
||||
RightAlt: 1 << 6,
|
||||
RightMeta: 1 << 7
|
||||
} as const;
|
||||
|
||||
// Map event.code to HID modifier bit
|
||||
export const ModifierMap: Record<string, number> = {
|
||||
ControlLeft: ModifierBits.LeftCtrl,
|
||||
ShiftLeft: ModifierBits.LeftShift,
|
||||
AltLeft: ModifierBits.LeftAlt,
|
||||
MetaLeft: ModifierBits.LeftMeta,
|
||||
ControlRight: ModifierBits.RightCtrl,
|
||||
ShiftRight: ModifierBits.RightShift,
|
||||
AltRight: ModifierBits.RightAlt,
|
||||
MetaRight: ModifierBits.RightMeta
|
||||
};
|
||||
|
||||
// Map event.code to HID keycode
|
||||
export const KeycodeMap: Record<string, number> = {
|
||||
// Letters
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
KeyD: 0x07,
|
||||
KeyE: 0x08,
|
||||
KeyF: 0x09,
|
||||
KeyG: 0x0a,
|
||||
KeyH: 0x0b,
|
||||
KeyI: 0x0c,
|
||||
KeyJ: 0x0d,
|
||||
KeyK: 0x0e,
|
||||
KeyL: 0x0f,
|
||||
KeyM: 0x10,
|
||||
KeyN: 0x11,
|
||||
KeyO: 0x12,
|
||||
KeyP: 0x13,
|
||||
KeyQ: 0x14,
|
||||
KeyR: 0x15,
|
||||
KeyS: 0x16,
|
||||
KeyT: 0x17,
|
||||
KeyU: 0x18,
|
||||
KeyV: 0x19,
|
||||
KeyW: 0x1a,
|
||||
KeyX: 0x1b,
|
||||
KeyY: 0x1c,
|
||||
KeyZ: 0x1d,
|
||||
|
||||
// Numbers
|
||||
Digit1: 0x1e,
|
||||
Digit2: 0x1f,
|
||||
Digit3: 0x20,
|
||||
Digit4: 0x21,
|
||||
Digit5: 0x22,
|
||||
Digit6: 0x23,
|
||||
Digit7: 0x24,
|
||||
Digit8: 0x25,
|
||||
Digit9: 0x26,
|
||||
Digit0: 0x27,
|
||||
|
||||
// Special keys
|
||||
Enter: 0x28,
|
||||
Escape: 0x29,
|
||||
Backspace: 0x2a,
|
||||
Tab: 0x2b,
|
||||
Space: 0x2c,
|
||||
Minus: 0x2d,
|
||||
Equal: 0x2e,
|
||||
BracketLeft: 0x2f,
|
||||
BracketRight: 0x30,
|
||||
Backslash: 0x31,
|
||||
Semicolon: 0x33,
|
||||
Quote: 0x34,
|
||||
Backquote: 0x35,
|
||||
Comma: 0x36,
|
||||
Period: 0x37,
|
||||
Slash: 0x38,
|
||||
CapsLock: 0x39,
|
||||
|
||||
// Function keys
|
||||
F1: 0x3a,
|
||||
F2: 0x3b,
|
||||
F3: 0x3c,
|
||||
F4: 0x3d,
|
||||
F5: 0x3e,
|
||||
F6: 0x3f,
|
||||
F7: 0x40,
|
||||
F8: 0x41,
|
||||
F9: 0x42,
|
||||
F10: 0x43,
|
||||
F11: 0x44,
|
||||
F12: 0x45,
|
||||
|
||||
// Control keys
|
||||
PrintScreen: 0x46,
|
||||
ScrollLock: 0x47,
|
||||
Pause: 0x48,
|
||||
Insert: 0x49,
|
||||
Home: 0x4a,
|
||||
PageUp: 0x4b,
|
||||
Delete: 0x4c,
|
||||
End: 0x4d,
|
||||
PageDown: 0x4e,
|
||||
|
||||
// Arrow keys
|
||||
ArrowRight: 0x4f,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowDown: 0x51,
|
||||
ArrowUp: 0x52,
|
||||
|
||||
// Numpad
|
||||
NumLock: 0x53,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadAdd: 0x57,
|
||||
NumpadEnter: 0x58,
|
||||
Numpad1: 0x59,
|
||||
Numpad2: 0x5a,
|
||||
Numpad3: 0x5b,
|
||||
Numpad4: 0x5c,
|
||||
Numpad5: 0x5d,
|
||||
Numpad6: 0x5e,
|
||||
Numpad7: 0x5f,
|
||||
Numpad8: 0x60,
|
||||
Numpad9: 0x61,
|
||||
Numpad0: 0x62,
|
||||
NumpadDecimal: 0x63,
|
||||
|
||||
// International / Non-US keyboard keys
|
||||
IntlBackslash: 0x64,
|
||||
ContextMenu: 0x65,
|
||||
Power: 0x66,
|
||||
NumpadEqual: 0x67,
|
||||
|
||||
// Extended function keys
|
||||
F13: 0x68,
|
||||
F14: 0x69,
|
||||
F15: 0x6a,
|
||||
F16: 0x6b,
|
||||
F17: 0x6c,
|
||||
F18: 0x6d,
|
||||
F19: 0x6e,
|
||||
F20: 0x6f,
|
||||
F21: 0x70,
|
||||
F22: 0x71,
|
||||
F23: 0x72,
|
||||
F24: 0x73,
|
||||
|
||||
// System / Edit keys
|
||||
Execute: 0x74,
|
||||
Help: 0x75,
|
||||
Props: 0x76,
|
||||
Select: 0x77,
|
||||
Stop: 0x78,
|
||||
Again: 0x79,
|
||||
Undo: 0x7a,
|
||||
Cut: 0x7b,
|
||||
Copy: 0x7c,
|
||||
Paste: 0x7d,
|
||||
Find: 0x7e,
|
||||
|
||||
// Media / Volume keys
|
||||
AudioVolumeMute: 0x7f,
|
||||
AudioVolumeUp: 0x80,
|
||||
AudioVolumeDown: 0x81,
|
||||
VolumeMute: 0x7f, // Alias
|
||||
VolumeUp: 0x80, // Alias
|
||||
VolumeDown: 0x81, // Alias
|
||||
|
||||
// Locking keys (for keyboards with physical lock keys)
|
||||
LockingCapsLock: 0x82,
|
||||
LockingNumLock: 0x83,
|
||||
LockingScrollLock: 0x84,
|
||||
|
||||
// Numpad additional
|
||||
NumpadComma: 0x85,
|
||||
NumpadEqual2: 0x86, // AS/400 keyboard equal key
|
||||
|
||||
// International keys - Japanese
|
||||
IntlRo: 0x87, // Japanese Ro key (ろ)
|
||||
KanaMode: 0x88, // Katakana/Hiragana toggle
|
||||
IntlYen: 0x89, // Japanese Yen (¥)
|
||||
Convert: 0x8a, // Japanese Henkan (変換)
|
||||
NonConvert: 0x8b, // Japanese Muhenkan (無変換)
|
||||
|
||||
// International keys - Additional Japanese
|
||||
International6: 0x8c,
|
||||
International7: 0x8d,
|
||||
International8: 0x8e,
|
||||
International9: 0x8f,
|
||||
|
||||
// Language keys - Korean/Japanese/Chinese
|
||||
Lang1: 0x90, // Korean Hangul/English toggle
|
||||
Lang2: 0x91, // Korean Hanja
|
||||
Lang3: 0x92, // Japanese Katakana
|
||||
Lang4: 0x93, // Japanese Hiragana
|
||||
Lang5: 0x94, // Japanese Zenkaku/Hankaku
|
||||
Lang6: 0x95,
|
||||
Lang7: 0x96,
|
||||
Lang8: 0x97,
|
||||
Lang9: 0x98,
|
||||
|
||||
// ISO keyboard specific
|
||||
IntlHash: 0x32, // Non-US # and ~ (ISO keyboards)
|
||||
|
||||
// Numpad extended
|
||||
NumpadParenLeft: 0xb6,
|
||||
NumpadParenRight: 0xb7,
|
||||
NumpadBackspace: 0xbb,
|
||||
NumpadMemoryStore: 0xd0,
|
||||
NumpadMemoryRecall: 0xd1,
|
||||
NumpadMemoryClear: 0xd2,
|
||||
NumpadMemoryAdd: 0xd3,
|
||||
NumpadMemorySubtract: 0xd4,
|
||||
NumpadClear: 0xd8,
|
||||
NumpadClearEntry: 0xd9,
|
||||
|
||||
// Additional browser/system keys
|
||||
BrowserSearch: 0xf0,
|
||||
BrowserHome: 0xf1,
|
||||
BrowserBack: 0xf2,
|
||||
BrowserForward: 0xf3,
|
||||
BrowserStop: 0xf4,
|
||||
BrowserRefresh: 0xf5,
|
||||
BrowserFavorites: 0xf6,
|
||||
|
||||
// Media keys
|
||||
MediaPlayPause: 0xe8,
|
||||
MediaStop: 0xe9,
|
||||
MediaTrackPrevious: 0xea,
|
||||
MediaTrackNext: 0xeb,
|
||||
Eject: 0xec,
|
||||
MediaSelect: 0xed,
|
||||
|
||||
// Application launch keys
|
||||
LaunchMail: 0xee,
|
||||
LaunchApp1: 0xef,
|
||||
LaunchApp2: 0xf0,
|
||||
|
||||
// Sleep/Wake keys
|
||||
Sleep: 0xf8,
|
||||
Wake: 0xf9,
|
||||
|
||||
// Accessibility keys
|
||||
MediaRewind: 0xfa,
|
||||
MediaFastForward: 0xfb,
|
||||
|
||||
// Modifier keys (HID Usage Page 0x07, codes 0xE0-0xE7)
|
||||
ControlLeft: 0xe0,
|
||||
ShiftLeft: 0xe1,
|
||||
AltLeft: 0xe2,
|
||||
MetaLeft: 0xe3,
|
||||
WinLeft: 0xe3,
|
||||
ControlRight: 0xe4,
|
||||
ShiftRight: 0xe5,
|
||||
AltRight: 0xe6,
|
||||
MetaRight: 0xe7,
|
||||
WinRight: 0xe7
|
||||
};
|
||||
|
||||
// Check if code is a modifier key
|
||||
export function isModifier(code: string): boolean {
|
||||
return code in ModifierMap;
|
||||
}
|
||||
|
||||
// Get modifier bit for code
|
||||
export function getModifierBit(code: string): number {
|
||||
return ModifierMap[code] ?? 0;
|
||||
}
|
||||
|
||||
// Get keycode for code
|
||||
export function getKeycode(code: string): number | undefined {
|
||||
return KeycodeMap[code];
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { device } from '@/libs/device';
|
||||
import { Key } from '@/libs/device/mouse.ts';
|
||||
import { MouseReportRelative } from '@/libs/mouse';
|
||||
|
||||
const MOUSE_JIGGLER_INTERVAL = 15_000;
|
||||
const EMPTY_KEY: Key = new Key(false, false, false);
|
||||
|
||||
class MouseJiggler {
|
||||
private lastMoveTime: number;
|
||||
private timer: number | null;
|
||||
private mode: 'enable' | 'disable';
|
||||
private mouseReport: MouseReportRelative;
|
||||
|
||||
constructor() {
|
||||
this.lastMoveTime = Date.now();
|
||||
this.timer = null;
|
||||
this.mode = 'disable';
|
||||
this.mouseReport = new MouseReportRelative();
|
||||
}
|
||||
|
||||
// enable or disable mouse jiggler
|
||||
@@ -43,8 +44,11 @@ class MouseJiggler {
|
||||
}
|
||||
|
||||
async sendJiggle(): Promise<void> {
|
||||
await device.sendMouseRelativeData(EMPTY_KEY, 10, 10, 0);
|
||||
await device.sendMouseRelativeData(EMPTY_KEY, -10, -10, 0);
|
||||
const report1 = this.mouseReport.buildReport(10, 10, 0);
|
||||
const report2 = this.mouseReport.buildReport(-10, -10, 0);
|
||||
|
||||
await device.sendKeyboardData([0x01, ...report1]);
|
||||
await device.sendKeyboardData([0x01, ...report2]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
browser/src/libs/mouse/index.ts
Normal file
134
browser/src/libs/mouse/index.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// Maximum absolute coordinate value
|
||||
const MAX_ABS_COORD = 4096;
|
||||
|
||||
// Button bit positions
|
||||
const MouseButtons = {
|
||||
Left: 1 << 0,
|
||||
Right: 1 << 1,
|
||||
Middle: 1 << 2,
|
||||
Back: 1 << 3,
|
||||
Forward: 1 << 4
|
||||
} as const;
|
||||
|
||||
// Map browser button index to HID bit
|
||||
function getMouseButtonBit(button: number): number {
|
||||
switch (button) {
|
||||
case 0:
|
||||
return MouseButtons.Left;
|
||||
case 1:
|
||||
return MouseButtons.Middle;
|
||||
case 2:
|
||||
return MouseButtons.Right;
|
||||
case 3:
|
||||
return MouseButtons.Back;
|
||||
case 4:
|
||||
return MouseButtons.Forward;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative Mouse Report (4 bytes)
|
||||
*
|
||||
* Byte 0: Buttons
|
||||
* Byte 1: X movement (-127 to 127)
|
||||
* Byte 2: Y movement (-127 to 127)
|
||||
* Byte 3: Wheel (-127 to 127)
|
||||
*/
|
||||
export class MouseReportRelative {
|
||||
private buttons: number = 0;
|
||||
|
||||
buttonDown(button: number): void {
|
||||
this.buttons |= getMouseButtonBit(button);
|
||||
}
|
||||
|
||||
buttonUp(button: number): void {
|
||||
this.buttons &= ~getMouseButtonBit(button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build relative mouse report
|
||||
* @param deltaX X movement (-127 to 127)
|
||||
* @param deltaY Y movement (-127 to 127)
|
||||
* @param wheel Scroll wheel (-127 to 127, negative = down)
|
||||
*/
|
||||
buildReport(deltaX: number, deltaY: number, wheel: number = 0): number[] {
|
||||
const x = this.clamp(Math.round(deltaX), -127, 127) & 0xff;
|
||||
const y = this.clamp(Math.round(deltaY), -127, 127) & 0xff;
|
||||
const scroll = this.clamp(Math.round(wheel), -127, 127) & 0xff;
|
||||
return [this.buttons, x, y, scroll];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build button-only report (no movement)
|
||||
*/
|
||||
buildButtonReport(): number[] {
|
||||
return this.buildReport(0, 0, 0);
|
||||
}
|
||||
|
||||
reset(): number[] {
|
||||
this.buttons = 0;
|
||||
return this.buildReport(0, 0, 0);
|
||||
}
|
||||
|
||||
private clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute Mouse Report (6 bytes)
|
||||
*
|
||||
* Byte 0: Buttons
|
||||
* Byte 1-2: X position (0 to 32767, Little Endian)
|
||||
* Byte 3-4: Y position (0 to 32767, Little Endian)
|
||||
* Byte 5: Wheel
|
||||
*/
|
||||
export class MouseAbsoluteRelative {
|
||||
private buttons: number = 0;
|
||||
|
||||
buttonDown(button: number): void {
|
||||
this.buttons |= getMouseButtonBit(button);
|
||||
}
|
||||
|
||||
buttonUp(button: number): void {
|
||||
this.buttons &= ~getMouseButtonBit(button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build absolute mouse report
|
||||
* @param x X position (0.0 to 1.0, normalized)
|
||||
* @param y Y position (0.0 to 1.0, normalized)
|
||||
* @param wheel Scroll wheel (-127 to 127)
|
||||
*/
|
||||
buildReport(x: number, y: number, wheel: number = 0): number[] {
|
||||
// Convert normalized coordinates (0-1) to absolute coordinates (0-4096)
|
||||
const xAbs = Math.floor(Math.max(0, Math.min(1, x)) * MAX_ABS_COORD);
|
||||
const yAbs = Math.floor(Math.max(0, Math.min(1, y)) * MAX_ABS_COORD);
|
||||
|
||||
const x1 = xAbs & 0xff;
|
||||
const x2 = (xAbs >> 8) & 0xff;
|
||||
const y1 = yAbs & 0xff;
|
||||
const y2 = (yAbs >> 8) & 0xff;
|
||||
const scroll = this.clamp(Math.round(wheel), -127, 127) & 0xff;
|
||||
|
||||
return [this.buttons, x1, x2, y1, y2, scroll];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build button-only report (keeps last position)
|
||||
*/
|
||||
buildButtonReport(lastX: number, lastY: number): number[] {
|
||||
return this.buildReport(lastX, lastY, 0);
|
||||
}
|
||||
|
||||
reset(): number[] {
|
||||
this.buttons = 0;
|
||||
return this.buildReport(0, 0, 0);
|
||||
}
|
||||
|
||||
private clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
2
browser/vite.config.d.ts
vendored
2
browser/vite.config.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
declare const _default: import('vite').UserConfig;
|
||||
export default _default;
|
||||
|
||||
Reference in New Issue
Block a user