Compare commits

...

4 Commits

Author SHA1 Message Date
Marc Brooks 9ffb614c3a
Merge a5b07b4862 into 584768bacf 2025-07-10 12:23:30 +02:00
Aveline 584768bacf
chore: remove /device/ui-config.js endpoint (#678) 2025-07-10 12:04:47 +02:00
adammkelly 488276f3a8
feat(ui): reboot device (#421) (#505) 2025-07-10 00:02:13 +02:00
Marc Brooks a5b07b4862
refactor(ui): Refactor the keyboardLayouts
Add missing keyboard mappings for most layouts
Change  pasteModel.tsx to use the new structure and vastly clarified the way that keys are emitted.
Make each layout export just the KeyboardLayout object (which is a package of isoCode, name, and chars)
Made keyboardLayouts.ts export a function to select keyboard by `isoCode`, export the keyboards as label . value pairs (for a select list) and the list of keyboards.
Changed devices.$id.settings.keyboard.tsx use the exported keyboard option list.
2025-06-12 13:29:13 -05:00
25 changed files with 318 additions and 166 deletions

View File

@ -67,19 +67,19 @@ function Terminal({
}) {
const enableTerminal = useUiStore(state => state.terminalType == type);
const setTerminalType = useUiStore(state => state.setTerminalType);
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
useEffect(() => {
setTimeout(() => {
setDisableKeyboardFocusTrap(enableTerminal);
setDisableVideoFocusTrap(enableTerminal);
}, 500);
return () => {
setDisableKeyboardFocusTrap(false);
setDisableVideoFocusTrap(false);
};
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
}, [enableTerminal, setDisableVideoFocusTrap]);
const readyState = dataChannel.readyState;
useEffect(() => {
@ -116,7 +116,7 @@ function Terminal({
const { domEvent } = e;
if (domEvent.key === "Escape") {
setTerminalType("none");
setDisableKeyboardFocusTrap(false);
setDisableVideoFocusTrap(false);
domEvent.preventDefault();
}
});
@ -131,7 +131,7 @@ function Terminal({
onDataHandler.dispose();
onKeyHandler.dispose();
};
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
useEffect(() => {
if (!instance) return;
@ -158,7 +158,7 @@ function Terminal({
return () => {
window.removeEventListener("resize", handleResize);
};
}, [ref, instance]);
}, [instance]);
return (
<div

View File

@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { layouts, chars } from "@/keyboardLayouts";
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
import notifications from "@/notifications";
const hidKeyboardPayload = (keys: number[], modifier: number) => {
return { keys, modifier };
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
return { modifier, keys };
};
const modifierCode = (shift?: boolean, altRight?: boolean) => {
@ -62,49 +62,56 @@ export default function PasteModal() {
const onConfirmPaste = useCallback(async () => {
setPasteMode(false);
setDisableVideoFocusTrap(false);
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
if (!safeKeyboardLayout) return;
if (!chars[safeKeyboardLayout]) return;
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
if (!keyboard) return;
const text = TextAreaRef.current.value;
try {
for (const char of text) {
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
const keyprops = keyboard.chars[char];
if (!keyprops) continue;
const { key, shift, altRight, deadKey, accentKey } = keyprops;
if (!key) continue;
const keyz = [ keys[key] ];
const modz = [ modifierCode(shift, altRight) ];
if (deadKey) {
keyz.push(keys["Space"]);
modz.push(noModifier);
}
// if this is an accented character, we need to send that accent FIRST
if (accentKey) {
keyz.unshift(keys[accentKey.key])
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
}
for (const [index, kei] of keyz.entries()) {
// now send the actual key
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
// if what was requested was a dead key, we need to send an unmodified space to emit
// just the accent character
if (deadKey) {
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
}
// now send a message with no keys down to "release" the keys
await sendKeystroke({ modifier: 0, keys: [] });
}
} catch (error) {
console.error("Failed to paste text:", error);
notifications.error("Failed to paste text");
}
async function sendKeystroke(stroke: KeyStroke) {
await new Promise<void>((resolve, reject) => {
send(
"keyboardReport",
hidKeyboardPayload([kei], modz[index]),
hidKeyboardPayload(stroke.modifier, stroke.keys),
params => {
if ("error" in params) return reject(params.error);
send("keyboardReport", hidKeyboardPayload([], 0), params => {
if ("error" in params) return reject(params.error);
resolve();
});
},
}
);
});
}
}
} catch (error) {
console.error(error);
notifications.error("Failed to paste text");
}
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
useEffect(() => {
if (TextAreaRef.current) {
@ -154,7 +161,7 @@ export default function PasteModal() {
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
[...new Intl.Segmenter().segment(value)]
.map(x => x.segment)
.filter(char => !chars[safeKeyboardLayout][char]),
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
),
];
@ -175,7 +182,7 @@ export default function PasteModal() {
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
</p>
</div>
</div>

View File

@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
export default function WakeOnLanModal() {
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
const onCancelWakeOnLanModal = useCallback(() => {
setDisableVideoFocusTrap(false);
close();
setDisableFocusTrap(false);
}, [close, setDisableFocusTrap]);
}, [close, setDisableVideoFocusTrap]);
const onSendMagicPacket = useCallback(
(macAddress: string) => {
@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
}
} else {
notifications.success("Magic Packet sent successfully");
setDisableFocusTrap(false);
setDisableVideoFocusTrap(false);
close();
}
});
},
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
);
const syncStoredDevices = useCallback(() => {
@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
}
});
},
[storedDevices, send, syncStoredDevices],
[send, storedDevices, syncStoredDevices],
);
const onAddDevice = useCallback(

View File

@ -935,5 +935,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
} finally {
set({ loading: false });
}
},
}
}));

