diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index 7ddf11bdf..c1520ff01 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Gem Jiggler-konfiguration", "jiggler_timezone_description": "Tidszone for cron-plan", "jiggler_timezone_label": "Tidszone", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Konfigurer tastaturindstillinger for din enhed", "keyboard_layout_description": "Tastaturlayout for måloperativsystemet", "keyboard_layout_error": "Kunne ikke indstille tastaturlayout: {error}", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 5f7f54f19..da5b7be8b 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Jiggler-Konfiguration speichern", "jiggler_timezone_description": "Zeitzone für Cron-Zeitplan", "jiggler_timezone_label": "Zeitzone", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Konfigurieren Sie die Tastatureinstellungen für Ihr Gerät", "keyboard_layout_description": "Tastaturlayout des Zielbetriebssystems", "keyboard_layout_error": "Tastaturlayout konnte nicht festgelegt werden: {error}", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 6ad2c6b62..cffcd7668 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -389,6 +389,13 @@ "jiggler_save_jiggler_config": "Save Jiggler Config", "jiggler_timezone_description": "Timezone for cron schedule", "jiggler_timezone_label": "Timezone", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Configure keyboard settings for your device", "keyboard_layout_description": "Keyboard layout of target operating system", "keyboard_layout_error": "Failed to set keyboard layout: {error}", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index 957e2f5f0..303c02d84 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Guardar configuración de Jiggler", "jiggler_timezone_description": "Zona horaria para la programación cron", "jiggler_timezone_label": "Zona horaria", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Configure los ajustes del teclado para su dispositivo", "keyboard_layout_description": "Disposición del teclado del sistema operativo de destino", "keyboard_layout_error": "No se pudo establecer la distribución del teclado: {error}", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index 1e21e0b8a..3a6fdf607 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Enregistrer la configuration de Jiggler", "jiggler_timezone_description": "Fuseau horaire pour la planification cron", "jiggler_timezone_label": "Fuseau horaire", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Configurer les paramètres du clavier pour votre appareil", "keyboard_layout_description": "Disposition du clavier du système d'exploitation cible", "keyboard_layout_error": "Échec de la définition de la disposition du clavier : {error}", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 7886685d9..565db4ff7 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Salva la configurazione di Jiggler", "jiggler_timezone_description": "Fuso orario per la pianificazione cron", "jiggler_timezone_label": "Fuso orario", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Configura le impostazioni della tastiera per il tuo dispositivo", "keyboard_layout_description": "Layout della tastiera del sistema operativo di destinazione", "keyboard_layout_error": "Impossibile impostare il layout della tastiera: {error}", diff --git a/ui/localization/messages/ja.json b/ui/localization/messages/ja.json index dbc98da12..b7c927b7c 100644 --- a/ui/localization/messages/ja.json +++ b/ui/localization/messages/ja.json @@ -389,6 +389,13 @@ "jiggler_save_jiggler_config": "ジグラー設定を保存", "jiggler_timezone_description": "Cronスケジュールのタイムゾーン", "jiggler_timezone_label": "タイムゾーン", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "デバイスのキーボード設定を構成します", "keyboard_layout_description": "ターゲットOSのキーボードレイアウト", "keyboard_layout_error": "キーボードレイアウトの設定に失敗しました: {error}", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index 29d2cf83c..92009c301 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Lagre Jiggler-konfigurasjon", "jiggler_timezone_description": "Tidssone for cron-plan", "jiggler_timezone_label": "Tidssone", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Konfigurer tastaturinnstillinger for enheten din", "keyboard_layout_description": "Tastaturoppsett for måloperativsystemet", "keyboard_layout_error": "Klarte ikke å angi tastaturoppsett: {error}", diff --git a/ui/localization/messages/pt.json b/ui/localization/messages/pt.json index bd090d86e..7252c4e73 100644 --- a/ui/localization/messages/pt.json +++ b/ui/localization/messages/pt.json @@ -389,6 +389,13 @@ "jiggler_save_jiggler_config": "Salvar Configuração do Jiggler", "jiggler_timezone_description": "Fuso horário para agendamento cron", "jiggler_timezone_label": "Fuso Horário", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Configure as opções de teclado para o seu dispositivo", "keyboard_layout_description": "Layout do teclado do sistema operacional de destino", "keyboard_layout_error": "Falha ao definir layout do teclado: {error}", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index dfc8ff008..230d5d1d8 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "Spara Jiggler-konfiguration", "jiggler_timezone_description": "Tidszon för cron-schema", "jiggler_timezone_label": "Tidszon", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "Konfigurera tangentbordsinställningar för din enhet", "keyboard_layout_description": "Tangentbordslayout för måloperativsystemet", "keyboard_layout_error": "Misslyckades med att ställa in tangentbordslayout: {error}", diff --git a/ui/localization/messages/zh-tw.json b/ui/localization/messages/zh-tw.json index c07fa76d5..4bbf80da0 100644 --- a/ui/localization/messages/zh-tw.json +++ b/ui/localization/messages/zh-tw.json @@ -389,6 +389,13 @@ "jiggler_save_jiggler_config": "儲存防休眠設定", "jiggler_timezone_description": "Cron 排程的時區", "jiggler_timezone_label": "時區", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "設定您裝置的鍵盤設定", "keyboard_layout_description": "目標作業系統的鍵盤配置", "keyboard_layout_error": "設定鍵盤配置失敗:{error}", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 615276188..9b4345ed7 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -384,6 +384,13 @@ "jiggler_save_jiggler_config": "保存防休眠配置", "jiggler_timezone_description": "Cron 计划所使用的时区。", "jiggler_timezone_label": "时区", + "keyboard_capture_active": "Active", + "keyboard_capture_description": "Capture browser shortcuts (Cmd/Ctrl+Tab, Alt+Tab) and send them to the remote machine", + "keyboard_capture_disabled": "Keyboard Capture disabled", + "keyboard_capture_enabled": "Keyboard Capture enabled", + "keyboard_capture_fullscreen_hint": "Enter fullscreen to capture all keyboard shortcuts including Alt+Tab", + "keyboard_capture_limited": "Limited", + "keyboard_capture_title": "Keyboard Capture", "keyboard_description": "为您的设备配置键盘相关设置。", "keyboard_layout_description": "目标操作系统的键盘布局。", "keyboard_layout_error": "设置键盘布局失败:{error}", diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 364845193..5de14dcc6 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -1,12 +1,21 @@ import { Fragment, useCallback, useRef } from "react"; import { MdOutlineContentPasteGo } from "react-icons/md"; -import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; +import { + LuCable, + LuHardDrive, + LuLock, + LuLockOpen, + LuMaximize, + LuSettings, + LuSignal, +} from "react-icons/lu"; import { FaKeyboard } from "react-icons/fa6"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { CommandLineIcon } from "@heroicons/react/20/solid"; import { cx } from "@/cva.config"; import { useHidStore, useMountMediaStore, useSettingsStore, useUiStore } from "@hooks/stores"; +import notifications from "@/notifications"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; import { Button } from "@components/Button"; import Container from "@components/Container"; @@ -26,7 +35,7 @@ export default function Actionbar({ const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore(); const { remoteVirtualMediaState } = useMountMediaStore(); - const { developerMode } = useSettingsStore(); + const { developerMode, keyboardCaptureMode, setKeyboardCaptureMode } = useSettingsStore(); // This is the only way to get a reliable state change for the popover // at time of writing this there is no mount, or unmount event for the popover @@ -188,6 +197,21 @@ export default function Actionbar({ +
+
)} + {keyboardCaptureMode && ( +
+ {m.keyboard_capture_title()}: + + {isKeyboardLockActive + ? m.keyboard_capture_active() + : m.keyboard_capture_limited()} + +
+ )} + {showPressedKeys && (
{m.info_keys()} diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 1690aaebe..df2062e5f 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState, useRef, useCallback } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; -import { LuPlay } from "react-icons/lu"; +import { LuKeyboard, LuPlay } from "react-icons/lu"; import { BsMouseFill } from "react-icons/bs"; import { m } from "@localizations/messages.js"; @@ -395,6 +395,52 @@ export function PointerLockBar({ show }: PointerLockBarProps) { ); } +interface KeyboardCaptureBarProps { + readonly show: boolean; +} + +export function KeyboardCaptureBar({ show }: KeyboardCaptureBarProps) { + // When show goes false, unmount inner component entirely. + // When show goes true, inner mounts fresh with visible=true and starts auto-dismiss timer. + return show ? : null; +} + +function KeyboardCaptureBarInner() { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => setVisible(false), 8000); + return () => clearTimeout(timer); + }, []); + + return ( + + {visible ? ( + +
+ +
+
+ + + {m.keyboard_capture_fullscreen_hint()} + +
+
+
+
+
+ ) : null} +
+ ); +} + interface RebootingOverlayProps { readonly show: boolean; readonly postRebootAction: PostRebootAction; diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 1e44e545c..a2d964c53 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -5,13 +5,14 @@ import { cx } from "@/cva.config"; import { isWindows } from "@/utils"; import useKeyboard from "@hooks/useKeyboard"; import useMouse from "@hooks/useMouse"; -import { useRTCStore, useSettingsStore, useVideoStore } from "@hooks/stores"; +import { useRTCStore, useSettingsStore, useUiStore, useVideoStore } from "@hooks/stores"; import VirtualKeyboard from "@components/VirtualKeyboard"; import Actionbar from "@components/ActionBar"; import MacroBar from "@components/MacroBar"; import InfoBar from "@components/InfoBar"; import { HDMIErrorOverlay, + KeyboardCaptureBar, LoadingVideoOverlay, NoAutoplayPermissionsOverlay, PointerLockBar, @@ -27,11 +28,14 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu const { mediaStream, peerConnectionState } = useRTCStore(); const [isPlaying, setIsPlaying] = useState(false); const [isPointerLockActive, setIsPointerLockActive] = useState(false); - const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false); + const { setIsKeyboardLockActive } = useUiStore(); const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost"; + // macOS detection for Meta key fix + const isMacClient = useMemo(() => /Mac|iPhone|iPad|iPod/.test(navigator.userAgent), []); + // Store hooks const settings = useSettingsStore(); const { handleKeyPress, resetKeyboardState } = useKeyboard(); @@ -147,37 +151,29 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu }, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]); const requestKeyboardLock = useCallback(async () => { - if (videoElm.current === null) return; - - const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); + if (!navigator || !("keyboard" in navigator)) return; - if (isKeyboardLockGranted && navigator && "keyboard" in navigator) { - try { - // @ts-expect-error - keyboard lock is not supported in all browsers - await navigator.keyboard.lock(); - setIsKeyboardLockActive(true); - } catch { - // ignore errors - } + try { + // @ts-expect-error - keyboard lock is not supported in all browsers + await navigator.keyboard.lock(); + console.debug("Keyboard lock acquired"); + setIsKeyboardLockActive(true); + } catch (e) { + console.debug("Keyboard lock not available:", e); } - }, [checkNavigatorPermissions, setIsKeyboardLockActive]); + }, [setIsKeyboardLockActive]); const releaseKeyboardLock = useCallback(async () => { - if ( - fullscreenContainerRef.current === null || - document.fullscreenElement !== fullscreenContainerRef.current - ) - return; + if (!navigator || !("keyboard" in navigator)) return; - if (navigator && "keyboard" in navigator) { - try { - // @ts-expect-error - keyboard unlock is not supported in all browsers - await navigator.keyboard.unlock(); - } catch { - // ignore errors - } - setIsKeyboardLockActive(false); + try { + // @ts-expect-error - keyboard unlock is not supported in all browsers + navigator.keyboard.unlock(); + console.debug("Keyboard lock released"); + } catch { + // ignore errors } + setIsKeyboardLockActive(false); }, [setIsKeyboardLockActive]); useEffect(() => { @@ -206,30 +202,55 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu const requestFullscreen = useCallback(async () => { if (!isFullscreenEnabled || !fullscreenContainerRef.current) return; - // per https://wicg.github.io/keyboard-lock/#system-key-press-handler - // If keyboard lock is activated after fullscreen is already in effect, then the user my - // see multiple messages about how to exit fullscreen. For this reason, we recommend that - // developers call lock() before they enter fullscreen: - await requestKeyboardLock(); await requestPointerLock(); await fullscreenContainerRef.current.requestFullscreen({ navigationUI: "show", }); - }, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]); + // keyboard.lock() is called in the fullscreenchange handler below, + // after fullscreen is confirmed active (required by the API) + }, [isFullscreenEnabled, requestPointerLock]); - // setup to release the keyboard lock anytime the fullscreen ends + // Handle fullscreen enter/exit: acquire or release keyboard lock accordingly useEffect(() => { if (!videoElm.current) return; const handleFullscreenChange = () => { - if (!document.fullscreenElement) { - releaseKeyboardLock(); + if (document.fullscreenElement) { + // Entering fullscreen: always acquire keyboard lock + requestKeyboardLock(); + } else { + // Exiting fullscreen: re-acquire lock if capture mode is on, otherwise release + if (settings.keyboardCaptureMode) { + requestKeyboardLock(); + } else { + releaseKeyboardLock(); + } } }; - document.addEventListener("fullscreenchange", handleFullscreenChange); - }, [releaseKeyboardLock]); + const abortController = new AbortController(); + document.addEventListener("fullscreenchange", handleFullscreenChange, { + signal: abortController.signal, + }); + + return () => { + abortController.abort(); + }; + }, [releaseKeyboardLock, requestKeyboardLock, settings.keyboardCaptureMode]); + + // Sync keyboard lock state with capture mode setting + useEffect( + function syncKeyboardCaptureMode() { + if (settings.keyboardCaptureMode) { + requestKeyboardLock(); + } else if (!document.fullscreenElement) { + // Only release if not in fullscreen (fullscreen manages its own lock) + releaseKeyboardLock(); + } + }, + [settings.keyboardCaptureMode, requestKeyboardLock, releaseKeyboardLock], + ); const absMouseMoveHandler = useMemo( () => @@ -319,30 +340,10 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu } } - // When pressing the meta key + another key, the key will never trigger a keyup - // event, so we need to clear the keys after a short delay - // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 - // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 - if (e.metaKey && hidKey < 0xe0) { - setTimeout(() => { - console.debug(`Forcing the meta key release of associated key: ${hidKey}`); - handleKeyPress(hidKey, false); - }, 10); - } console.debug(`Key down: ${hidKey}`); handleKeyPress(hidKey, true); - - if (!isKeyboardLockActive && hidKey === keys.MetaLeft) { - // If the left meta key was just pressed and we're not keyboard locked - // we'll never see the keyup event because the browser is going to lose - // focus so set a deferred keyup after a short delay - setTimeout(() => { - console.debug(`Forcing the left meta key release`); - handleKeyPress(hidKey, false); - }, 100); - } }, - [handleKeyPress, isKeyboardLockActive, isWindowsClient], + [handleKeyPress, isWindowsClient], ); const keyUpHandler = useCallback( @@ -383,8 +384,15 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu console.debug(`Key up: ${hidKey}`); handleKeyPress(hidKey, false); + + // PiKVM-style fix: When Meta is released on macOS, release all keys to clean up + // stuck companion keys (Chrome doesn't fire their keyup events) + // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 + if (isMacClient && (code === "MetaLeft" || code === "MetaRight")) { + resetKeyboardState(); + } }, - [handleKeyPress, isWindowsClient], + [handleKeyPress, isMacClient, isWindowsClient, resetKeyboardState], ); const videoKeyUpHandler = useCallback((e: KeyboardEvent) => { @@ -547,6 +555,14 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu return true; }, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]); + const showKeyboardCaptureBar = useMemo(() => { + if (!settings.keyboardCaptureMode) return false; + if (isVideoLoading) return false; + if (!isPlaying) return false; + if (videoHeight === 0 || videoWidth === 0) return false; + return true; + }, [settings.keyboardCaptureMode, isPlaying, isVideoLoading, videoHeight, videoWidth]); + const showPointerLockBar = useMemo(() => { if (settings.mouseMode !== "relative") return false; if (!isPointerLockPossible) return false; @@ -602,6 +618,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */} +
void; + isKeyboardLockActive: boolean; + setIsKeyboardLockActive: (active: boolean) => void; + rebootState: { isRebooting: boolean; postRebootAction: PostRebootAction } | null; setRebootState: ( state: { isRebooting: boolean; postRebootAction: PostRebootAction } | null, @@ -103,6 +106,9 @@ export const useUiStore = create(set => ({ setAttachedVirtualKeyboardVisibility: (enabled: boolean) => set({ isAttachedVirtualKeyboardVisible: enabled }), + isKeyboardLockActive: false, + setIsKeyboardLockActive: (active: boolean) => set({ isKeyboardLockActive: active }), + rebootState: null, setRebootState: state => set({ rebootState: state }), })); @@ -360,6 +366,9 @@ export interface SettingsState { showPressedKeys: boolean; setShowPressedKeys: (show: boolean) => void; + keyboardCaptureMode: boolean; + setKeyboardCaptureMode: (enabled: boolean) => void; + // Video enhancement settings videoSaturation: number; setVideoSaturation: (value: number) => void; @@ -406,6 +415,9 @@ export const useSettingsStore = create( showPressedKeys: true, setShowPressedKeys: (show: boolean) => set({ showPressedKeys: show }), + keyboardCaptureMode: false, + setKeyboardCaptureMode: (enabled: boolean) => set({ keyboardCaptureMode: enabled }), + // Video enhancement settings with default values (1.0 = normal) videoSaturation: 1.0, setVideoSaturation: (value: number) => set({ videoSaturation: value }), diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index f6db269b2..3e3256ef6 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -13,6 +13,7 @@ import { m } from "@localizations/messages.js"; export default function SettingsKeyboardRoute() { const { setKeyboardLayout } = useSettingsStore(); const { showPressedKeys, setShowPressedKeys } = useSettingsStore(); + const { keyboardCaptureMode, setKeyboardCaptureMode } = useSettingsStore(); const { selectedKeyboard, keyboardOptions } = useKeyboardLayout(); const { send } = useJsonRpc(); @@ -78,6 +79,18 @@ export default function SettingsKeyboardRoute() { />
+ +
+ + setKeyboardCaptureMode(e.target.checked)} + /> + +
); }