mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-24 11:56:36 +08:00
- WebRTC:默认 mDNS 调整为 QueryOnly,Answer 阶段改为等待 ICE gathering complete(2.5s 超时),提升首次建连成功率与候选完整性 - WebRTC:前端建连流程增加阶段化状态与串行保护(connectInFlight/ready gate),优化配置变更后的重连时机与失败处理,减少竞态和无效重试 - Device:平台信息补充 `/proc/device-tree/model` 回退并统一展示为“处理器/平台” - HID:键盘输入链路统一为 HID usage + modifier bitmask,修复虚拟键盘/宏/粘贴键值映射错误
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
// HID input composable - manages keyboard and mouse input
|
|
// Extracted from ConsoleView.vue for better separation of concerns
|
|
|
|
import { ref, type Ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { toast } from 'vue-sonner'
|
|
import { hidApi } from '@/api'
|
|
import { keyboardEventToHidCode, updateModifierMaskForHidKey } from '@/lib/keyboardMappings'
|
|
|
|
export interface HidInputState {
|
|
mouseMode: Ref<'absolute' | 'relative'>
|
|
pressedKeys: Ref<string[]>
|
|
keyboardLed: Ref<{ capsLock: boolean; numLock: boolean; scrollLock: boolean }>
|
|
mousePosition: Ref<{ x: number; y: number }>
|
|
isPointerLocked: Ref<boolean>
|
|
cursorVisible: Ref<boolean>
|
|
}
|
|
|
|
export interface UseHidInputOptions {
|
|
videoContainerRef: Ref<HTMLDivElement | null>
|
|
getVideoElement: () => HTMLElement | null
|
|
isFullscreen: Ref<boolean>
|
|
}
|
|
|
|
export function useHidInput(options: UseHidInputOptions) {
|
|
const { t } = useI18n()
|
|
|
|
// State
|
|
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
|
const pressedKeys = ref<string[]>([])
|
|
const keyboardLed = ref({
|
|
capsLock: false,
|
|
numLock: false,
|
|
scrollLock: false,
|
|
})
|
|
const activeModifierMask = ref(0)
|
|
const mousePosition = ref({ x: 0, y: 0 })
|
|
const lastMousePosition = ref({ x: 0, y: 0 })
|
|
const isPointerLocked = ref(false)
|
|
const cursorVisible = ref(localStorage.getItem('hidShowCursor') !== 'false')
|
|
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
|
|
|
// Error handling - silently handle all HID errors
|
|
function handleHidError(_error: unknown, _operation: string) {
|
|
// All HID errors are silently ignored
|
|
}
|
|
|
|
// Check if a key should be blocked
|
|
function shouldBlockKey(e: KeyboardEvent): boolean {
|
|
if (options.isFullscreen.value) return true
|
|
|
|
const key = e.key.toUpperCase()
|
|
if (e.ctrlKey && ['W', 'T', 'N'].includes(key)) return false
|
|
if (key === 'F11') return false
|
|
if (e.altKey && key === 'TAB') return false
|
|
|
|
return true
|
|
}
|
|
|
|
// Keyboard handlers
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
const container = options.videoContainerRef.value
|
|
if (!container) return
|
|
|
|
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
|
|
|
if (shouldBlockKey(e)) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
if (!options.isFullscreen.value && (e.metaKey || e.key === 'Meta')) {
|
|
toast.info(t('console.metaKeyHint'), {
|
|
description: t('console.metaKeyHintDesc'),
|
|
duration: 3000,
|
|
})
|
|
}
|
|
|
|
const keyName = e.key === ' ' ? 'Space' : e.key
|
|
if (!pressedKeys.value.includes(keyName)) {
|
|
pressedKeys.value = [...pressedKeys.value, keyName]
|
|
}
|
|
|
|
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
|
|
keyboardLed.value.numLock = e.getModifierState('NumLock')
|
|
keyboardLed.value.scrollLock = e.getModifierState('ScrollLock')
|
|
|
|
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
|
if (hidKey === undefined) {
|
|
return
|
|
}
|
|
|
|
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, true)
|
|
activeModifierMask.value = modifierMask
|
|
hidApi.keyboard('down', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard down'))
|
|
}
|
|
|
|
function handleKeyUp(e: KeyboardEvent) {
|
|
const container = options.videoContainerRef.value
|
|
if (!container) return
|
|
|
|
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
|
|
|
if (shouldBlockKey(e)) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
const keyName = e.key === ' ' ? 'Space' : e.key
|
|
pressedKeys.value = pressedKeys.value.filter(k => k !== keyName)
|
|
|
|
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
|
if (hidKey === undefined) {
|
|
return
|
|
}
|
|
|
|
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, false)
|
|
activeModifierMask.value = modifierMask
|
|
hidApi.keyboard('up', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard up'))
|
|
}
|
|
|
|
// Mouse handlers
|
|
function handleMouseMove(e: MouseEvent) {
|
|
const videoElement = options.getVideoElement()
|
|
if (!videoElement) return
|
|
|
|
if (mouseMode.value === 'absolute') {
|
|
const rect = videoElement.getBoundingClientRect()
|
|
const x = Math.round((e.clientX - rect.left) / rect.width * 32767)
|
|
const y = Math.round((e.clientY - rect.top) / rect.height * 32767)
|
|
|
|
mousePosition.value = { x, y }
|
|
hidApi.mouse({ type: 'move_abs', x, y }).catch(err => handleHidError(err, 'mouse move'))
|
|
} else {
|
|
if (isPointerLocked.value) {
|
|
const dx = e.movementX
|
|
const dy = e.movementY
|
|
|
|
if (dx !== 0 || dy !== 0) {
|
|
const clampedDx = Math.max(-127, Math.min(127, dx))
|
|
const clampedDy = Math.max(-127, Math.min(127, dy))
|
|
hidApi.mouse({ type: 'move', x: clampedDx, y: clampedDy }).catch(err => handleHidError(err, 'mouse move'))
|
|
}
|
|
|
|
mousePosition.value = {
|
|
x: mousePosition.value.x + dx,
|
|
y: mousePosition.value.y + dy,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleMouseDown(e: MouseEvent) {
|
|
e.preventDefault()
|
|
|
|
const container = options.videoContainerRef.value
|
|
if (container && document.activeElement !== container) {
|
|
if (typeof container.focus === 'function') {
|
|
container.focus()
|
|
}
|
|
}
|
|
|
|
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
|
requestPointerLock()
|
|
return
|
|
}
|
|
|
|
const button = e.button === 0 ? 'left' : e.button === 2 ? 'right' : 'middle'
|
|
pressedMouseButton.value = button
|
|
hidApi.mouse({ type: 'down', button }).catch(err => handleHidError(err, 'mouse down'))
|
|
}
|
|
|
|
function handleMouseUp(e: MouseEvent) {
|
|
e.preventDefault()
|
|
handleMouseUpInternal(e.button)
|
|
}
|
|
|
|
function handleWindowMouseUp(e: MouseEvent) {
|
|
if (pressedMouseButton.value !== null) {
|
|
handleMouseUpInternal(e.button)
|
|
}
|
|
}
|
|
|
|
function handleMouseUpInternal(rawButton: number) {
|
|
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
|
pressedMouseButton.value = null
|
|
return
|
|
}
|
|
|
|
const button = rawButton === 0 ? 'left' : rawButton === 2 ? 'right' : 'middle'
|
|
|
|
if (pressedMouseButton.value !== button) return
|
|
|
|
pressedMouseButton.value = null
|
|
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up'))
|
|
}
|
|
|
|
function handleWheel(e: WheelEvent) {
|
|
e.preventDefault()
|
|
const scroll = e.deltaY > 0 ? -1 : 1
|
|
hidApi.mouse({ type: 'scroll', scroll }).catch(err => handleHidError(err, 'mouse scroll'))
|
|
}
|
|
|
|
function handleContextMenu(e: MouseEvent) {
|
|
e.preventDefault()
|
|
}
|
|
|
|
// Pointer lock
|
|
function requestPointerLock() {
|
|
const container = options.videoContainerRef.value
|
|
if (!container) return
|
|
|
|
container.requestPointerLock().catch((err: Error) => {
|
|
toast.error(t('console.pointerLockFailed'), {
|
|
description: err.message,
|
|
})
|
|
})
|
|
}
|
|
|
|
function exitPointerLock() {
|
|
if (document.pointerLockElement) {
|
|
document.exitPointerLock()
|
|
}
|
|
}
|
|
|
|
function handlePointerLockChange() {
|
|
const container = options.videoContainerRef.value
|
|
isPointerLocked.value = document.pointerLockElement === container
|
|
|
|
if (isPointerLocked.value) {
|
|
mousePosition.value = { x: 0, y: 0 }
|
|
toast.info(t('console.pointerLocked'), {
|
|
description: t('console.pointerLockedDesc'),
|
|
duration: 3000,
|
|
})
|
|
}
|
|
}
|
|
|
|
function handlePointerLockError() {
|
|
isPointerLocked.value = false
|
|
}
|
|
|
|
function handleBlur() {
|
|
pressedKeys.value = []
|
|
activeModifierMask.value = 0
|
|
if (pressedMouseButton.value !== null) {
|
|
const button = pressedMouseButton.value
|
|
pressedMouseButton.value = null
|
|
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up (blur)'))
|
|
}
|
|
}
|
|
|
|
// Mode toggle
|
|
function toggleMouseMode() {
|
|
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
|
exitPointerLock()
|
|
}
|
|
|
|
mouseMode.value = mouseMode.value === 'absolute' ? 'relative' : 'absolute'
|
|
lastMousePosition.value = { x: 0, y: 0 }
|
|
mousePosition.value = { x: 0, y: 0 }
|
|
|
|
if (mouseMode.value === 'relative') {
|
|
toast.info(t('console.relativeModeHint'), {
|
|
description: t('console.relativeModeHintDesc'),
|
|
duration: 5000,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Virtual keyboard handlers
|
|
function handleVirtualKeyDown(key: string) {
|
|
if (!pressedKeys.value.includes(key)) {
|
|
pressedKeys.value = [...pressedKeys.value, key]
|
|
}
|
|
}
|
|
|
|
function handleVirtualKeyUp(key: string) {
|
|
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
|
}
|
|
|
|
// Cursor visibility
|
|
function handleCursorVisibilityChange(e: Event) {
|
|
const customEvent = e as CustomEvent<{ visible: boolean }>
|
|
cursorVisible.value = customEvent.detail.visible
|
|
}
|
|
|
|
// Setup event listeners
|
|
function setupEventListeners() {
|
|
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
|
document.addEventListener('pointerlockerror', handlePointerLockError)
|
|
window.addEventListener('blur', handleBlur)
|
|
window.addEventListener('mouseup', handleWindowMouseUp)
|
|
window.addEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
|
}
|
|
|
|
function cleanupEventListeners() {
|
|
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
|
document.removeEventListener('pointerlockerror', handlePointerLockError)
|
|
window.removeEventListener('blur', handleBlur)
|
|
window.removeEventListener('mouseup', handleWindowMouseUp)
|
|
window.removeEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
|
}
|
|
|
|
return {
|
|
// State
|
|
mouseMode,
|
|
pressedKeys,
|
|
keyboardLed,
|
|
mousePosition,
|
|
isPointerLocked,
|
|
cursorVisible,
|
|
|
|
// Keyboard handlers
|
|
handleKeyDown,
|
|
handleKeyUp,
|
|
|
|
// Mouse handlers
|
|
handleMouseMove,
|
|
handleMouseDown,
|
|
handleMouseUp,
|
|
handleWheel,
|
|
handleContextMenu,
|
|
|
|
// Pointer lock
|
|
requestPointerLock,
|
|
exitPointerLock,
|
|
|
|
// Mode toggle
|
|
toggleMouseMode,
|
|
|
|
// Virtual keyboard
|
|
handleVirtualKeyDown,
|
|
handleVirtualKeyUp,
|
|
|
|
// Cursor visibility
|
|
handleCursorVisibilityChange,
|
|
|
|
// Lifecycle
|
|
setupEventListeners,
|
|
cleanupEventListeners,
|
|
}
|
|
}
|