Compare commits

..

6 Commits

Author SHA1 Message Date
Silke pilon 25e476e715 Merge branch 'jetkvm:dev' into feat-mac-text-wait-delay 2025-09-21 15:50:04 +02:00
Silke pilon 8dd004ab54 feat(macros add import/export, sanitize imports, and refactor
- add buildDownloadFilename and pad2 helpers to consistently
  generate safe timestamped filenames for macro downloads
- extract macro download logic into handleDownloadMacro and wire up
  Download button to use it
- refactor normalizeSortOrders to a concise one-liner
- introduce sanitizeImportedStep and sanitizeImportedMacro to validate
  imported JSON, enforce types, default values, and limit name length,
  preventing malformed data from corrupting store
- generate new IDs for imported macros and ensure correct sortOrder
- update Memo dependencies to include handleDownloadMacro

These changes enable reliable macro export/import with sanitized
inputs, improve code clarity by extracting utilities, and prevent
issues from malformed external files.
2025-09-21 15:49:48 +02:00
Silke pilon 9f27a5d5c3 feat(macros): add text/wait step types and improve delays
Lower minimum step delay to 10ms to allow finer-grained macro timing.
Introduce optional "text" and "wait" fields on macro steps (Go and
TypeScript types, JSON-RPC parsing) so steps can either type text using
the selected keyboard layout or act as explicit wait-only pauses.

Implement client-side expansion of text steps into per-character key
events (handling shift, AltRight, dead/accent keys and trailing space)
and wire expansion into both remote and client-side macro execution.
Adjust macro execution logic to treat wait steps as no-op delays and
ensure key press followed by explicit release delay is sent for typed
keys.

These changes enable richer macro semantics (text composition and
explicit waits) and more responsive timing control.
2025-09-21 15:49:48 +02:00
Aveline 83caa8f82d
feat: get local version only (#813) 2025-09-19 13:45:59 +02:00
Adam Shiervani 27750b9cc2
feat: re-add keyboard and keypress report handlers to RPC (#811) 2025-09-18 17:33:08 +02:00
Adam Shiervani 5112bef19c
fix: remove unnecessary grow-0 utility from in keyboard (#810) 2025-09-18 17:02:08 +02:00
6 changed files with 109 additions and 57 deletions

View File

@ -282,6 +282,17 @@ func rpcGetUpdateStatus() (*UpdateStatus, error) {
return updateStatus, nil
}
func rpcGetLocalVersion() (*LocalMetadata, error) {
systemVersion, appVersion, err := GetLocalVersion()
if err != nil {
return nil, fmt.Errorf("error getting local version: %w", err)
}
return &LocalMetadata{
AppVersion: appVersion.String(),
SystemVersion: systemVersion.String(),
}, nil
}
func rpcTryUpdate() error {
includePreRelease := config.IncludePreRelease
go func() {
@ -1177,6 +1188,8 @@ var rpcHandlers = map[string]RPCHandler{
"renewDHCPLease": {Func: rpcRenewDHCPLease},
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
"getKeyDownState": {Func: rpcGetKeysDownState},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
@ -1198,6 +1211,7 @@ var rpcHandlers = map[string]RPCHandler{
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion},
"getUpdateStatus": {Func: rpcGetUpdateStatus},
"tryUpdate": {Func: rpcTryUpdate},
"getDevModeState": {Func: rpcGetDevModeState},

View File

@ -29,6 +29,8 @@ export interface JsonRpcErrorResponse {
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
export const RpcMethodNotFound = -32601;
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
let requestCounter = 0;

View File

@ -0,0 +1,79 @@
import { useCallback } from "react";
import { useDeviceStore } from "@/hooks/stores";
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
export interface VersionInfo {
appVersion: string;
systemVersion: string;
}
export interface SystemVersionInfo {
local: VersionInfo;
remote?: VersionInfo;
systemUpdateAvailable: boolean;
appUpdateAvailable: boolean;
error?: string;
}
export function useVersion() {
const {
appVersion,
systemVersion,
setAppVersion,
setSystemVersion,
} = useDeviceStore();
const { send } = useJsonRpc();
const getVersionInfo = useCallback(() => {
return new Promise<SystemVersionInfo>((resolve, reject) => {
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Failed to check for updates: ${resp.error}`);
reject(new Error("Failed to check for updates"));
} else {
const result = resp.result as SystemVersionInfo;
setAppVersion(result.local.appVersion);
setSystemVersion(result.local.systemVersion);
if (result.error) {
notifications.error(`Failed to check for updates: ${result.error}`);
reject(new Error("Failed to check for updates"));
} else {
resolve(result);
}
}
});
});
}, [send, setAppVersion, setSystemVersion]);
const getLocalVersion = useCallback(() => {
return new Promise<VersionInfo>((resolve, reject) => {
send("getLocalVersion", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.log(resp.error)
if (resp.error.code === RpcMethodNotFound) {
console.warn("Failed to get device version, using legacy version");
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
}
console.error("Failed to get device version N", resp.error);
notifications.error(`Failed to get device version: ${resp.error}`);
reject(new Error("Failed to get device version"));
} else {
const result = resp.result as VersionInfo;
setAppVersion(result.appVersion);
setSystemVersion(result.systemVersion);
resolve(result);
}
});
});
}, [send, setAppVersion, setSystemVersion, getVersionInfo]);
return {
getVersionInfo,
getLocalVersion,
appVersion,
systemVersion,
};
}

View File

@ -239,7 +239,7 @@ video::-webkit-media-controls {
}
.simple-keyboard-arrows .hg-button {
@apply flex w-[50px] grow-0 items-center justify-center;
@apply flex w-[50px] items-center justify-center;
}
.controlArrows {
@ -264,7 +264,7 @@ video::-webkit-media-controls {
}
.simple-keyboard-control .hg-button {
@apply flex w-[50px] grow-0 items-center justify-center;
@apply flex w-[50px] items-center justify-center;
}
.numPad {
@ -332,7 +332,7 @@ video::-webkit-media-controls {
.keyboard-detached .simple-keyboard.hg-theme-default div.hg-button {
text-wrap: auto;
text-align: center;
text-align: center;
min-width: 6ch;
}
.keyboard-detached .simple-keyboard.hg-theme-default .hg-button span {

View File

@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
import { UpdateState, useDeviceStore, useUpdateStore } from "@/hooks/stores";
import notifications from "@/notifications";
import { UpdateState, useUpdateStore } from "@/hooks/stores";
import LoadingSpinner from "@/components/LoadingSpinner";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { SystemVersionInfo, useVersion } from "@/hooks/useVersion";
export default function SettingsGeneralUpdateRoute() {
const navigate = useNavigate();
@ -41,13 +41,7 @@ export default function SettingsGeneralUpdateRoute() {
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export interface SystemVersionInfo {
local: { appVersion: string; systemVersion: string };
remote?: { appVersion: string; systemVersion: string };
systemUpdateAvailable: boolean;
appUpdateAvailable: boolean;
error?: string;
}
export function Dialog({
onClose,
@ -134,30 +128,8 @@ function LoadingState({
}) {
const [progressWidth, setProgressWidth] = useState("0%");
const abortControllerRef = useRef<AbortController | null>(null);
const { setAppVersion, setSystemVersion } = useDeviceStore();
const { send } = useJsonRpc();
const getVersionInfo = useCallback(() => {
return new Promise<SystemVersionInfo>((resolve, reject) => {
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Failed to check for updates: ${resp.error}`);
reject(new Error("Failed to check for updates"));
} else {
const result = resp.result as SystemVersionInfo;
setAppVersion(result.local.appVersion);
setSystemVersion(result.local.systemVersion);
if (result.error) {
notifications.error(`Failed to check for updates: ${result.error}`);
reject(new Error("Failed to check for updates"));
} else {
resolve(result);
}
}
});
});
}, [send, setAppVersion, setSystemVersion]);
const { getVersionInfo } = useVersion();
const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => {

View File

@ -19,14 +19,12 @@ import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "@/api";
import { checkAuth, isInCloud, isOnDevice } from "@/main";
import { cx } from "@/cva.config";
import notifications from "@/notifications";
import {
KeyboardLedState,
KeysDownState,
NetworkState,
OtaState,
USBStates,
useDeviceStore,
useHidStore,
useNetworkStateStore,
User,
@ -42,7 +40,7 @@ const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectio
const Terminal = lazy(() => import('@components/Terminal'));
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
import Modal from "@/components/Modal";
import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import {
ConnectionFailedOverlay,
LoadingConnectionOverlay,
@ -51,7 +49,7 @@ import {
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local";
import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update";
import { useVersion } from "@/hooks/useVersion";
interface LocalLoaderResp {
authMode: "password" | "noPassword" | null;
@ -715,7 +713,7 @@ export default function KvmIdRoute() {
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
// -32601 means the method is not supported
if (resp.error.code === -32601) {
if (resp.error.code === RpcMethodNotFound) {
// if we don't support key down state, we know key press is also not available
console.warn("Failed to get key down state, switching to old-school", resp.error);
setHidRpcDisabled(true);
@ -758,26 +756,13 @@ export default function KvmIdRoute() {
if (location.pathname !== "/other-session") navigateTo("/");
}, [navigateTo, location.pathname]);
const { appVersion, setAppVersion, setSystemVersion} = useDeviceStore();
const { appVersion, getLocalVersion} = useVersion();
useEffect(() => {
if (appVersion) return;
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Failed to get device version: ${resp.error}`);
return
}
const result = resp.result as SystemVersionInfo;
if (result.error) {
notifications.error(`Failed to get device version: ${result.error}`);
}
setAppVersion(result.local.appVersion);
setSystemVersion(result.local.systemVersion);
});
}, [appVersion, send, setAppVersion, setSystemVersion]);
getLocalVersion();
}, [appVersion, getLocalVersion]);
const ConnectionStatusElement = useMemo(() => {
const hasConnectionFailed =