mirror of https://github.com/jetkvm/kvm.git
Add documentation on the legacy support.
Cleanup react state management to enable upgrading Zustand
This commit is contained in:
parent
a4851f980d
commit
39b39c7b72
|
@ -114,7 +114,7 @@ var defaultConfig = &Config{
|
|||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
DisplayRotation: "270",
|
||||
KeyboardLayout: "en_US",
|
||||
KeyboardLayout: "en-US",
|
||||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
|
|
|
@ -105,14 +105,14 @@ func (u *UsbGadget) updateKeyboardState(state byte) {
|
|||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
if state&^ValidKeyboardLedMasks != 0 {
|
||||
u.log.Error().Uint8("state", state).Msg("ignoring invalid bits")
|
||||
u.log.Warn().Uint8("state", state).Msg("ignoring invalid bits")
|
||||
return
|
||||
}
|
||||
|
||||
if u.keyboardState == state {
|
||||
return
|
||||
}
|
||||
u.log.Trace().Interface("old", u.keyboardState).Interface("new", state).Msg("keyboardState updated")
|
||||
u.log.Trace().Uint8("old", u.keyboardState).Uint8("new", state).Msg("keyboardState updated")
|
||||
u.keyboardState = state
|
||||
|
||||
if u.onKeyboardStateChange != nil {
|
||||
|
@ -131,23 +131,6 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
|||
return getKeyboardState(u.keyboardState)
|
||||
}
|
||||
|
||||
const (
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf Appendix C
|
||||
ModifierMaskLeftControl = 0x01
|
||||
ModifierMaskRightControl = 0x10
|
||||
ModifierMaskLeftShift = 0x02
|
||||
ModifierMaskRightShift = 0x20
|
||||
ModifierMaskLeftAlt = 0x04
|
||||
ModifierMaskRightAlt = 0x40
|
||||
ModifierMaskLeftSuper = 0x08
|
||||
ModifierMaskRightSuper = 0x80
|
||||
|
||||
EitherShiftMask = ModifierMaskLeftShift | ModifierMaskRightShift
|
||||
EitherControlMask = ModifierMaskLeftControl | ModifierMaskRightControl
|
||||
EitherAltMask = ModifierMaskLeftAlt | ModifierMaskRightAlt
|
||||
EitherSuperMask = ModifierMaskLeftSuper | ModifierMaskRightSuper
|
||||
)
|
||||
|
||||
func (u *UsbGadget) GetKeysDownState() KeysDownState {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
@ -310,6 +293,18 @@ const (
|
|||
RightSuper = 0xE7 // Right GUI (e.g. Windows key, Apple Command key)
|
||||
)
|
||||
|
||||
const (
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf Appendix C
|
||||
ModifierMaskLeftControl = 0x01
|
||||
ModifierMaskRightControl = 0x10
|
||||
ModifierMaskLeftShift = 0x02
|
||||
ModifierMaskRightShift = 0x20
|
||||
ModifierMaskLeftAlt = 0x04
|
||||
ModifierMaskRightAlt = 0x40
|
||||
ModifierMaskLeftSuper = 0x08
|
||||
ModifierMaskRightSuper = 0x80
|
||||
)
|
||||
|
||||
// KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup
|
||||
var KeyCodeToMaskMap = map[byte]byte{
|
||||
LeftControl: ModifierMaskLeftControl,
|
||||
|
@ -327,6 +322,11 @@ func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error)
|
|||
defer u.keyboardLock.Unlock()
|
||||
defer u.resetUserInputTime()
|
||||
|
||||
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||
// for handling key presses and releases. It ensures that the USB gadget
|
||||
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||
// in the client/browser-side code in useKeyboard.ts so make sure to keep
|
||||
// them in sync.
|
||||
var state = u.keysDownState
|
||||
modifier := state.Modifier
|
||||
keys := append([]byte(nil), state.Keys...)
|
||||
|
@ -334,7 +334,8 @@ func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error)
|
|||
if mask, exists := KeyCodeToMaskMap[key]; exists {
|
||||
// If the key is a modifier key, we update the keyboardModifier state
|
||||
// by setting or clearing the corresponding bit in the modifier byte.
|
||||
// This allows us to track the state of modifier keys like Shift, Control, Alt, and Super.
|
||||
// This allows us to track the state of dynamic modifier keys like
|
||||
// Shift, Control, Alt, and Super.
|
||||
if press {
|
||||
modifier |= mask
|
||||
} else {
|
||||
|
|
|
@ -26,17 +26,13 @@ export default function Actionbar({
|
|||
requestFullscreen: () => Promise<void>;
|
||||
}) {
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
|
||||
|
||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||
const toggleSidebarView = useUiStore(state => state.toggleSidebarView);
|
||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const terminalType = useUiStore(state => state.terminalType);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const remoteVirtualMediaState = useMountMediaStore(
|
||||
state => state.remoteVirtualMediaState,
|
||||
);
|
||||
const developerMode = useSettingsStore(state => state.developerMode);
|
||||
const { developerMode } = 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
|
||||
|
@ -47,13 +43,13 @@ export default function Actionbar({
|
|||
isOpen.current = open;
|
||||
if (!open) {
|
||||
setTimeout(() => {
|
||||
setDisableFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
console.debug("Popover is closing. Returning focus trap to video");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setDisableFocusTrap],
|
||||
[setDisableVideoFocusTrap],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -81,7 +77,7 @@ export default function Actionbar({
|
|||
text="Paste text"
|
||||
LeadingIcon={MdOutlineContentPasteGo}
|
||||
onClick={() => {
|
||||
setDisableFocusTrap(true);
|
||||
setDisableVideoFocusTrap(true);
|
||||
}}
|
||||
/>
|
||||
</PopoverButton>
|
||||
|
@ -123,7 +119,7 @@ export default function Actionbar({
|
|||
);
|
||||
}}
|
||||
onClick={() => {
|
||||
setDisableFocusTrap(true);
|
||||
setDisableVideoFocusTrap(true);
|
||||
}}
|
||||
/>
|
||||
</PopoverButton>
|
||||
|
@ -154,7 +150,7 @@ export default function Actionbar({
|
|||
theme="light"
|
||||
text="Wake on LAN"
|
||||
onClick={() => {
|
||||
setDisableFocusTrap(true);
|
||||
setDisableVideoFocusTrap(true);
|
||||
}}
|
||||
LeadingIcon={({ className }) => (
|
||||
<svg
|
||||
|
@ -204,7 +200,7 @@ export default function Actionbar({
|
|||
theme="light"
|
||||
text="Virtual Keyboard"
|
||||
LeadingIcon={FaKeyboard}
|
||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -218,7 +214,7 @@ export default function Actionbar({
|
|||
text="Extension"
|
||||
LeadingIcon={LuCable}
|
||||
onClick={() => {
|
||||
setDisableFocusTrap(true);
|
||||
setDisableVideoFocusTrap(true);
|
||||
}}
|
||||
/>
|
||||
</PopoverButton>
|
||||
|
@ -243,7 +239,7 @@ export default function Actionbar({
|
|||
theme="light"
|
||||
text="Virtual Keyboard"
|
||||
LeadingIcon={FaKeyboard}
|
||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function DashboardNavbar({
|
|||
navigate("/");
|
||||
}, [navigate, setUser]);
|
||||
|
||||
const usbState = useHidStore(state => state.usbState);
|
||||
const { usbState } = useHidStore();
|
||||
|
||||
// for testing
|
||||
//userEmail = "user@example.org";
|
||||
|
|
|
@ -2,24 +2,18 @@ import { useEffect, useMemo } from "react";
|
|||
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
HidState,
|
||||
MouseState,
|
||||
RTCState,
|
||||
SettingsState,
|
||||
useHidStore,
|
||||
useMouseStore,
|
||||
useRTCStore,
|
||||
useSettingsStore,
|
||||
useVideoStore,
|
||||
VideoState,
|
||||
VideoState
|
||||
} from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export default function InfoBar() {
|
||||
const keysDownState = useHidStore((state: HidState) => state.keysDownState);
|
||||
const mouseX = useMouseStore((state: MouseState) => state.mouseX);
|
||||
const mouseY = useMouseStore((state: MouseState) => state.mouseY);
|
||||
const mouseMove = useMouseStore((state: MouseState) => state.mouseMove);
|
||||
const { keysDownState } = useHidStore();
|
||||
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
||||
|
||||
const videoClientSize = useVideoStore(
|
||||
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
||||
|
@ -29,10 +23,8 @@ export default function InfoBar() {
|
|||
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
|
||||
);
|
||||
|
||||
const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel);
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const showPressedKeys = useSettingsStore((state: SettingsState) => state.showPressedKeys);
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcDataChannel) return;
|
||||
|
@ -41,11 +33,9 @@ export default function InfoBar() {
|
|||
console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||
}, [rpcDataChannel]);
|
||||
|
||||
const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState);
|
||||
const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse);
|
||||
|
||||
const usbState = useHidStore((state: HidState) => state.usbState);
|
||||
const hdmiState = useVideoStore((state: VideoState) => state.hdmiState);
|
||||
const { keyboardLedState, usbState } = useHidStore();
|
||||
const { isTurnServerInUse } = useRTCStore();
|
||||
const { hdmiState } = useVideoStore();
|
||||
|
||||
const displayKeys = useMemo(() => {
|
||||
if (!showPressedKeys)
|
||||
|
@ -64,21 +54,21 @@ export default function InfoBar() {
|
|||
<div className="flex flex-wrap items-stretch justify-between gap-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
||||
{settings.debugMode ? (
|
||||
{debugMode ? (
|
||||
<div className="flex">
|
||||
<span className="text-xs font-semibold">Resolution:</span>{" "}
|
||||
<span className="text-xs">{videoSize}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{settings.debugMode ? (
|
||||
{debugMode ? (
|
||||
<div className="flex">
|
||||
<span className="text-xs font-semibold">Video Size: </span>
|
||||
<span className="text-xs">{videoClientSize}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(settings.debugMode && settings.mouseMode == "absolute") ? (
|
||||
{(debugMode && mouseMode == "absolute") ? (
|
||||
<div className="flex w-[118px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Pointer:</span>
|
||||
<span className="text-xs">
|
||||
|
@ -87,7 +77,7 @@ export default function InfoBar() {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{(settings.debugMode && settings.mouseMode == "relative") ? (
|
||||
{(debugMode && mouseMode == "relative") ? (
|
||||
<div className="flex w-[118px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Last Move:</span>
|
||||
<span className="text-xs">
|
||||
|
@ -98,13 +88,13 @@ export default function InfoBar() {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{settings.debugMode && (
|
||||
{debugMode && (
|
||||
<div className="flex w-[156px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">USB State:</span>
|
||||
<span className="text-xs">{usbState}</span>
|
||||
</div>
|
||||
)}
|
||||
{settings.debugMode && (
|
||||
{debugMode && (
|
||||
<div className="flex w-[156px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">HDMI State:</span>
|
||||
<span className="text-xs">{hdmiState}</span>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|||
export default function MacroBar() {
|
||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||
const { executeMacro } = useKeyboard();
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
setSendFn(send);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "react-simple-keyboard/build/css/index.css";
|
||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useXTerm } from "react-xtermjs";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
|
@ -65,21 +65,22 @@ function Terminal({
|
|||
readonly dataChannel: RTCDataChannel;
|
||||
readonly type: AvailableTerminalTypes;
|
||||
}) {
|
||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const { terminalType, setTerminalType, setDisableVideoFocusTrap } = useUiStore();
|
||||
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||
|
||||
const isTerminalTypeEnabled = useMemo(() => {
|
||||
return terminalType == type;
|
||||
}, [terminalType, type]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDisableVideoFocusTrap(enableTerminal);
|
||||
setDisableVideoFocusTrap(isTerminalTypeEnabled);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [enableTerminal, setDisableVideoFocusTrap]);
|
||||
}, [setDisableVideoFocusTrap, isTerminalTypeEnabled]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
|
@ -175,9 +176,9 @@ function Terminal({
|
|||
],
|
||||
{
|
||||
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
||||
!enableTerminal,
|
||||
!isTerminalTypeEnabled,
|
||||
"pointer-events-auto -translate-y-[0px] opacity-100 transition duration-300":
|
||||
enableTerminal,
|
||||
isTerminalTypeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -4,9 +4,7 @@ import { cx } from "@/cva.config";
|
|||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import StatusCard from "@components/StatusCards";
|
||||
import { HidState } from "@/hooks/stores";
|
||||
|
||||
type USBStates = HidState["usbState"];
|
||||
import { USBStates } from "@/hooks/stores";
|
||||
|
||||
type StatusProps = Record<
|
||||
USBStates,
|
||||
|
|
|
@ -59,7 +59,7 @@ const usbPresets = [
|
|||
];
|
||||
|
||||
export function UsbDeviceSetting() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [usbDeviceConfig, setUsbDeviceConfig] =
|
||||
|
|
|
@ -54,7 +54,7 @@ const usbConfigs = [
|
|||
type UsbConfigMap = Record<string, USBConfig>;
|
||||
|
||||
export function UsbInfoSetting() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [usbConfigProduct, setUsbConfigProduct] = useState("");
|
||||
|
@ -205,7 +205,7 @@ function USBConfigDialog({
|
|||
product: "",
|
||||
});
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const syncUsbConfig = useCallback(() => {
|
||||
send("getUsbConfig", {}, resp => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import "react-simple-keyboard/build/css/index.css";
|
|||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { HidState, useHidStore, useUiStore } from "@/hooks/stores";
|
||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { keyDisplayMap, keys } from "@/keyboardMappings";
|
||||
|
||||
|
@ -28,17 +28,8 @@ function KeyboardWrapper() {
|
|||
const [layoutName] = useState("default");
|
||||
|
||||
const keyboardRef = useRef<HTMLDivElement>(null);
|
||||
const showAttachedVirtualKeyboard = useUiStore(
|
||||
state => state.isAttachedVirtualKeyboardVisible,
|
||||
);
|
||||
const setShowAttachedVirtualKeyboard = useUiStore(
|
||||
state => state.setAttachedVirtualKeyboardVisibility,
|
||||
);
|
||||
|
||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||
|
||||
const keysDownState = useHidStore((state: HidState) => state.keysDownState);
|
||||
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
|
||||
const { keysDownState, isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||
const { handleKeyPress, executeMacro } = useKeyboard();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
@ -129,23 +120,23 @@ function KeyboardWrapper() {
|
|||
}, [endDrag, onDrag, startDrag]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(key: string) => {
|
||||
async (key: string) => {
|
||||
const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "Meta", "Compose", "Kana"];
|
||||
const dynamicKeys = ["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "MetaLeft", "MetaRight"];
|
||||
|
||||
// handle the fake key-macros we have defined for common combinations
|
||||
if (key === "CtrlAltDelete") {
|
||||
executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "AltMetaEscape") {
|
||||
executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
|
||||
await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "CtrlAltBackspace") {
|
||||
executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -175,7 +166,7 @@ function KeyboardWrapper() {
|
|||
);
|
||||
|
||||
// TODO handle the display of down keys and the layout change for shift/caps lock
|
||||
// const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState.caps_lock));
|
||||
// const { isCapsLockActive } = useShallow(useHidStore());
|
||||
// // Handle toggle of layout for shift or caps lock
|
||||
// const toggleLayout = () => {
|
||||
// setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default"));
|
||||
|
@ -185,11 +176,11 @@ function KeyboardWrapper() {
|
|||
<div
|
||||
className="transition-all duration-500 ease-in-out"
|
||||
style={{
|
||||
marginBottom: virtualKeyboard ? "0px" : `-${350}px`,
|
||||
marginBottom: isVirtualKeyboardEnabled ? "0px" : `-${350}px`,
|
||||
}}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{virtualKeyboard && (
|
||||
{isVirtualKeyboardEnabled && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: "100%" }}
|
||||
animate={{ opacity: 1, y: "0%" }}
|
||||
|
@ -201,30 +192,30 @@ function KeyboardWrapper() {
|
|||
>
|
||||
<div
|
||||
className={cx(
|
||||
!showAttachedVirtualKeyboard
|
||||
!isAttachedVirtualKeyboardVisible
|
||||
? "fixed left-0 top-0 z-50 select-none"
|
||||
: "relative",
|
||||
)}
|
||||
ref={keyboardRef}
|
||||
style={{
|
||||
...(!showAttachedVirtualKeyboard
|
||||
...(!isAttachedVirtualKeyboardVisible
|
||||
? { transform: `translate(${newPosition.x}px, ${newPosition.y}px)` }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={cx("overflow-hidden", {
|
||||
"rounded-none": showAttachedVirtualKeyboard,
|
||||
"rounded-none": isAttachedVirtualKeyboardVisible,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-1 dark:border-b-slate-300/20 dark:bg-slate-800">
|
||||
<div className="absolute left-2 flex items-center gap-x-2">
|
||||
{showAttachedVirtualKeyboard ? (
|
||||
{isAttachedVirtualKeyboardVisible ? (
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Detach"
|
||||
onClick={() => setShowAttachedVirtualKeyboard(false)}
|
||||
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -232,7 +223,7 @@ function KeyboardWrapper() {
|
|||
theme="light"
|
||||
text="Attach"
|
||||
LeadingIcon={AttachIcon}
|
||||
onClick={() => setShowAttachedVirtualKeyboard(true)}
|
||||
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -245,7 +236,7 @@ function KeyboardWrapper() {
|
|||
theme="light"
|
||||
text="Hide"
|
||||
LeadingIcon={ChevronDownIcon}
|
||||
onClick={() => setVirtualKeyboard(false)}
|
||||
onClick={() => setVirtualKeyboardEnabled(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,14 +11,10 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|||
import { cx } from "@/cva.config";
|
||||
import { keys } from "@/keyboardMappings";
|
||||
import {
|
||||
MouseState,
|
||||
RTCState,
|
||||
SettingsState,
|
||||
useMouseStore,
|
||||
useRTCStore,
|
||||
useSettingsStore,
|
||||
useVideoStore,
|
||||
VideoState,
|
||||
} from "@/hooks/stores";
|
||||
|
||||
import {
|
||||
|
@ -31,16 +27,14 @@ import {
|
|||
export default function WebRTCVideo() {
|
||||
// Video and stream related refs and states
|
||||
const videoElm = useRef<HTMLVideoElement>(null);
|
||||
const mediaStream = useRTCStore((state: RTCState) => state.mediaStream);
|
||||
const { mediaStream, peerConnectionState } = useRTCStore();
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState);
|
||||
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
||||
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
||||
// Store hooks
|
||||
const settings = useSettingsStore();
|
||||
const { handleKeyPress, resetKeyboardState } = useKeyboard();
|
||||
const setMousePosition = useMouseStore((state: MouseState) => state.setMousePosition);
|
||||
const setMouseMove = useMouseStore((state: MouseState) => state.setMouseMove);
|
||||
const { setMousePosition, setMouseMove } = useMouseStore();
|
||||
const {
|
||||
setClientSize: setVideoClientSize,
|
||||
setSize: setVideoSize,
|
||||
|
@ -48,18 +42,16 @@ export default function WebRTCVideo() {
|
|||
height: videoHeight,
|
||||
clientWidth: videoClientWidth,
|
||||
clientHeight: videoClientHeight,
|
||||
hdmiState,
|
||||
} = useVideoStore();
|
||||
|
||||
// Video enhancement settings
|
||||
const videoSaturation = useSettingsStore((state: SettingsState) => state.videoSaturation);
|
||||
const videoBrightness = useSettingsStore((state: SettingsState) => state.videoBrightness);
|
||||
const videoContrast = useSettingsStore((state: SettingsState) => state.videoContrast);
|
||||
const { videoSaturation, videoBrightness, videoContrast } = useSettingsStore();
|
||||
|
||||
// RTC related states
|
||||
const peerConnection = useRTCStore((state: RTCState) => state.peerConnection);
|
||||
const { peerConnection } = useRTCStore();
|
||||
|
||||
// HDMI and UI states
|
||||
const hdmiState = useVideoStore((state: VideoState) => state.hdmiState);
|
||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||
const isVideoLoading = !isPlaying;
|
||||
|
||||
|
@ -67,14 +59,14 @@ export default function WebRTCVideo() {
|
|||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||
|
||||
// Misc states and hooks
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
// Video-related
|
||||
const handleResize = useCallback(
|
||||
({ width, height }: { width: number; height: number }) => {
|
||||
( { width, height }: { width: number | undefined; height: number | undefined }) => {
|
||||
if (!videoElm.current) return;
|
||||
// Do something with width and height, e.g.:
|
||||
setVideoClientSize(width, height);
|
||||
setVideoClientSize(width || 0, height || 0);
|
||||
setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight);
|
||||
},
|
||||
[setVideoClientSize, setVideoSize]
|
||||
|
@ -103,7 +95,7 @@ export default function WebRTCVideo() {
|
|||
function updateVideoSizeOnMount() {
|
||||
if (videoElm.current) updateVideoSizeStore(videoElm.current);
|
||||
},
|
||||
[setVideoClientSize, updateVideoSizeStore, setVideoSize],
|
||||
[updateVideoSizeStore],
|
||||
);
|
||||
|
||||
// Pointer lock and keyboard lock related
|
||||
|
@ -447,13 +439,7 @@ export default function WebRTCVideo() {
|
|||
// We set the as early as possible
|
||||
addStreamToVideoElm(mediaStream);
|
||||
},
|
||||
[
|
||||
setVideoClientSize,
|
||||
mediaStream,
|
||||
updateVideoSizeStore,
|
||||
peerConnection,
|
||||
addStreamToVideoElm,
|
||||
],
|
||||
[addStreamToVideoElm, mediaStream],
|
||||
);
|
||||
|
||||
// Setup Keyboard Events
|
||||
|
|
|
@ -23,7 +23,7 @@ export function ATXPowerControl() {
|
|||
> | null>(null);
|
||||
const [atxState, setAtxState] = useState<ATXState | null>(null);
|
||||
|
||||
const [send] = useJsonRpc(function onRequest(resp) {
|
||||
const { send } = useJsonRpc(function onRequest(resp) {
|
||||
if (resp.method === "atxState") {
|
||||
setAtxState(resp.params as ATXState);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ interface DCPowerState {
|
|||
}
|
||||
|
||||
export function DCPowerControl() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [powerState, setPowerState] = useState<DCPowerState | null>(null);
|
||||
|
||||
const getDCPowerState = useCallback(() => {
|
||||
|
|
|
@ -17,7 +17,7 @@ interface SerialSettings {
|
|||
}
|
||||
|
||||
export function SerialConsole() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [settings, setSettings] = useState<SerialSettings>({
|
||||
baudRate: "9600",
|
||||
dataBits: "8",
|
||||
|
@ -49,7 +49,7 @@ export function SerialConsole() {
|
|||
setSettings(newSettings);
|
||||
});
|
||||
};
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const { setTerminalType } = useUiStore();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
|
@ -39,7 +39,7 @@ const AVAILABLE_EXTENSIONS: Extension[] = [
|
|||
];
|
||||
|
||||
export default function ExtensionPopover() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
|
||||
|
||||
// Load active extension on component mount
|
||||
|
|
|
@ -21,8 +21,8 @@ import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
|||
import notifications from "@/notifications";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
|
||||
const [send] = useJsonRpc();
|
||||
const { diskDataChannelStats } = useRTCStore();
|
||||
const { send } = useJsonRpc();
|
||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||
useMountMediaStore();
|
||||
|
||||
|
|
|
@ -25,25 +25,23 @@ const noModifier = 0
|
|||
|
||||
export default function PasteModal() {
|
||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const { setPasteModeEnabled } = useHidStore();
|
||||
const { setDisableVideoFocusTrap } = useUiStore();
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
const { send } = useJsonRpc();
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
|
||||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||
const close = useClose();
|
||||
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
// this ensures we always get the original en_US if it hasn't been set yet
|
||||
const { keyboardLayout, setKeyboardLayout } = useSettingsStore();
|
||||
|
||||
// this ensures we always get the en-US if it hasn't been set yet
|
||||
// and if we get en_US from the backend, we convert it to en-US
|
||||
const safeKeyboardLayout = useMemo(() => {
|
||||
if (keyboardLayout && keyboardLayout.length > 0)
|
||||
return keyboardLayout;
|
||||
return "en_US";
|
||||
return keyboardLayout.replace("en_US", "en-US");
|
||||
return "en-US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -54,13 +52,13 @@ export default function PasteModal() {
|
|||
}, [send, setKeyboardLayout]);
|
||||
|
||||
const onCancelPasteMode = useCallback(() => {
|
||||
setPasteMode(false);
|
||||
setPasteModeEnabled(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
setInvalidChars([]);
|
||||
}, [setDisableVideoFocusTrap, setPasteMode]);
|
||||
}, [setDisableVideoFocusTrap, setPasteModeEnabled]);
|
||||
|
||||
const onConfirmPaste = useCallback(async () => {
|
||||
setPasteMode(false);
|
||||
setPasteModeEnabled(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
|
@ -111,7 +109,7 @@ export default function PasteModal() {
|
|||
);
|
||||
});
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
|
||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteModeEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (TextAreaRef.current) {
|
||||
|
|
|
@ -14,11 +14,9 @@ import AddDeviceForm from "./AddDeviceForm";
|
|||
export default function WakeOnLanModal() {
|
||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { setDisableVideoFocusTrap } = useUiStore();
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { send } = useJsonRpc();
|
||||
const close = useClose();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
||||
|
|
|
@ -37,10 +37,18 @@ function createChartArray<T, K extends keyof T>(
|
|||
}
|
||||
|
||||
export default function ConnectionStatsSidebar() {
|
||||
const inboundRtpStats = useRTCStore(state => state.inboundRtpStats);
|
||||
|
||||
const candidatePairStats = useRTCStore(state => state.candidatePairStats);
|
||||
const setSidebarView = useUiStore(state => state.setSidebarView);
|
||||
const { sidebarView, setSidebarView } = useUiStore();
|
||||
const {
|
||||
mediaStream,
|
||||
peerConnection,
|
||||
inboundRtpStats,
|
||||
appendInboundRtpStats,
|
||||
candidatePairStats,
|
||||
appendCandidatePairStats,
|
||||
appendLocalCandidateStats,
|
||||
appendRemoteCandidateStats,
|
||||
appendDiskDataChannelStats,
|
||||
} = useRTCStore();
|
||||
|
||||
function isMetricSupported<T, K extends keyof T>(
|
||||
stream: Map<number, T>,
|
||||
|
@ -49,20 +57,6 @@ export default function ConnectionStatsSidebar() {
|
|||
return Array.from(stream).some(([, stat]) => stat[metric] !== undefined);
|
||||
}
|
||||
|
||||
const appendInboundRtpStats = useRTCStore(state => state.appendInboundRtpStats);
|
||||
const appendIceCandidatePair = useRTCStore(state => state.appendCandidatePairStats);
|
||||
const appendDiskDataChannelStats = useRTCStore(
|
||||
state => state.appendDiskDataChannelStats,
|
||||
);
|
||||
const appendLocalCandidateStats = useRTCStore(state => state.appendLocalCandidateStats);
|
||||
const appendRemoteCandidateStats = useRTCStore(
|
||||
state => state.appendRemoteCandidateStats,
|
||||
);
|
||||
|
||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
||||
const mediaStream = useRTCStore(state => state.mediaStream);
|
||||
const sidebarView = useUiStore(state => state.sidebarView);
|
||||
|
||||
useInterval(function collectWebRTCStats() {
|
||||
(async () => {
|
||||
if (!mediaStream) return;
|
||||
|
@ -80,8 +74,7 @@ export default function ConnectionStatsSidebar() {
|
|||
successfulLocalCandidateId = report.localCandidateId;
|
||||
successfulRemoteCandidateId = report.remoteCandidateId;
|
||||
}
|
||||
|
||||
appendIceCandidatePair(report);
|
||||
appendCandidatePairStats(report);
|
||||
} else if (report.type === "local-candidate") {
|
||||
// We only want to append the local candidate stats that were used in nominated candidate pair
|
||||
if (successfulLocalCandidateId === report.id) {
|
||||
|
|
|
@ -235,9 +235,12 @@ export const useMouseStore = create<MouseState>(set => ({
|
|||
setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }),
|
||||
}));
|
||||
|
||||
export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
||||
export type HdmiErrorStates = Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">
|
||||
|
||||
export interface HdmiState {
|
||||
ready: boolean;
|
||||
error?: Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">;
|
||||
error?: HdmiErrorStates;
|
||||
}
|
||||
|
||||
export interface VideoState {
|
||||
|
@ -247,19 +250,13 @@ export interface VideoState {
|
|||
clientHeight: number;
|
||||
setClientSize: (width: number, height: number) => void;
|
||||
setSize: (width: number, height: number) => void;
|
||||
hdmiState: "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
||||
hdmiState: HdmiStates;
|
||||
setHdmiState: (state: {
|
||||
ready: boolean;
|
||||
error?: Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">;
|
||||
error?: HdmiErrorStates;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export interface BacklightSettings {
|
||||
max_brightness: number;
|
||||
dim_after: number;
|
||||
off_after: number;
|
||||
}
|
||||
|
||||
export const useVideoStore = create<VideoState>(set => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
@ -288,6 +285,12 @@ export const useVideoStore = create<VideoState>(set => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export interface BacklightSettings {
|
||||
max_brightness: number;
|
||||
dim_after: number;
|
||||
off_after: number;
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
isCursorHidden: boolean;
|
||||
setCursorVisibility: (enabled: boolean) => void;
|
||||
|
@ -444,6 +447,13 @@ export interface KeysDownState {
|
|||
keys: number[];
|
||||
}
|
||||
|
||||
export type USBStates =
|
||||
| "configured"
|
||||
| "attached"
|
||||
| "not attached"
|
||||
| "suspended"
|
||||
| "addressed";
|
||||
|
||||
export interface HidState {
|
||||
keyboardLedState: KeyboardLedState;
|
||||
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||
|
@ -451,8 +461,8 @@ export interface HidState {
|
|||
keysDownState: KeysDownState;
|
||||
setKeysDownState: (state: KeysDownState) => void;
|
||||
|
||||
keyPressAvailable: boolean;
|
||||
setKeyPressAvailable: (available: boolean) => void;
|
||||
keyPressReportApiAvailable: boolean;
|
||||
setkeyPressReportApiAvailable: (available: boolean) => void;
|
||||
|
||||
isVirtualKeyboardEnabled: boolean;
|
||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||
|
@ -460,8 +470,8 @@ export interface HidState {
|
|||
isPasteModeEnabled: boolean;
|
||||
setPasteModeEnabled: (enabled: boolean) => void;
|
||||
|
||||
usbState: "configured" | "attached" | "not attached" | "suspended" | "addressed";
|
||||
setUsbState: (state: HidState["usbState"]) => void;
|
||||
usbState: USBStates;
|
||||
setUsbState: (state: USBStates) => void;
|
||||
}
|
||||
|
||||
export const useHidStore = create<HidState>(set => ({
|
||||
|
@ -471,8 +481,8 @@ export const useHidStore = create<HidState>(set => ({
|
|||
keysDownState: { modifier: 0, keys: [0,0,0,0,0,0] } as KeysDownState,
|
||||
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||
|
||||
keyPressAvailable: true,
|
||||
setKeyPressAvailable: (available: boolean) => set({ keyPressAvailable: available }),
|
||||
keyPressReportApiAvailable: true,
|
||||
setkeyPressReportApiAvailable: (available: boolean) => set({ keyPressReportApiAvailable: available }),
|
||||
|
||||
isVirtualKeyboardEnabled: false,
|
||||
setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }),
|
||||
|
@ -482,7 +492,7 @@ export const useHidStore = create<HidState>(set => ({
|
|||
|
||||
// Add these new properties for USB state
|
||||
usbState: "not attached",
|
||||
setUsbState: (state: HidState["usbState"]) => set({ usbState: state }),
|
||||
setUsbState: (state: USBStates) => set({ usbState: state }),
|
||||
}));
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
|
@ -490,11 +500,15 @@ export const useUserStore = create<UserState>(set => ({
|
|||
setUser: user => set({ user }),
|
||||
}));
|
||||
|
||||
export interface UpdateState {
|
||||
isUpdatePending: boolean;
|
||||
setIsUpdatePending: (isPending: boolean) => void;
|
||||
updateDialogHasBeenMinimized: boolean;
|
||||
otaState: {
|
||||
export type UpdateModalViews =
|
||||
| "loading"
|
||||
| "updating"
|
||||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateCompleted"
|
||||
| "error";
|
||||
|
||||
export interface OtaState {
|
||||
updating: boolean;
|
||||
error: string | null;
|
||||
|
||||
|
@ -523,17 +537,17 @@ export interface UpdateState {
|
|||
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
};
|
||||
setOtaState: (state: UpdateState["otaState"]) => void;
|
||||
};
|
||||
|
||||
export interface UpdateState {
|
||||
isUpdatePending: boolean;
|
||||
setIsUpdatePending: (isPending: boolean) => void;
|
||||
updateDialogHasBeenMinimized: boolean;
|
||||
otaState: OtaState;
|
||||
setOtaState: (state: OtaState) => void;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
modalView:
|
||||
| "loading"
|
||||
| "updating"
|
||||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateCompleted"
|
||||
| "error";
|
||||
setModalView: (view: UpdateState["modalView"]) => void;
|
||||
modalView: UpdateModalViews
|
||||
setModalView: (view: UpdateModalViews) => void;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
updateErrorMessage: string | null;
|
||||
}
|
||||
|
@ -567,15 +581,19 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
|||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||
modalView: "loading",
|
||||
setModalView: (view: UpdateState["modalView"]) => set({ modalView: view }),
|
||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||
updateErrorMessage: null,
|
||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||
}));
|
||||
|
||||
interface UsbConfigModalState {
|
||||
modalView: "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||
export type UsbConfigModalViews =
|
||||
| "updateUsbConfig"
|
||||
| "updateUsbConfigSuccess";
|
||||
|
||||
export interface UsbConfigModalState {
|
||||
modalView: UsbConfigModalViews ;
|
||||
errorMessage: string | null;
|
||||
setModalView: (view: UsbConfigModalState["modalView"]) => void;
|
||||
setModalView: (view: UsbConfigModalViews) => void;
|
||||
setErrorMessage: (message: string | null) => void;
|
||||
}
|
||||
|
||||
|
@ -590,24 +608,26 @@ export interface UsbConfigState {
|
|||
export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
|
||||
modalView: "updateUsbConfig",
|
||||
errorMessage: null,
|
||||
setModalView: (view: UsbConfigModalState["modalView"]) => set({ modalView: view }),
|
||||
setModalView: (view: UsbConfigModalViews) => set({ modalView: view }),
|
||||
setErrorMessage: (message: string | null) => set({ errorMessage: message }),
|
||||
}));
|
||||
|
||||
interface LocalAuthModalState {
|
||||
modalView:
|
||||
| "createPassword"
|
||||
| "deletePassword"
|
||||
| "updatePassword"
|
||||
| "creationSuccess"
|
||||
| "deleteSuccess"
|
||||
| "updateSuccess";
|
||||
setModalView: (view: LocalAuthModalState["modalView"]) => void;
|
||||
export type LocalAuthModalViews =
|
||||
| "createPassword"
|
||||
| "deletePassword"
|
||||
| "updatePassword"
|
||||
| "creationSuccess"
|
||||
| "deleteSuccess"
|
||||
| "updateSuccess";
|
||||
|
||||
export interface LocalAuthModalState {
|
||||
modalView:LocalAuthModalViews;
|
||||
setModalView: (view:LocalAuthModalViews) => void;
|
||||
}
|
||||
|
||||
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
|
||||
modalView: "createPassword",
|
||||
setModalView: (view: LocalAuthModalState["modalView"]) => set({ modalView: view }),
|
||||
setModalView: (view: LocalAuthModalViews) => set({ modalView: view }),
|
||||
}));
|
||||
|
||||
export interface DeviceState {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { RTCState, useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: string;
|
||||
|
@ -33,7 +33,7 @@ const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>(
|
|||
let requestCounter = 0;
|
||||
|
||||
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||
const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel);
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
|
||||
const send = useCallback(
|
||||
(method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
||||
|
@ -45,7 +45,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
|||
|
||||
rpcDataChannel.send(JSON.stringify(payload));
|
||||
},
|
||||
[rpcDataChannel],
|
||||
[rpcDataChannel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -76,7 +76,8 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
|||
return () => {
|
||||
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||
};
|
||||
}, [rpcDataChannel, onRequest]);
|
||||
},
|
||||
[rpcDataChannel, onRequest]);
|
||||
|
||||
return [send];
|
||||
return { send };
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
import { useCallback } from "react";
|
||||
|
||||
import { KeysDownState, HidState, useHidStore, RTCState, useRTCStore, hidKeyBufferSize, hidErrorRollOver } from "@/hooks/stores";
|
||||
import { KeysDownState, useHidStore, useRTCStore, hidKeyBufferSize, hidErrorRollOver } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export default function useKeyboard() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel);
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
|
||||
const keysDownState = useHidStore((state: HidState) => state.keysDownState);
|
||||
const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState);
|
||||
const { keysDownState, setKeysDownState } = useHidStore();
|
||||
|
||||
const keyPressAvailable = useHidStore((state: HidState) => state.keyPressAvailable);
|
||||
const setKeyPressAvailable = useHidStore((state: HidState) => state.setKeyPressAvailable);
|
||||
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
||||
// being tracked on the browser/client-side. When adding the keyPressReport API to the
|
||||
// device-side code, we have to still support the situation where the browser/client-side code
|
||||
// is running on the cloud against a device that has not been updated yet and thus does not
|
||||
// support the keyPressReport API. In that case, we need to handle the key presses locally
|
||||
// and send the full state to the device, so it can behave like a real USB HID keyboard.
|
||||
// This flag indicates whether the keyPressReport API is available on the device which is
|
||||
// dynamically set when the device responds to the first key press event or reports its
|
||||
// keysDownState when queried since the keyPressReport was introduced together with the
|
||||
// getKeysDownState API.
|
||||
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable} = useHidStore();
|
||||
|
||||
// sendKeyboardEvent is used to send the full keyboard state to the device for macro handling and resetting keyboard state.
|
||||
// It sends the keys currently pressed and the modifier state.
|
||||
// The device will respond with the keysDownState if it supports the keyPressReport API
|
||||
// or just accept the state if it does not support (returning no result)
|
||||
const sendKeyboardEvent = useCallback(
|
||||
(state: KeysDownState) => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
@ -24,23 +36,30 @@ export default function useKeyboard() {
|
|||
if ("error" in resp) {
|
||||
console.error(`Failed to send keyboard report ${state}`, resp.error);
|
||||
} else {
|
||||
// If the device supports keyPressReport API, it will (also) return the keysDownState when we send
|
||||
// the keyboardReport
|
||||
const keysDownState = resp.result as KeysDownState;
|
||||
|
||||
if (keysDownState) {
|
||||
// new devices return the keyDownState, so we can use it to update the state
|
||||
setKeysDownState(keysDownState);
|
||||
setKeyPressAvailable(true); // if they returned a keysDownState, we know they also support keyPressReport
|
||||
setKeysDownState(keysDownState); // treat the response as the canonical state
|
||||
setkeyPressReportApiAvailable(true); // if they returned a keysDownState, we ALSO know they also support keyPressReport
|
||||
} else {
|
||||
// old devices do not return the keyDownState, so we just pretend they accepted what we sent
|
||||
setKeysDownState(state);
|
||||
// and we shouldn't set keyPressAvailable here because we don't know if they support it
|
||||
// older devices versions do not return the keyDownState
|
||||
setKeysDownState(state); // we just pretend they accepted what we sent
|
||||
setkeyPressReportApiAvailable(false); // we ALSO know they do not support keyPressReport
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[rpcDataChannel?.readyState, send, setKeyPressAvailable, setKeysDownState],
|
||||
[rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState],
|
||||
);
|
||||
|
||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||
// It sends the key and whether it is pressed or released.
|
||||
// Older device version will not understand this request and will respond with
|
||||
// an error with code -32601, which means that the RPC method name was not recognized.
|
||||
// In that case we will switch to local key handling and update the keysDownState
|
||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||
const sendKeypressEvent = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
@ -48,11 +67,10 @@ export default function useKeyboard() {
|
|||
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
|
||||
send("keypressReport", { key, press }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
// -32601 means the method is not supported
|
||||
// -32601 means the method is not supported because the device is running an older version
|
||||
if (resp.error.code === -32601) {
|
||||
// if we don't support key press report, we need to disable all that handling
|
||||
console.error("Failed calling keypressReport, switching to local handling", resp.error);
|
||||
setKeyPressAvailable(false);
|
||||
console.error("Legacy device does not support keypressReport API, switching to local key down state handling", resp.error);
|
||||
setkeyPressReportApiAvailable(false);
|
||||
} else {
|
||||
console.error(`Failed to send key ${key} press: ${press}`, resp.error);
|
||||
}
|
||||
|
@ -61,14 +79,17 @@ export default function useKeyboard() {
|
|||
|
||||
if (keysDownState) {
|
||||
setKeysDownState(keysDownState);
|
||||
// we don't need to set keyPressAvailable here, because it's already true or we never landed here
|
||||
// we don't need to set keyPressReportApiAvailable here, because it's already true or we never landed here
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[rpcDataChannel?.readyState, send, setKeyPressAvailable, setKeysDownState],
|
||||
[rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState],
|
||||
);
|
||||
|
||||
// resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers.
|
||||
// This is useful for macros and when the browser loses focus to ensure that the keyboard state
|
||||
// is clean.
|
||||
const resetKeyboardState = useCallback(() => {
|
||||
console.debug("Resetting keyboard state");
|
||||
keysDownState.keys.fill(0); // Reset the keys buffer to zeros
|
||||
|
@ -76,6 +97,12 @@ export default function useKeyboard() {
|
|||
sendKeyboardEvent(keysDownState);
|
||||
}, [keysDownState, sendKeyboardEvent]);
|
||||
|
||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||
// The keys and modifiers are pressed together and held for the delay duration.
|
||||
// After the delay, the keys and modifiers are released and the next step is executed.
|
||||
// If a step has no keys or modifiers, it is treated as a delay-only step.
|
||||
// A small pause is added between steps to ensure that the device can process the events.
|
||||
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
|
||||
for (const [index, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
|
@ -99,14 +126,43 @@ export default function useKeyboard() {
|
|||
}
|
||||
};
|
||||
|
||||
// this code exists because we have devices that don't support the keysPress api yet (not current)
|
||||
// so we mirror the device-side code here to keep track of the keyboard state
|
||||
function handleKeyLocally(state: KeysDownState, key: number, press: boolean): KeysDownState {
|
||||
// handleKeyPress is used to handle a key press or release event.
|
||||
// This function handle both key press and key release events.
|
||||
// It checks if the keyPressReport API is available and sends the key press event.
|
||||
// If the keyPressReport API is not available, it simulates the device-side key
|
||||
// handling for legacy devices and updates the keysDownState accordingly.
|
||||
// It then sends the full keyboard state to the device.
|
||||
const handleKeyPress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
||||
if (keyPressReportApiAvailable) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
sendKeypressEvent(key, press);
|
||||
} else {
|
||||
// if the keyPress api is not available, we need to handle the key locally
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(keysDownState, key, press);
|
||||
sendKeyboardEvent(downState); // then we send the full state
|
||||
}
|
||||
},
|
||||
[keyPressReportApiAvailable, keysDownState, rpcDataChannel?.readyState, sendKeyboardEvent, sendKeypressEvent],
|
||||
);
|
||||
|
||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(state: KeysDownState, key: number, press: boolean): KeysDownState {
|
||||
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||
// for handling key presses and releases. It ensures that the USB gadget
|
||||
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||
// in the device-side code in hid_keyboard.go so make sure to keep them in sync.
|
||||
const keys = state.keys;
|
||||
let modifiers = state.modifier;
|
||||
const modifierMask = hidKeyToModifierMask[key] || 0;
|
||||
|
||||
if (modifierMask !== 0) {
|
||||
// If the key is a modifier key, we update the keyboardModifier state
|
||||
// by setting or clearing the corresponding bit in the modifier byte.
|
||||
// This allows us to track the state of dynamic modifier keys like
|
||||
// Shift, Control, Alt, and Super.
|
||||
console.debug(`Handling modifier key: ${key}, press: ${press}, current modifiers: ${modifiers}, modifier mask: ${modifierMask}`);
|
||||
if (press) {
|
||||
modifiers |= modifierMask;
|
||||
|
@ -151,26 +207,5 @@ export default function useKeyboard() {
|
|||
return { modifier: modifiers, keys };
|
||||
}
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
||||
if (keyPressAvailable) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
sendKeypressEvent(key, press);
|
||||
// if keyPress api is STILL available, we don't need to handle the key locally
|
||||
if (keyPressAvailable) return;
|
||||
}
|
||||
|
||||
// if the keyPress api is not available, we need to handle the key locally
|
||||
const downState = handleKeyLocally(keysDownState, key, press);
|
||||
setKeysDownState(downState);
|
||||
|
||||
// then we send the full state
|
||||
sendKeyboardEvent(downState);
|
||||
},
|
||||
[keyPressAvailable, keysDownState, rpcDataChannel?.readyState, sendKeyboardEvent, sendKeypressEvent, setKeysDownState],
|
||||
);
|
||||
|
||||
return { handleKeyPress, resetKeyboardState, executeMacro };
|
||||
}
|
||||
|
|
|
@ -86,7 +86,6 @@ export const keys = {
|
|||
KeyZ: 0x1d,
|
||||
KeypadExclamation: 0xcf,
|
||||
Minus: 0x2d,
|
||||
None: 0x00,
|
||||
NumLock: 0x53, // and Clear
|
||||
Numpad0: 0x62, // and Insert
|
||||
Numpad1: 0x59, // and End
|
||||
|
|
|
@ -64,7 +64,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
setRemoteVirtualMediaState(null);
|
||||
}
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
async function syncRemoteVirtualMediaState() {
|
||||
return new Promise((resolve, reject) => {
|
||||
send("getVirtualMediaState", {}, resp => {
|
||||
|
@ -684,7 +684,7 @@ function DeviceFileView({
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const filesPerPage = 5;
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
interface StorageSpace {
|
||||
bytesUsed: number;
|
||||
|
@ -996,7 +996,7 @@ function UploadFileView({
|
|||
const [fileError, setFileError] = useState<string | null>(null);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const rtcDataChannelRef = useRef<RTCDataChannel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const [isAdopted, setAdopted] = useState(false);
|
||||
const [deviceId, setDeviceId] = useState<string | null>(null);
|
||||
|
@ -166,9 +166,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
|
||||
notifications.success("TLS settings updated successfully");
|
||||
});
|
||||
},
|
||||
[send],
|
||||
);
|
||||
}, [send]);
|
||||
|
||||
// Handle TLS mode change
|
||||
const handleTlsModeChange = (value: string) => {
|
||||
|
|
|
@ -15,10 +15,10 @@ import notifications from "../notifications";
|
|||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const [sshKey, setSSHKey] = useState<string>("");
|
||||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
||||
const { setDeveloperMode } = useSettingsStore();
|
||||
const [devChannel, setDevChannel] = useState(false);
|
||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useDeviceStore } from "../hooks/stores";
|
|||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Button } from "@components/Button";
|
|||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
// This is where we send the RPC to the golang binary
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
const { updateSuccess } = location.state || {};
|
||||
|
||||
const { setModalView, otaState } = useUpdateStore();
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
send("tryUpdate", {});
|
||||
|
@ -134,10 +134,8 @@ function LoadingState({
|
|||
}) {
|
||||
const [progressWidth, setProgressWidth] = useState("0%");
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const setAppVersion = useDeviceStore(state => state.setAppVersion);
|
||||
const setSystemVersion = useDeviceStore(state => state.setSystemVersion);
|
||||
const { send } = useJsonRpc();
|
||||
const { setAppVersion, setSystemVersion } = useDeviceStore();
|
||||
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||
|
|
|
@ -12,10 +12,9 @@ import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
|||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation);
|
||||
const { setDisplayRotation } = useSettingsStore();
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
|
@ -34,7 +33,7 @@ export default function SettingsHardwareRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
|
||||
const { setBacklightSettings } = useSettingsStore();
|
||||
|
||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||
// If the user has set the display to dim after it turns off, set the dim_after
|
||||
|
|
|
@ -12,25 +12,20 @@ import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
|||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
const setShowPressedKeys = useSettingsStore(
|
||||
state => state.setShowPressedKeys,
|
||||
);
|
||||
const { keyboardLayout, setKeyboardLayout } = useSettingsStore();
|
||||
const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
|
||||
|
||||
// this ensures we always get the original en_US if it hasn't been set yet
|
||||
// this ensures we always get the en-US if it hasn't been set yet
|
||||
// and if we get en_US from the backend, we convert it to en-US
|
||||
const safeKeyboardLayout = useMemo(() => {
|
||||
if (keyboardLayout && keyboardLayout.length > 0)
|
||||
return keyboardLayout;
|
||||
return "en_US";
|
||||
return keyboardLayout.replace("en_US", "en-US");
|
||||
return "en-US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = keyboardOptions();
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
|
|
|
@ -66,14 +66,11 @@ const jigglerOptions = [
|
|||
type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom";
|
||||
|
||||
export default function SettingsMouseRoute() {
|
||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||
|
||||
const mouseMode = useSettingsStore(state => state.mouseMode);
|
||||
const setMouseMode = useSettingsStore(state => state.setMouseMode);
|
||||
|
||||
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
|
||||
const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling);
|
||||
const {
|
||||
isCursorHidden, setCursorVisibility,
|
||||
mouseMode, setMouseMode,
|
||||
scrollThrottling, setScrollThrottling
|
||||
} = useSettingsStore();
|
||||
|
||||
const [selectedJigglerOption, setSelectedJigglerOption] =
|
||||
useState<JigglerValues | null>(null);
|
||||
|
@ -86,7 +83,7 @@ export default function SettingsMouseRoute() {
|
|||
{ value: "100", label: "Very High" },
|
||||
];
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const syncJigglerSettings = useCallback(() => {
|
||||
send("getJigglerState", {}, resp => {
|
||||
|
@ -182,8 +179,8 @@ export default function SettingsMouseRoute() {
|
|||
description="Hide the cursor when sending mouse movements"
|
||||
>
|
||||
<Checkbox
|
||||
checked={hideCursor}
|
||||
onChange={e => setHideCursor(e.target.checked)}
|
||||
checked={isCursorHidden}
|
||||
onChange={e => setCursorVisibility(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
|||
}
|
||||
|
||||
export default function SettingsNetworkRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
||||
state,
|
||||
state.setNetworkState,
|
||||
|
|
|
@ -28,7 +28,7 @@ import { cx } from "../cva.config";
|
|||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
export default function SettingsRoute() {
|
||||
const location = useLocation();
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const { setDisableVideoFocusTrap } = useUiStore();
|
||||
const { resetKeyboardState } = useKeyboard();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||
|
|
|
@ -41,18 +41,17 @@ const streamQualityOptions = [
|
|||
];
|
||||
|
||||
export default function SettingsVideoRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { send } = useJsonRpc();
|
||||
const [streamQuality, setStreamQuality] = useState("1");
|
||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
const [edid, setEdid] = useState<string | null>(null);
|
||||
|
||||
// Video enhancement settings from store
|
||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||
const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation);
|
||||
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||
const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness);
|
||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||
const {
|
||||
videoSaturation, setVideoSaturation,
|
||||
videoBrightness, setVideoBrightness,
|
||||
videoContrast, setVideoContrast
|
||||
} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
send("getStreamQualityFactor", {}, resp => {
|
||||
|
|
|
@ -18,15 +18,11 @@ import useWebSocket from "react-use-websocket";
|
|||
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
DeviceState,
|
||||
HidState,
|
||||
KeyboardLedState,
|
||||
KeysDownState,
|
||||
MountMediaState,
|
||||
NetworkState,
|
||||
RTCState,
|
||||
UIState,
|
||||
UpdateState,
|
||||
OtaState,
|
||||
USBStates,
|
||||
useDeviceStore,
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
|
@ -132,22 +128,22 @@ export default function KvmIdRoute() {
|
|||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||
|
||||
const params = useParams() as { id: string };
|
||||
const sidebarView = useUiStore((state: UIState) => state.sidebarView);
|
||||
const [queryParams, setQueryParams] = useSearchParams();
|
||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||
const [ queryParams, setQueryParams ] = useSearchParams();
|
||||
|
||||
const {
|
||||
peerConnection, setPeerConnection,
|
||||
peerConnectionState, setPeerConnectionState,
|
||||
diskChannel, setDiskChannel,
|
||||
setMediaStream,
|
||||
setRpcDataChannel,
|
||||
isTurnServerInUse, setTurnServerInUse,
|
||||
rpcDataChannel,
|
||||
setTransceiver
|
||||
} = useRTCStore();
|
||||
|
||||
const setIsTurnServerInUse = useRTCStore((state: RTCState) => state.setTurnServerInUse);
|
||||
const peerConnection = useRTCStore((state: RTCState) => state.peerConnection);
|
||||
const setPeerConnectionState = useRTCStore((state: RTCState) => state.setPeerConnectionState);
|
||||
const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState);
|
||||
const setMediaMediaStream = useRTCStore((state: RTCState) => state.setMediaStream);
|
||||
const setPeerConnection = useRTCStore((state: RTCState) => state.setPeerConnection);
|
||||
const setDiskChannel = useRTCStore((state: RTCState) => state.setDiskChannel);
|
||||
const setRpcDataChannel = useRTCStore((state: RTCState) => state.setRpcDataChannel);
|
||||
const setTransceiver = useRTCStore((state: RTCState) => state.setTransceiver);
|
||||
const location = useLocation();
|
||||
|
||||
const isLegacySignalingEnabled = useRef(false);
|
||||
|
||||
const [connectionFailed, setConnectionFailed] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
@ -480,7 +476,7 @@ export default function KvmIdRoute() {
|
|||
};
|
||||
|
||||
pc.ontrack = function (event) {
|
||||
setMediaMediaStream(event.streams[0]);
|
||||
setMediaStream(event.streams[0]);
|
||||
};
|
||||
|
||||
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
||||
|
@ -502,7 +498,7 @@ export default function KvmIdRoute() {
|
|||
legacyHTTPSignaling,
|
||||
sendWebRTCSignal,
|
||||
setDiskChannel,
|
||||
setMediaMediaStream,
|
||||
setMediaStream,
|
||||
setPeerConnection,
|
||||
setPeerConnectionState,
|
||||
setRpcDataChannel,
|
||||
|
@ -517,9 +513,7 @@ export default function KvmIdRoute() {
|
|||
}, [peerConnectionState, cleanupAndStopReconnecting]);
|
||||
|
||||
// Cleanup effect
|
||||
const clearInboundRtpStats = useRTCStore((state: RTCState) => state.clearInboundRtpStats);
|
||||
const clearCandidatePairStats = useRTCStore((state: RTCState) => state.clearCandidatePairStats);
|
||||
const setSidebarView = useUiStore((state: UIState) => state.setSidebarView);
|
||||
const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -550,11 +544,10 @@ export default function KvmIdRoute() {
|
|||
if (!lastRemoteStat?.length) return;
|
||||
const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here
|
||||
|
||||
setIsTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
|
||||
}, [peerConnectionState, setIsTurnServerInUse]);
|
||||
setTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
|
||||
}, [peerConnectionState, setTurnServerInUse]);
|
||||
|
||||
// TURN server usage reporting
|
||||
const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse);
|
||||
const lastBytesReceived = useRef<number>(0);
|
||||
const lastBytesSent = useRef<number>(0);
|
||||
|
||||
|
@ -587,17 +580,13 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, 10000);
|
||||
|
||||
const setNetworkState = useNetworkStateStore((state: NetworkState) => state.setNetworkState);
|
||||
|
||||
const setUsbState = useHidStore((state: HidState) => state.setUsbState);
|
||||
const setHdmiState = useVideoStore((state: VideoState) => state.setHdmiState);
|
||||
|
||||
const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState);
|
||||
const setKeyboardLedState = useHidStore((state: HidState) => state.setKeyboardLedState);
|
||||
|
||||
const keysDownState = useHidStore((state: HidState) => state.keysDownState);
|
||||
const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState);
|
||||
const setKeyPressAvailable = useHidStore((state: HidState) => state.setKeyPressAvailable);
|
||||
const { setNetworkState} = useNetworkStateStore();
|
||||
const { setHdmiState } = useVideoStore();
|
||||
const {
|
||||
keyboardLedState, setKeyboardLedState,
|
||||
keysDownState, setKeysDownState, setUsbState,
|
||||
setkeyPressReportApiAvailable
|
||||
} = useHidStore();
|
||||
|
||||
const [hasUpdated, setHasUpdated] = useState(false);
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
@ -608,7 +597,7 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
if (resp.method === "usbState") {
|
||||
setUsbState(resp.params as unknown as HidState["usbState"]);
|
||||
setUsbState(resp.params as unknown as USBStates);
|
||||
}
|
||||
|
||||
if (resp.method === "videoInputState") {
|
||||
|
@ -632,12 +621,12 @@ export default function KvmIdRoute() {
|
|||
if (downState) {
|
||||
console.debug("Setting key down state:", downState);
|
||||
setKeysDownState(downState);
|
||||
setKeyPressAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||
setkeyPressReportApiAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.method === "otaState") {
|
||||
const otaState = resp.params as UpdateState["otaState"];
|
||||
const otaState = resp.params as OtaState;
|
||||
setOtaState(otaState);
|
||||
|
||||
if (otaState.updating === true) {
|
||||
|
@ -661,8 +650,7 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
}
|
||||
|
||||
const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel);
|
||||
const [send] = useJsonRpc(onJsonRpcRequest);
|
||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||
|
||||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
@ -709,7 +697,7 @@ export default function KvmIdRoute() {
|
|||
if (resp.error.code === -32601) {
|
||||
// if we don't support key down state, we know key press is also not available
|
||||
console.error("Failed to get key down state, switching to old-school", resp.error);
|
||||
setKeyPressAvailable(false);
|
||||
setkeyPressReportApiAvailable(false);
|
||||
} else {
|
||||
console.error("Failed to get key down state", resp.error);
|
||||
}
|
||||
|
@ -719,12 +707,12 @@ export default function KvmIdRoute() {
|
|||
if (downState) {
|
||||
console.debug("Keyboard key down state", downState);
|
||||
setKeysDownState(downState);
|
||||
setKeyPressAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||
setkeyPressReportApiAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||
}
|
||||
}
|
||||
setNeedKeyDownState(false);
|
||||
});
|
||||
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeyPressAvailable, setKeysDownState]);
|
||||
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState]);
|
||||
|
||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||
useEffect(() => {
|
||||
|
@ -733,14 +721,13 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||
|
||||
const diskChannel = useRTCStore((state: RTCState) => state.diskChannel)!;
|
||||
const file = useMountMediaStore((state: MountMediaState) => state.localFile)!;
|
||||
const { localFile } = useMountMediaStore();
|
||||
useEffect(() => {
|
||||
if (!diskChannel || !file) return;
|
||||
if (!diskChannel || !localFile) return;
|
||||
diskChannel.onmessage = async e => {
|
||||
console.debug("Received", e.data);
|
||||
const data = JSON.parse(e.data);
|
||||
const blob = file.slice(data.start, data.end);
|
||||
const blob = localFile.slice(data.start, data.end);
|
||||
const buf = await blob.arrayBuffer();
|
||||
const header = new ArrayBuffer(16);
|
||||
const headerView = new DataView(header);
|
||||
|
@ -751,11 +738,9 @@ export default function KvmIdRoute() {
|
|||
fullData.set(new Uint8Array(buf), header.byteLength);
|
||||
diskChannel.send(fullData);
|
||||
};
|
||||
}, [diskChannel, file]);
|
||||
}, [diskChannel, localFile]);
|
||||
|
||||
// System update
|
||||
const disableVideoFocusTrap = useUiStore((state: UIState) => state.disableVideoFocusTrap);
|
||||
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
|
||||
|
@ -775,9 +760,7 @@ export default function KvmIdRoute() {
|
|||
if (location.pathname !== "/other-session") navigateTo("/");
|
||||
}, [navigateTo, location.pathname]);
|
||||
|
||||
const appVersion = useDeviceStore((state: DeviceState) => state.appVersion);
|
||||
const setAppVersion = useDeviceStore((state: DeviceState) => state.setAppVersion);
|
||||
const setSystemVersion = useDeviceStore((state: DeviceState) => state.setSystemVersion);
|
||||
const { appVersion, setAppVersion, setSystemVersion} = useDeviceStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (appVersion) return;
|
||||
|
|
Loading…
Reference in New Issue