feat: sync keyboard led status (#502)

This commit is contained in:
Aveline
2025-05-23 00:12:18 +02:00
committed by GitHub
parent 0cee284561
commit 0c5c69f2d3
9 changed files with 236 additions and 75 deletions

View File

@@ -36,9 +36,7 @@ export default function InfoBar() {
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
}, [rpcDataChannel]);
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
const isNumLockActive = useHidStore(state => state.isNumLockActive);
const isScrollLockActive = useHidStore(state => state.isScrollLockActive);
const keyboardLedState = useHidStore(state => state.keyboardLedState);
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
@@ -121,7 +119,7 @@ export default function InfoBar() {
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isCapsLockActive
keyboardLedState?.caps_lock
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
@@ -131,7 +129,7 @@ export default function InfoBar() {
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isNumLockActive
keyboardLedState?.num_lock
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
@@ -141,13 +139,23 @@ export default function InfoBar() {
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isScrollLockActive
keyboardLedState?.scroll_lock
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Scroll Lock
</div>
{keyboardLedState?.compose ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Compose
</div>
) : null}
{keyboardLedState?.kana ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Kana
</div>
) : null}
</div>
</div>
</div>

View File

@@ -1,7 +1,8 @@
import { useShallow } from "zustand/react/shallow";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import Keyboard from "react-simple-keyboard";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { motion, AnimatePresence } from "framer-motion";
import Card from "@components/Card";
// eslint-disable-next-line import/order
@@ -9,12 +10,12 @@ import { Button } from "@components/Button";
import "react-simple-keyboard/build/css/index.css";
import { useHidStore, useUiStore } from "@/hooks/stores";
import { cx } from "@/cva.config";
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
import useKeyboard from "@/hooks/useKeyboard";
import DetachIconRaw from "@/assets/detach-icon.svg";
import AttachIconRaw from "@/assets/attach-icon.svg";
import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config";
import { useHidStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
export const DetachIcon = ({ className }: { className?: string }) => {
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
@@ -40,8 +41,8 @@ function KeyboardWrapper() {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
if (!keyboardRef.current) return;
@@ -157,17 +158,11 @@ function KeyboardWrapper() {
toggleLayout();
if (isCapsLockActive) {
setIsCapsLockActive(false);
sendKeyboardEvent([keys["CapsLock"]], []);
return;
}
}
// Handle caps lock state change
if (isKeyCaps) {
setIsCapsLockActive(!isCapsLockActive);
}
// Collect new active keys and modifiers
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : [];
const newModifiers =
@@ -183,7 +178,7 @@ function KeyboardWrapper() {
setTimeout(resetKeyboardState, 100);
},
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState],
);
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);

View File

@@ -55,10 +55,6 @@ export default function WebRTCVideo() {
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
const isVideoLoading = !isPlaying;
// Keyboard related states
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
useHidStore();
// Misc states and hooks
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [send] = useJsonRpc();
@@ -355,10 +351,6 @@ export default function WebRTCVideo() {
// console.log(document.activeElement);
setIsNumLockActive(e.getModifierState("NumLock"));
setIsCapsLockActive(e.getModifierState("CapsLock"));
setIsScrollLockActive(e.getModifierState("ScrollLock"));
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
@@ -388,9 +380,6 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
},
[
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
handleModifierKeys,
sendKeyboardEvent,
],
@@ -401,10 +390,6 @@ export default function WebRTCVideo() {
e.preventDefault();
const prev = useHidStore.getState();
setIsNumLockActive(e.getModifierState("NumLock"));
setIsCapsLockActive(e.getModifierState("CapsLock"));
setIsScrollLockActive(e.getModifierState("ScrollLock"));
// Filtering out the key that was just released (keys[e.code])
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
@@ -417,9 +402,6 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
},
[
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
handleModifierKeys,
sendKeyboardEvent,
],

View File

@@ -405,6 +405,14 @@ export const useMountMediaStore = create<MountMediaState>(set => ({
setErrorMessage: message => set({ errorMessage: message }),
}));
export interface KeyboardLedState {
num_lock: boolean;
caps_lock: boolean;
scroll_lock: boolean;
compose: boolean;
kana: boolean;
}
export interface HidState {
activeKeys: number[];
activeModifiers: number[];
@@ -423,18 +431,12 @@ export interface HidState {
altGrCtrlTime: number; // _altGrCtrlTime
setAltGrCtrlTime: (time: number) => void;
isNumLockActive: boolean;
setIsNumLockActive: (enabled: boolean) => void;
isScrollLockActive: boolean;
setIsScrollLockActive: (enabled: boolean) => void;
keyboardLedState?: KeyboardLedState;
setKeyboardLedState: (state: KeyboardLedState) => void;
isVirtualKeyboardEnabled: boolean;
setVirtualKeyboardEnabled: (enabled: boolean) => void;
isCapsLockActive: boolean;
setIsCapsLockActive: (enabled: boolean) => void;
isPasteModeEnabled: boolean;
setPasteModeEnabled: (enabled: boolean) => void;
@@ -458,18 +460,11 @@ export const useHidStore = create<HidState>(set => ({
altGrCtrlTime: 0,
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
isNumLockActive: false,
setIsNumLockActive: enabled => set({ isNumLockActive: enabled }),
isScrollLockActive: false,
setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }),
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
isVirtualKeyboardEnabled: false,
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
isCapsLockActive: false,
setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }),
isPasteModeEnabled: false,
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),

View File

@@ -19,6 +19,7 @@ import useWebSocket from "react-use-websocket";
import { cx } from "@/cva.config";
import {
HidState,
KeyboardLedState,
NetworkState,
UpdateState,
useDeviceStore,
@@ -586,6 +587,9 @@ export default function KvmIdRoute() {
const setUsbState = useHidStore(state => state.setUsbState);
const setHdmiState = useVideoStore(state => state.setHdmiState);
const keyboardLedState = useHidStore(state => state.keyboardLedState);
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
const [hasUpdated, setHasUpdated] = useState(false);
const { navigateTo } = useDeviceUiNavigation();
@@ -607,6 +611,12 @@ export default function KvmIdRoute() {
setNetworkState(resp.params as NetworkState);
}
if (resp.method === "keyboardLedState") {
const ledState = resp.params as KeyboardLedState;
console.log("Setting keyboard led state", ledState);
setKeyboardLedState(ledState);
}
if (resp.method === "otaState") {
const otaState = resp.params as UpdateState["otaState"];
setOtaState(otaState);
@@ -643,6 +653,18 @@ export default function KvmIdRoute() {
});
}, [rpcDataChannel?.readyState, send, setHdmiState]);
// request keyboard led state from the device
useEffect(() => {
if (rpcDataChannel?.readyState !== "open") return;
if (keyboardLedState !== undefined) return;
console.log("Requesting keyboard led state");
send("getKeyboardLedState", {}, resp => {
if ("error" in resp) return;
console.log("Keyboard led state", resp.result);
setKeyboardLedState(resp.result as KeyboardLedState);
});
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState]);
// When the update is successful, we need to refresh the client javascript and show a success modal
useEffect(() => {
if (queryParams.get("updateSuccess")) {