mirror of https://github.com/jetkvm/kvm.git
refactor: consolidate sleep utility and update usages across components
This commit is contained in:
parent
1afd61e687
commit
5f394d8e9f
|
|
@ -9,6 +9,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
|||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import notifications from "@/notifications";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
export interface USBConfig {
|
||||
vendor_id: string;
|
||||
|
|
@ -108,7 +109,7 @@ export function UsbDeviceSetting() {
|
|||
}
|
||||
|
||||
// We need some time to ensure the USB devices are updated
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await sleep(2000);
|
||||
setLoading(false);
|
||||
syncUsbDeviceConfig();
|
||||
notifications.success(m.usb_device_updated());
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
|||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
|
||||
|
||||
|
|
@ -123,7 +124,7 @@ export function UsbInfoSetting() {
|
|||
}
|
||||
|
||||
// We need some time to ensure the USB devices are updated
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await sleep(2000);
|
||||
setLoading(false);
|
||||
notifications.success(
|
||||
m.usb_config_set_success({ manufacturer: usbConfig.manufacturer, product: usbConfig.product }),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
const MACRO_RESET_KEYBOARD_STATE = {
|
||||
keys: new Array(hidKeyBufferSize).fill(0),
|
||||
|
|
@ -31,8 +32,6 @@ export interface MacroStep {
|
|||
|
||||
export type MacroSteps = MacroStep[];
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export default function useKeyboard() {
|
||||
const { send } = useJsonRpc();
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
|
|
@ -97,24 +96,23 @@ export default function useKeyboard() {
|
|||
[send, setKeysDownState],
|
||||
);
|
||||
|
||||
const sendKeystrokeLegacy = useCallback(async (keys: number[], modifier: number, ac?: AbortController) => {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
const abortListener = () => {
|
||||
reject(new Error("Keyboard report aborted"));
|
||||
};
|
||||
const sendKeystrokeLegacy = useCallback(
|
||||
async (keys: number[], modifier: number, ac?: AbortController) => {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
const abortListener = () => {
|
||||
reject(new Error("Keyboard report aborted"));
|
||||
};
|
||||
|
||||
ac?.signal?.addEventListener("abort", abortListener);
|
||||
ac?.signal?.addEventListener("abort", abortListener);
|
||||
|
||||
send(
|
||||
"keyboardReport",
|
||||
{ keys, modifier },
|
||||
params => {
|
||||
send("keyboardReport", { keys, modifier }, params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}, [send]);
|
||||
});
|
||||
});
|
||||
},
|
||||
[send],
|
||||
);
|
||||
|
||||
const KEEPALIVE_INTERVAL = 50;
|
||||
|
||||
|
|
@ -149,7 +147,6 @@ export default function useKeyboard() {
|
|||
}
|
||||
}, [rpcHidReady, sendKeyboardEventHidRpc, handleLegacyKeyboardReport, cancelKeepAlive]);
|
||||
|
||||
|
||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
state: KeysDownState,
|
||||
|
|
@ -200,7 +197,9 @@ export default function useKeyboard() {
|
|||
// If we reach here it means we didn't find an empty slot or the key in the buffer
|
||||
if (overrun) {
|
||||
if (press) {
|
||||
console.warn(`keyboard buffer overflow current keys ${keys}, key: ${key} not added`);
|
||||
console.warn(
|
||||
`keyboard buffer overflow current keys ${keys}, key: ${key} not added`,
|
||||
);
|
||||
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow
|
||||
keys.length = hidKeyBufferSize;
|
||||
keys.fill(hidErrorRollOver);
|
||||
|
|
@ -284,85 +283,92 @@ export default function useKeyboard() {
|
|||
// 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 executeMacroRemote = useCallback(async (
|
||||
steps: MacroSteps,
|
||||
) => {
|
||||
const macro: KeyboardMacroStep[] = [];
|
||||
const executeMacroRemote = useCallback(
|
||||
async (steps: MacroSteps) => {
|
||||
const macro: KeyboardMacroStep[] = [];
|
||||
|
||||
for (const [_, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
for (const [_, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
|
||||
.map(mod => modifiers[mod])
|
||||
.map(mod => modifiers[mod])
|
||||
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
macro.push({ keys: keyValues, modifier: modifierMask, delay: 20 });
|
||||
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
|
||||
}
|
||||
}
|
||||
|
||||
sendKeyboardMacroEventHidRpc(macro);
|
||||
}, [sendKeyboardMacroEventHidRpc]);
|
||||
|
||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||
const promises: (() => Promise<void>)[] = [];
|
||||
|
||||
const ac = new AbortController();
|
||||
setAbortController(ac);
|
||||
|
||||
for (const [_, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
.map(mod => modifiers[mod])
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
promises.push(() => sendKeystrokeLegacy(keyValues, modifierMask, ac));
|
||||
promises.push(() => resetKeyboardState());
|
||||
promises.push(() => sleep(step.delay || 100));
|
||||
}
|
||||
}
|
||||
|
||||
const runAll = async () => {
|
||||
for (const promise of promises) {
|
||||
// Check if we've been aborted before executing each promise
|
||||
if (ac.signal.aborted) {
|
||||
throw new Error("Macro execution aborted");
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
macro.push({ keys: keyValues, modifier: modifierMask, delay: 20 });
|
||||
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
|
||||
}
|
||||
await promise();
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Macro execution aborted"));
|
||||
sendKeyboardMacroEventHidRpc(macro);
|
||||
},
|
||||
[sendKeyboardMacroEventHidRpc],
|
||||
);
|
||||
|
||||
const executeMacroClientSide = useCallback(
|
||||
async (steps: MacroSteps) => {
|
||||
const promises: (() => Promise<void>)[] = [];
|
||||
|
||||
const ac = new AbortController();
|
||||
setAbortController(ac);
|
||||
|
||||
for (const [_, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
.map(mod => modifiers[mod])
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
promises.push(() => sendKeystrokeLegacy(keyValues, modifierMask, ac));
|
||||
promises.push(() => resetKeyboardState());
|
||||
promises.push(() => sleep(step.delay || 100));
|
||||
}
|
||||
}
|
||||
|
||||
const runAll = async () => {
|
||||
for (const promise of promises) {
|
||||
// Check if we've been aborted before executing each promise
|
||||
if (ac.signal.aborted) {
|
||||
throw new Error("Macro execution aborted");
|
||||
}
|
||||
await promise();
|
||||
}
|
||||
};
|
||||
|
||||
ac.signal.addEventListener("abort", abortListener);
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Macro execution aborted"));
|
||||
};
|
||||
|
||||
runAll()
|
||||
.then(() => {
|
||||
ac.signal.removeEventListener("abort", abortListener);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
ac.signal.removeEventListener("abort", abortListener);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
|
||||
ac.signal.addEventListener("abort", abortListener);
|
||||
|
||||
const executeMacro = useCallback(async (steps: MacroSteps) => {
|
||||
if (rpcHidReady) {
|
||||
return executeMacroRemote(steps);
|
||||
}
|
||||
return executeMacroClientSide(steps);
|
||||
}, [rpcHidReady, executeMacroRemote, executeMacroClientSide]);
|
||||
runAll()
|
||||
.then(() => {
|
||||
ac.signal.removeEventListener("abort", abortListener);
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
ac.signal.removeEventListener("abort", abortListener);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
[sendKeystrokeLegacy, resetKeyboardState, setAbortController],
|
||||
);
|
||||
|
||||
const executeMacro = useCallback(
|
||||
async (steps: MacroSteps) => {
|
||||
if (rpcHidReady) {
|
||||
return executeMacroRemote(steps);
|
||||
}
|
||||
return executeMacroClientSide(steps);
|
||||
},
|
||||
[rpcHidReady, executeMacroRemote, executeMacroClientSide],
|
||||
);
|
||||
|
||||
const cancelExecuteMacro = useCallback(async () => {
|
||||
if (abortController.current) {
|
||||
|
|
@ -375,5 +381,11 @@ export default function useKeyboard() {
|
|||
cancelOngoingKeyboardMacroHidRpc();
|
||||
}, [rpcHidReady, cancelOngoingKeyboardMacroHidRpc, abortController]);
|
||||
|
||||
return { handleKeyPress, resetKeyboardState, executeMacro, cleanup, cancelExecuteMacro };
|
||||
return {
|
||||
handleKeyPress,
|
||||
resetKeyboardState,
|
||||
executeMacro,
|
||||
cleanup,
|
||||
cancelExecuteMacro,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import Card from "@components/Card";
|
|||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { SystemVersionInfo } from "../utils/jsonrpc";
|
||||
import { sleep } from "@/utils";
|
||||
import { SystemVersionInfo } from "@/utils/jsonrpc";
|
||||
|
||||
export default function SettingsGeneralUpdateRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -136,13 +136,14 @@ function LoadingState({
|
|||
}, 0);
|
||||
|
||||
getVersionInfo()
|
||||
.then(versionInfo => {
|
||||
.then(async versionInfo => {
|
||||
// Add a small delay to ensure it's not just flickering
|
||||
return new Promise(resolve => setTimeout(() => resolve(versionInfo), 600));
|
||||
await sleep(600);
|
||||
return versionInfo
|
||||
})
|
||||
.then(versionInfo => {
|
||||
if (!signal.aborted) {
|
||||
onFinished(versionInfo as SystemVersionInfo);
|
||||
onFinished(versionInfo);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { KeySequence } from "@hooks/stores";
|
||||
import { getLocale } from '@localizations/runtime.js';
|
||||
import { getLocale , locales } from "@localizations/runtime.js";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { locales } from '@localizations/runtime.js';
|
||||
|
||||
export const formatters = {
|
||||
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
|
||||
|
|
@ -47,14 +46,14 @@ export const formatters = {
|
|||
amount: number;
|
||||
name: Intl.RelativeTimeFormatUnit;
|
||||
}[] = [
|
||||
{ amount: 60, name: "seconds" },
|
||||
{ amount: 60, name: "minutes" },
|
||||
{ amount: 24, name: "hours" },
|
||||
{ amount: 7, name: "days" },
|
||||
{ amount: 4.34524, name: "weeks" },
|
||||
{ amount: 12, name: "months" },
|
||||
{ amount: Number.POSITIVE_INFINITY, name: "years" },
|
||||
];
|
||||
{ amount: 60, name: "seconds" },
|
||||
{ amount: 60, name: "minutes" },
|
||||
{ amount: 24, name: "hours" },
|
||||
{ amount: 7, name: "days" },
|
||||
{ amount: 4.34524, name: "weeks" },
|
||||
{ amount: 12, name: "months" },
|
||||
{ amount: Number.POSITIVE_INFINITY, name: "years" },
|
||||
];
|
||||
|
||||
let duration = (date.valueOf() - new Date().valueOf()) / 1000;
|
||||
|
||||
|
|
@ -255,27 +254,41 @@ export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
|
|||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
type LocaleCode = typeof locales[number];
|
||||
type LocaleCode = (typeof locales)[number];
|
||||
|
||||
export function map_locale_code_to_name(currentLocale: LocaleCode, locale: string): [string, string] {
|
||||
// the first is the name in the current app locale (e.g. Inglese),
|
||||
// the second is the name in the language of the locale itself (e.g. English)
|
||||
switch (locale) {
|
||||
case '': return [m.locale_auto(), ""];
|
||||
case 'en': return [m.locale_en({}, { locale: currentLocale }), m.locale_en({}, { locale })];
|
||||
case 'da': return [m.locale_da({}, { locale: currentLocale }), m.locale_da({}, { locale })];
|
||||
case 'de': return [m.locale_de({}, { locale: currentLocale }), m.locale_de({}, { locale })];
|
||||
case 'es': return [m.locale_es({}, { locale: currentLocale }), m.locale_es({}, { locale })];
|
||||
case 'fr': return [m.locale_fr({}, { locale: currentLocale }), m.locale_fr({}, { locale })];
|
||||
case 'it': return [m.locale_it({}, { locale: currentLocale }), m.locale_it({}, { locale })];
|
||||
case 'nb': return [m.locale_nb({}, { locale: currentLocale }), m.locale_nb({}, { locale })];
|
||||
case 'sv': return [m.locale_sv({}, { locale: currentLocale }), m.locale_sv({}, { locale })];
|
||||
case 'zh': return [m.locale_zh({}, { locale: currentLocale }), m.locale_zh({}, { locale })];
|
||||
default: return [locale, ""];
|
||||
}
|
||||
export function map_locale_code_to_name(
|
||||
currentLocale: LocaleCode,
|
||||
locale: string,
|
||||
): [string, string] {
|
||||
// the first is the name in the current app locale (e.g. Inglese),
|
||||
// the second is the name in the language of the locale itself (e.g. English)
|
||||
switch (locale) {
|
||||
case "":
|
||||
return [m.locale_auto(), ""];
|
||||
case "en":
|
||||
return [m.locale_en({}, { locale: currentLocale }), m.locale_en({}, { locale })];
|
||||
case "da":
|
||||
return [m.locale_da({}, { locale: currentLocale }), m.locale_da({}, { locale })];
|
||||
case "de":
|
||||
return [m.locale_de({}, { locale: currentLocale }), m.locale_de({}, { locale })];
|
||||
case "es":
|
||||
return [m.locale_es({}, { locale: currentLocale }), m.locale_es({}, { locale })];
|
||||
case "fr":
|
||||
return [m.locale_fr({}, { locale: currentLocale }), m.locale_fr({}, { locale })];
|
||||
case "it":
|
||||
return [m.locale_it({}, { locale: currentLocale }), m.locale_it({}, { locale })];
|
||||
case "nb":
|
||||
return [m.locale_nb({}, { locale: currentLocale }), m.locale_nb({}, { locale })];
|
||||
case "sv":
|
||||
return [m.locale_sv({}, { locale: currentLocale }), m.locale_sv({}, { locale })];
|
||||
case "zh":
|
||||
return [m.locale_zh({}, { locale: currentLocale }), m.locale_zh({}, { locale })];
|
||||
default:
|
||||
return [locale, ""];
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteCookie(name: string, domain?: string, path = "/") {
|
||||
const domainPart = domain ? `; domain=${domain}` : "";
|
||||
|
|
@ -283,4 +296,8 @@ export function deleteCookie(name: string, domain?: string, path = "/") {
|
|||
document.cookie = `${name}=; path=${path}; max-age=0${domainPart}`;
|
||||
// fallback: set an expires in the past for older agents
|
||||
document.cookie = `${name}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT${domainPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
// JSON-RPC utility for use outside of React components
|
||||
|
||||
|
|
@ -22,9 +23,6 @@ export interface JsonRpcCallResponse<T = unknown> {
|
|||
|
||||
let rpcCallCounter = 0;
|
||||
|
||||
// Helper: sleep utility for retry delays
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Helper: wait for RTC data channel to be ready
|
||||
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
||||
const pollInterval = 100;
|
||||
|
|
|
|||
Loading…
Reference in New Issue