diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx index 006159cf..2fd6eaeb 100644 --- a/ui/src/components/UsbDeviceSetting.tsx +++ b/ui/src/components/UsbDeviceSetting.tsx @@ -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()); diff --git a/ui/src/components/UsbInfoSetting.tsx b/ui/src/components/UsbInfoSetting.tsx index dc3aa277..13b185cd 100644 --- a/ui/src/components/UsbInfoSetting.tsx +++ b/ui/src/components/UsbInfoSetting.tsx @@ -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 }), diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index ee9573b5..4c4d2d43 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -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 => 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((resolve, reject) => { - const abortListener = () => { - reject(new Error("Keyboard report aborted")); - }; + const sendKeystrokeLegacy = useCallback( + async (keys: number[], modifier: number, ac?: AbortController) => { + return await new Promise((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)[] = []; - - 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((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)[] = []; + + 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((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, + }; } diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index b67db2e4..45c3235b 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -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 => { diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 1e73ada0..29d31ac1 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -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}`; -} \ No newline at end of file +} + +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts index b4b5d039..6ab5ec41 100644 --- a/ui/src/utils/jsonrpc.ts +++ b/ui/src/utils/jsonrpc.ts @@ -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 { 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 { const pollInterval = 100;