import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; // Utility function to append stats to a Map const appendStatToMap = ( stat: T, prevMap: Map, maxEntries = 130, ): Map => { if (prevMap.size > maxEntries) { const firstKey = prevMap.keys().next().value; if (firstKey !== undefined) { prevMap.delete(firstKey); } } const date = Math.floor(stat.timestamp / 1000); const newStat = { ...prevMap.get(date), ...stat }; return new Map(prevMap).set(date, newStat); }; // Constants and types export type AvailableSidebarViews = "connection-stats"; export type AvailableTerminalTypes = "kvm" | "serial" | "none"; export interface User { sub: string; email?: string; picture?: string; } interface UserState { user: User | null; setUser: (user: User | null) => void; } interface UIState { sidebarView: AvailableSidebarViews | null; setSidebarView: (view: AvailableSidebarViews | null) => void; disableVideoFocusTrap: boolean; setDisableVideoFocusTrap: (enabled: boolean) => void; isWakeOnLanModalVisible: boolean; setWakeOnLanModalVisibility: (enabled: boolean) => void; toggleSidebarView: (view: AvailableSidebarViews) => void; isAttachedVirtualKeyboardVisible: boolean; setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void; terminalType: AvailableTerminalTypes; setTerminalType: (enabled: UIState["terminalType"]) => void; } export const useUiStore = create(set => ({ terminalType: "none", setTerminalType: type => set({ terminalType: type }), sidebarView: null, setSidebarView: view => set({ sidebarView: view }), disableVideoFocusTrap: false, setDisableVideoFocusTrap: enabled => set({ disableVideoFocusTrap: enabled }), isWakeOnLanModalVisible: false, setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }), toggleSidebarView: view => set(state => { if (state.sidebarView === view) { return { sidebarView: null }; } else { return { sidebarView: view }; } }), isAttachedVirtualKeyboardVisible: true, setAttachedVirtualKeyboardVisibility: enabled => set({ isAttachedVirtualKeyboardVisible: enabled }), })); interface RTCState { peerConnection: RTCPeerConnection | null; setPeerConnection: (pc: RTCState["peerConnection"]) => void; setRpcDataChannel: (channel: RTCDataChannel) => void; rpcDataChannel: RTCDataChannel | null; diskChannel: RTCDataChannel | null; setDiskChannel: (channel: RTCDataChannel) => void; peerConnectionState: RTCPeerConnectionState | null; setPeerConnectionState: (state: RTCPeerConnectionState) => void; transceiver: RTCRtpTransceiver | null; setTransceiver: (transceiver: RTCRtpTransceiver) => void; mediaStream: MediaStream | null; setMediaStream: (stream: MediaStream) => void; videoStreamStats: RTCInboundRtpStreamStats | null; appendVideoStreamStats: (state: RTCInboundRtpStreamStats) => void; videoStreamStatsHistory: Map; isTurnServerInUse: boolean; setTurnServerInUse: (inUse: boolean) => void; inboundRtpStats: Map; appendInboundRtpStats: (state: RTCInboundRtpStreamStats) => void; clearInboundRtpStats: () => void; candidatePairStats: Map; appendCandidatePairStats: (pair: RTCIceCandidatePairStats) => void; clearCandidatePairStats: () => void; // Remote ICE candidates stat type doesn't exist as of today localCandidateStats: Map; appendLocalCandidateStats: (stats: RTCIceCandidateStats) => void; remoteCandidateStats: Map; appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => void; // Disk data channel stats type doesn't exist as of today diskDataChannelStats: Map; appendDiskDataChannelStats: (stat: RTCDataChannelStats) => void; terminalChannel: RTCDataChannel | null; setTerminalChannel: (channel: RTCDataChannel) => void; } export const useRTCStore = create(set => ({ peerConnection: null, setPeerConnection: pc => set({ peerConnection: pc }), rpcDataChannel: null, setRpcDataChannel: channel => set({ rpcDataChannel: channel }), transceiver: null, setTransceiver: transceiver => set({ transceiver }), peerConnectionState: null, setPeerConnectionState: state => set({ peerConnectionState: state }), diskChannel: null, setDiskChannel: channel => set({ diskChannel: channel }), mediaStream: null, setMediaStream: stream => set({ mediaStream: stream }), videoStreamStats: null, appendVideoStreamStats: stats => set({ videoStreamStats: stats }), videoStreamStatsHistory: new Map(), isTurnServerInUse: false, setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }), inboundRtpStats: new Map(), appendInboundRtpStats: newStat => { set(prevState => ({ inboundRtpStats: appendStatToMap(newStat, prevState.inboundRtpStats), })); }, clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }), candidatePairStats: new Map(), appendCandidatePairStats: newStat => { set(prevState => ({ candidatePairStats: appendStatToMap(newStat, prevState.candidatePairStats), })); }, clearCandidatePairStats: () => set({ candidatePairStats: new Map() }), localCandidateStats: new Map(), appendLocalCandidateStats: newStat => { set(prevState => ({ localCandidateStats: appendStatToMap(newStat, prevState.localCandidateStats), })); }, remoteCandidateStats: new Map(), appendRemoteCandidateStats: newStat => { set(prevState => ({ remoteCandidateStats: appendStatToMap(newStat, prevState.remoteCandidateStats), })); }, diskDataChannelStats: new Map(), appendDiskDataChannelStats: newStat => { set(prevState => ({ diskDataChannelStats: appendStatToMap(newStat, prevState.diskDataChannelStats), })); }, // Add these new properties to the store implementation terminalChannel: null, setTerminalChannel: channel => set({ terminalChannel: channel }), })); interface MouseState { mouseX: number; mouseY: number; setMousePosition: (x: number, y: number) => void; } export const useMouseStore = create(set => ({ mouseX: 0, mouseY: 0, setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }), })); export interface VideoState { width: number; height: number; clientWidth: number; clientHeight: number; setClientSize: (width: number, height: number) => void; setSize: (width: number, height: number) => void; hdmiState: "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting"; setHdmiState: (state: { ready: boolean; error?: Extract; }) => void; } export interface BacklightSettings { max_brightness: number; dim_after: number; off_after: number; } export const useVideoStore = create(set => ({ width: 0, height: 0, clientWidth: 0, clientHeight: 0, // The video element's client size setClientSize: (clientWidth, clientHeight) => set({ clientWidth, clientHeight }), // Resolution setSize: (width, height) => set({ width, height }), hdmiState: "connecting", setHdmiState: state => { if (!state) return; const { ready, error } = state; if (ready) { return set({ hdmiState: "ready" }); } else if (error) { return set({ hdmiState: error }); } else { return set({ hdmiState: "connecting" }); } }, })); interface SettingsState { isCursorHidden: boolean; setCursorVisibility: (enabled: boolean) => void; mouseMode: string; setMouseMode: (mode: string) => void; debugMode: boolean; setDebugMode: (enabled: boolean) => void; // Add new developer mode state developerMode: boolean; setDeveloperMode: (enabled: boolean) => void; backlightSettings: BacklightSettings; setBacklightSettings: (settings: BacklightSettings) => void; } export const useSettingsStore = create( persist( set => ({ isCursorHidden: false, setCursorVisibility: enabled => set({ isCursorHidden: enabled }), mouseMode: "absolute", setMouseMode: mode => set({ mouseMode: mode }), debugMode: import.meta.env.DEV, setDebugMode: enabled => set({ debugMode: enabled }), // Add developer mode with default value developerMode: false, setDeveloperMode: enabled => set({ developerMode: enabled }), backlightSettings: { max_brightness: 100, dim_after: 10000, off_after: 50000, }, setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }), }), { name: "settings", storage: createJSONStorage(() => localStorage), }, ), ); export interface DeviceSettingsState { trackpadSensitivity: number; mouseSensitivity: number; clampMin: number; clampMax: number; blockDelay: number; trackpadThreshold: number; scrollSensitivity: "low" | "default" | "high"; setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void; } export const useDeviceSettingsStore = create(set => ({ trackpadSensitivity: 3.0, mouseSensitivity: 5.0, clampMin: -8, clampMax: 8, blockDelay: 25, trackpadThreshold: 10, scrollSensitivity: "default", setScrollSensitivity: sensitivity => { const wheelSettings: Record< DeviceSettingsState["scrollSensitivity"], { trackpadSensitivity: DeviceSettingsState["trackpadSensitivity"]; mouseSensitivity: DeviceSettingsState["mouseSensitivity"]; clampMin: DeviceSettingsState["clampMin"]; clampMax: DeviceSettingsState["clampMax"]; blockDelay: DeviceSettingsState["blockDelay"]; trackpadThreshold: DeviceSettingsState["trackpadThreshold"]; } > = { low: { trackpadSensitivity: 2.0, mouseSensitivity: 3.0, clampMin: -6, clampMax: 6, blockDelay: 30, trackpadThreshold: 10, }, default: { trackpadSensitivity: 3.0, mouseSensitivity: 5.0, clampMin: -8, clampMax: 8, blockDelay: 25, trackpadThreshold: 10, }, high: { trackpadSensitivity: 4.0, mouseSensitivity: 6.0, clampMin: -9, clampMax: 9, blockDelay: 20, trackpadThreshold: 10, }, }; const settings = wheelSettings[sensitivity]; return set({ trackpadSensitivity: settings.trackpadSensitivity, trackpadThreshold: settings.trackpadThreshold, mouseSensitivity: settings.mouseSensitivity, clampMin: settings.clampMin, clampMax: settings.clampMax, blockDelay: settings.blockDelay, scrollSensitivity: sensitivity, }); }, })); export interface RemoteVirtualMediaState { source: "WebRTC" | "HTTP" | "Storage" | null; mode: "CDROM" | "Disk" | null; filename: string | null; url: string | null; path: string | null; size: number | null; } export interface MountMediaState { localFile: File | null; setLocalFile: (file: MountMediaState["localFile"]) => void; remoteVirtualMediaState: RemoteVirtualMediaState | null; setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void; modalView: "mode" | "browser" | "url" | "device" | "upload" | "error" | null; setModalView: (view: MountMediaState["modalView"]) => void; isMountMediaDialogOpen: boolean; setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => void; uploadedFiles: { name: string; size: string; uploadedAt: string }[]; addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) => void; errorMessage: string | null; setErrorMessage: (message: string | null) => void; } export const useMountMediaStore = create(set => ({ localFile: null, setLocalFile: file => set({ localFile: file }), remoteVirtualMediaState: null, setRemoteVirtualMediaState: state => set({ remoteVirtualMediaState: state }), modalView: "mode", setModalView: view => set({ modalView: view }), isMountMediaDialogOpen: false, setIsMountMediaDialogOpen: isOpen => set({ isMountMediaDialogOpen: isOpen }), uploadedFiles: [], addUploadedFile: file => set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), errorMessage: null, setErrorMessage: message => set({ errorMessage: message }), })); export interface HidState { activeKeys: number[]; activeModifiers: number[]; updateActiveKeysAndModifiers: (keysAndModifiers: { keys: number[]; modifiers: number[]; }) => void; altGrArmed: boolean; setAltGrArmed: (armed: boolean) => void; altGrTimer: number | null; // _altGrCtrlTime setAltGrTimer: (timeout: number | null) => void; altGrCtrlTime: number; // _altGrCtrlTime setAltGrCtrlTime: (time: number) => void; isNumLockActive: boolean; setIsNumLockActive: (enabled: boolean) => void; isScrollLockActive: boolean; setIsScrollLockActive: (enabled: boolean) => void; isVirtualKeyboardEnabled: boolean; setVirtualKeyboardEnabled: (enabled: boolean) => void; isCapsLockActive: boolean; setIsCapsLockActive: (enabled: boolean) => void; isPasteModeEnabled: boolean; setPasteModeEnabled: (enabled: boolean) => void; usbState: "configured" | "attached" | "not attached" | "suspended" | "addressed"; setUsbState: (state: HidState["usbState"]) => void; } export const useHidStore = create(set => ({ activeKeys: [], activeModifiers: [], updateActiveKeysAndModifiers: ({ keys, modifiers }) => { return set({ activeKeys: keys, activeModifiers: modifiers }); }, altGrArmed: false, setAltGrArmed: armed => set({ altGrArmed: armed }), altGrTimer: 0, setAltGrTimer: timeout => set({ altGrTimer: timeout }), altGrCtrlTime: 0, setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), isNumLockActive: false, setIsNumLockActive: enabled => set({ isNumLockActive: enabled }), isScrollLockActive: false, setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }), isVirtualKeyboardEnabled: false, setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), isCapsLockActive: false, setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }), isPasteModeEnabled: false, setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), // Add these new properties for USB state usbState: "not attached", setUsbState: state => set({ usbState: state }), })); export const useUserStore = create(set => ({ user: null, setUser: user => set({ user }), })); export interface UpdateState { isUpdatePending: boolean; setIsUpdatePending: (isPending: boolean) => void; updateDialogHasBeenMinimized: boolean; otaState: { updating: boolean; error: string | null; metadataFetchedAt: string | null; // App update appUpdatePending: boolean; appDownloadProgress: number; appDownloadFinishedAt: string | null; appVerificationProgress: number; appVerifiedAt: string | null; appUpdateProgress: number; appUpdatedAt: string | null; // System update systemUpdatePending: boolean; systemDownloadProgress: number; systemDownloadFinishedAt: string | null; systemVerificationProgress: number; systemVerifiedAt: string | null; systemUpdateProgress: number; systemUpdatedAt: string | null; }; setOtaState: (state: UpdateState["otaState"]) => void; setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; modalView: | "loading" | "updating" | "upToDate" | "updateAvailable" | "updateCompleted" | "error"; setModalView: (view: UpdateState["modalView"]) => void; setUpdateErrorMessage: (errorMessage: string) => void; updateErrorMessage: string | null; } export const useUpdateStore = create(set => ({ isUpdatePending: false, setIsUpdatePending: isPending => set({ isUpdatePending: isPending }), setOtaState: state => set({ otaState: state }), otaState: { updating: false, error: null, metadataFetchedAt: null, appUpdatePending: false, systemUpdatePending: false, appDownloadProgress: 0, appDownloadFinishedAt: null, appVerificationProgress: 0, appVerifiedAt: null, systemDownloadProgress: 0, systemDownloadFinishedAt: null, systemVerificationProgress: 0, systemVerifiedAt: null, appUpdateProgress: 0, appUpdatedAt: null, systemUpdateProgress: 0, systemUpdatedAt: null, }, updateDialogHasBeenMinimized: false, setUpdateDialogHasBeenMinimized: hasBeenMinimized => set({ updateDialogHasBeenMinimized: hasBeenMinimized }), modalView: "loading", setModalView: view => set({ modalView: view }), updateErrorMessage: null, setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), })); interface UsbConfigModalState { modalView: "updateUsbConfig" | "updateUsbConfigSuccess"; errorMessage: string | null; setModalView: (view: UsbConfigModalState["modalView"]) => void; setErrorMessage: (message: string | null) => void; } export interface UsbConfigState { vendor_id: string; product_id: string; serial_number: string; manufacturer: string; product: string; } export const useUsbConfigModalStore = create(set => ({ modalView: "updateUsbConfig", errorMessage: null, setModalView: view => set({ modalView: view }), setErrorMessage: message => set({ errorMessage: message }), })); interface LocalAuthModalState { modalView: | "createPassword" | "deletePassword" | "updatePassword" | "creationSuccess" | "deleteSuccess" | "updateSuccess"; setModalView: (view: LocalAuthModalState["modalView"]) => void; } export const useLocalAuthModalStore = create(set => ({ modalView: "createPassword", setModalView: view => set({ modalView: view }), })); export interface DeviceState { appVersion: string | null; systemVersion: string | null; setAppVersion: (version: string) => void; setSystemVersion: (version: string) => void; } export const useDeviceStore = create(set => ({ appVersion: null, systemVersion: null, setAppVersion: version => set({ appVersion: version }), setSystemVersion: version => set({ systemVersion: version }), }));