From 84830421eea596eb47f70b22784eb92eff100c80 Mon Sep 17 00:00:00 2001 From: wj-xiao Date: Thu, 15 Jan 2026 16:45:48 +0800 Subject: [PATCH] fix: compatible with Windows AltGr key --- web/src/lib/browser.ts | 56 ++++++++++++++++++++++++ web/src/pages/desktop/keyboard/index.tsx | 55 +++++++++++++++++++++-- 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 web/src/lib/browser.ts diff --git a/web/src/lib/browser.ts b/web/src/lib/browser.ts new file mode 100644 index 0000000..8f1f012 --- /dev/null +++ b/web/src/lib/browser.ts @@ -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() + }; +} diff --git a/web/src/pages/desktop/keyboard/index.tsx b/web/src/pages/desktop/keyboard/index.tsx index 4184902..c5f5b97 100644 --- a/web/src/pages/desktop/keyboard/index.tsx +++ b/web/src/pages/desktop/keyboard/index.tsx @@ -1,18 +1,31 @@ import { useEffect, useRef } from 'react'; import { useAtomValue } from 'jotai'; +import { getOperatingSystem } from '@/lib/browser.ts'; import { KeyboardReport } from '@/lib/keyboard.ts'; import { isModifier } from '@/lib/keymap'; import { client, MessageEvent } from '@/lib/websocket.ts'; import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts'; +interface AltGrState { + active: boolean; + ctrlLeftTimestamp: number; +} + +const ALTGR_THRESHOLD_MS = 10; + export const Keyboard = () => { const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom); const keyboardRef = useRef(new KeyboardReport()); const pressedKeys = useRef(new Set()); + const altGrState = useRef(null); useEffect(() => { + if (getOperatingSystem() === 'Windows' && !altGrState.current) { + altGrState.current = { active: false, ctrlLeftTimestamp: 0 }; + } + if (!isKeyboardEnabled) { releaseKeys(); return; @@ -31,8 +44,20 @@ export const Keyboard = () => { event.stopPropagation(); const code = event.code; - if (pressedKeys.current.has(code)) { - return; + 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); @@ -48,14 +73,28 @@ export const Keyboard = () => { 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)) { - handleKeyEvent({ type: 'keyup', code: pressedCode }); - pressedKeys.current.delete(pressedCode); + keysToRelease.push(pressedCode); } }); + + for (const key of keysToRelease) { + handleKeyEvent({ type: 'keyup', code: key }); + pressedKeys.current.delete(key); + } } pressedKeys.current.delete(code); @@ -82,6 +121,12 @@ export const Keyboard = () => { pressedKeys.current.clear(); + // Reset AltGr state + if (altGrState.current) { + altGrState.current.active = false; + altGrState.current.ctrlLeftTimestamp = 0; + } + const report = keyboardRef.current.reset(); sendReport(report); } @@ -91,6 +136,8 @@ export const Keyboard = () => { document.removeEventListener('keyup', handleKeyUp); window.removeEventListener('blur', handleBlur); document.removeEventListener('visibilitychange', handleVisibilityChange); + + releaseKeys(); }; }, [isKeyboardEnabled]);