View File

@ -1,45 +1,32 @@
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
export interface KeyStroke { modifier: number; keys: number[]; }
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
// to add a new layout, create a file like the above and add it to the list
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
import { de_CH } from "@/keyboardLayouts/de_CH"
import { de_DE } from "@/keyboardLayouts/de_DE"
import { en_US } from "@/keyboardLayouts/en_US"
import { en_UK } from "@/keyboardLayouts/en_UK"
import { es_ES } from "@/keyboardLayouts/es_ES"
import { fr_BE } from "@/keyboardLayouts/fr_BE"
import { fr_CH } from "@/keyboardLayouts/fr_CH"
import { fr_FR } from "@/keyboardLayouts/fr_FR"
import { it_IT } from "@/keyboardLayouts/it_IT"
import { nb_NO } from "@/keyboardLayouts/nb_NO"
import { sv_SE } from "@/keyboardLayouts/sv_SE"
export const layouts: Record<string, string> = {
be_FR: name_fr_BE,
cs_CZ: name_cs_CZ,
en_UK: name_en_UK,
en_US: name_en_US,
fr_FR: name_fr_FR,
de_DE: name_de_DE,
it_IT: name_it_IT,
nb_NO: name_nb_NO,
es_ES: name_es_ES,
sv_SE: name_sv_SE,
fr_CH: name_fr_CH,
de_CH: name_de_CH,
}
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
export const chars: Record<string, Record<string, KeyCombo>> = {
be_FR: chars_fr_BE,
cs_CZ: chars_cs_CZ,
en_UK: chars_en_UK,
en_US: chars_en_US,
fr_FR: chars_fr_FR,
de_DE: chars_de_DE,
it_IT: chars_it_IT,
nb_NO: chars_nb_NO,
es_ES: chars_es_ES,
sv_SE: chars_sv_SE,
fr_CH: chars_fr_CH,
de_CH: chars_de_CH,
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
// fallback to original behaviour of en-US if no isoCode given
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
};
export const keyboardOptions = () => {
return keyboards.map((keyboard) => {
return { label: keyboard.name, value: keyboard.isoCode }
});
}

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Čeština";
const name = "Čeština";
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -242,3 +242,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const cs_CZ: KeyboardLayout = {
isoCode: "cs-CZ",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Schwiizerdütsch";
const name = "Schwiizerdütsch";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -163,3 +163,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_CH: KeyboardLayout = {
isoCode: "de-CH",
name: name,
chars: chars
};

View File

@ -1,12 +1,12 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Deutsch";
const name = "Deutsch";
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
@ -150,3 +150,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_DE: KeyboardLayout = {
isoCode: "de-DE",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "English (UK)";
const name = "English (UK)";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -105,3 +105,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>
export const en_UK: KeyboardLayout = {
isoCode: "en-UK",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "English (US)";
const name = "English (US)";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -111,3 +111,9 @@ export const chars = {
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, KeyCombo>
export const en_US: KeyboardLayout = {
isoCode: "en-US",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Español";
const name = "Español";
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -166,3 +166,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const es_ES: KeyboardLayout = {
isoCode: "es-ES",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Belgisch Nederlands";
const name = "Belgisch Nederlands";
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyQ", shift: true },
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
@ -165,3 +165,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_BE: KeyboardLayout = {
isoCode: "fr-BE",
name: name,
chars: chars
};

View File

@ -1,11 +1,11 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { chars as chars_de_CH } from "./de_CH"
import { de_CH } from "./de_CH"
export const name = "Français de Suisse";
const name = "Français de Suisse";
export const chars = {
...chars_de_CH,
const chars = {
...de_CH.chars,
"è": { key: "BracketLeft" },
"ü": { key: "BracketLeft", shift: true },
"é": { key: "Semicolon" },
@ -13,3 +13,9 @@ export const chars = {
"à": { key: "Quote" },
"ä": { key: "Quote", shift: true },
} as Record<string, KeyCombo>;
export const fr_CH: KeyboardLayout = {
isoCode: "fr-CH",
name: name,
chars: chars
};

View File

@ -1,11 +1,11 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Français";
const name = "Français";
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyQ", shift: true },
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
@ -137,3 +137,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_FR: KeyboardLayout = {
isoCode: "fr-FR",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Italiano";
const name = "Italiano";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -111,3 +111,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const it_IT: KeyboardLayout = {
isoCode: "it-IT",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Norsk bokmål";
const name = "Norsk bokmål";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -165,3 +165,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const nb_NO: KeyboardLayout = {
isoCode: "nb-NO",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Svenska";
const name = "Svenska";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
@ -162,3 +162,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const sv_SE: KeyboardLayout = {
isoCode: "sv-SE",
name: name,
chars: chars
};

View File

@ -1,17 +1,19 @@
// Key codes and modifiers correspond to definitions in the
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf)
export const keys = {
ArrowDown: 0x51,
ArrowLeft: 0x50,
ArrowRight: 0x4f,
ArrowUp: 0x52,
Backquote: 0x35,
Backquote: 0x35, // aka Grave
Backslash: 0x31,
Backspace: 0x2a,
BracketLeft: 0x2f,
BracketRight: 0x30,
BracketLeft: 0x2f, // aka LeftBrace
BracketRight: 0x30, // aka RightBrace
CapsLock: 0x39,
Comma: 0x36,
Compose: 0x65,
ContextMenu: 0,
Delete: 0x4c,
Digit0: 0x27,
@ -40,10 +42,21 @@ export const keys = {
F10: 0x43,
F11: 0x44,
F12: 0x45,
F13: 0x68,
F14: 0x69,
F15: 0x6a,
F16: 0x6b,
F17: 0x6c,
F18: 0x6d,
F19: 0x6e,
F20: 0x6f,
F21: 0x70,
F22: 0x71,
F23: 0x72,
F24: 0x73,
Home: 0x4a,
HashTilde: 0x32, // non-US # and ~
Insert: 0x49,
IntlBackslash: 0x64,
IntlBackslash: 0x64, // non-US \ and |
KeyA: 0x04,
KeyB: 0x05,
KeyC: 0x06,
@ -72,30 +85,35 @@ export const keys = {
KeyZ: 0x1d,
KeypadExclamation: 0xcf,
Minus: 0x2d,
NumLock: 0x53,
Numpad0: 0x62,
Numpad1: 0x59,
Numpad2: 0x5a,
Numpad3: 0x5b,
Numpad4: 0x5c,
None: 0x00,
NumLock: 0x53, // and Clear
Numpad0: 0x62, // and Insert
Numpad1: 0x59, // and End
Numpad2: 0x5a, // and Down Arrow
Numpad3: 0x5b, // and Page Down
Numpad4: 0x5c, // and Left Arrow
Numpad5: 0x5d,
Numpad6: 0x5e,
Numpad7: 0x5f,
Numpad8: 0x60,
Numpad9: 0x61,
Numpad6: 0x5e, // and Right Arrow
Numpad7: 0x5f, // and Home
Numpad8: 0x60, // and Up Arrow
Numpad9: 0x61, // and Page Up
NumpadAdd: 0x57,
NumpadComma: 0x85,
NumpadDecimal: 0x63,
NumpadDivide: 0x54,
NumpadEnter: 0x58,
NumpadEqual: 0x67,
NumpadLeftParen: 0xb6,
NumpadMultiply: 0x55,
NumpadRightParen: 0xb7,
NumpadSubtract: 0x56,
NumpadDecimal: 0x63,
PageDown: 0x4e,
PageUp: 0x4b,
Period: 0x37,
PrintScreen: 0x46,
Pause: 0x48,
Quote: 0x34,
Power: 0x66,
Quote: 0x34, // aka Single Quote or Apostrophe
ScrollLock: 0x47,
Semicolon: 0x33,
Slash: 0x38,

View File

@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
@ -140,6 +141,10 @@ if (isOnDevice) {
index: true,
element: <SettingsGeneralIndexRoute.default />,
},
{
path: "reboot",
element: <SettingsGeneralRebootRoute />,
},
{
path: "update",
element: <SettingsGeneralUpdateRoute />,

View File

@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
/>
</SettingsItem>
</div>
<div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem
title="Reboot Device"
description="Power cycle the JetKVM"
/>
<div>
<Button
size="SM"
theme="light"
text="Reboot Device"
onClick={() => navigateTo("./reboot")}
/>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,66 @@
import { useNavigate } from "react-router-dom";
import { useCallback } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary
send("reboot", {force: true});
}, [send]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
onClose,
onConfirmUpdate,
}: {
onClose: () => void;
onConfirmUpdate: () => void;
}) {
return (
<div className="pointer-events-auto relative mx-auto text-left">
<div>
<ConfirmationBox
onYes={onConfirmUpdate}
onNo={onClose}
/>
</div>
</div>
);
}
function ConfirmationBox({
onYes,
onNo,
}: {
onYes: () => void;
onNo: () => void;
}) {
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left">
<p className="text-base font-semibold text-black dark:text-white">
Reboot JetKVM
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system?
</p>
<div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
</div>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { layouts } from "@/keyboardLayouts";
import { keyboardOptions } from "@/keyboardLayouts";
import { Checkbox } from "@/components/Checkbox";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() {
return "en_US";
}, [keyboardLayout]);
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
const layoutOptions = keyboardOptions();
const ledSyncOptions = [
{ value: "auto", label: "Automatic" },
{ value: "browser", label: "Browser Only" },

View File

@ -79,7 +79,7 @@ export default function SettingsRoute() {
return () => {
setDisableVideoFocusTrap(false);
};
}, [setDisableVideoFocusTrap, sendKeyboardEvent]);
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
return (
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">

View File

@ -707,7 +707,7 @@ export default function KvmIdRoute() {
}, [diskChannel, file]);
// System update
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
@ -805,7 +805,7 @@ export default function KvmIdRoute() {
)}
<div className="relative h-full">
<FocusTrap
paused={disableKeyboardFocusTrap}
paused={disableVideoFocusTrap}
focusTrapOptions={{
allowOutsideClick: true,
escapeDeactivates: false,

18
web.go
View File

@ -97,9 +97,6 @@ func setupRouter() *gin.Engine {
// We use this to determine if the device is setup
r.GET("/device/status", handleDeviceStatus)
// We use this to provide the UI with the device configuration
r.GET("/device/ui-config.js", handleDeviceUIConfig)
// We use this to setup the device in the welcome page
r.POST("/device/setup", handleSetup)
@ -694,21 +691,6 @@ func handleCloudState(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
func handleDeviceUIConfig(c *gin.Context) {
config, _ := json.Marshal(gin.H{
"CLOUD_API": config.CloudURL,
"DEVICE_VERSION": builtAppVersion,
})
if config == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
return
}
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
}
func handleSetup(c *gin.Context) {
// Check if the device is already set up
if config.LocalAuthMode != "" || config.HashedPassword != "" {