mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
Feat: Add AZERTY layout (virtual keyboard) and keyboard layout settings
This pull request introduces the following changes: * **Added AZERTY Keyboard Layout:** Implemented support for the AZERTY keyboard layout in the virtual keyboard. This includes both the visual layout and the corresponding key code mappings. * **Keyboard Layout Setting:** Added a new setting under *"Appearance"* to allow users to select their preferred keyboard layout (QWERTY Win, QWERTY Mac, AZERTY Win, Russian). The selected layout is saved to local storage and applied to the virtual keyboard.
This commit is contained in:
@@ -103,3 +103,7 @@
|
||||
.keyboard-header {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.hg-candidate-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ const en = {
|
||||
placeholder: 'Please input',
|
||||
submit: 'Submit',
|
||||
virtual: 'Keyboard',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
ctrlaltdel: 'Ctrl+Alt+Del',
|
||||
layout: 'Keyboard Layout'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
@@ -210,7 +211,8 @@ const en = {
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar',
|
||||
webTitle: 'Web Title',
|
||||
webTitleDesc: 'Customize the web page title'
|
||||
webTitleDesc: 'Customize the web page title',
|
||||
keyboardLayout: 'Keyboard Layout'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
|
||||
@@ -143,8 +143,13 @@ export function setKeyboardLayout(layout: string) {
|
||||
localStorage.setItem(KEYBOARD_LAYOUT_KEY, layout);
|
||||
}
|
||||
|
||||
export function getKeyboardLayout() {
|
||||
return localStorage.getItem(KEYBOARD_LAYOUT_KEY);
|
||||
export function getKeyboardLayout(): string {
|
||||
// Return 'azerty' or 'qwerty' (or 'qwerty' if not defined)
|
||||
const layout = localStorage.getItem(KEYBOARD_LAYOUT_KEY);
|
||||
if (layout === 'azerty' || layout === 'mac' || layout === 'rus') {
|
||||
return layout;
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
export function setSkipModifyPassword(skip: boolean) {
|
||||
|
||||
@@ -84,6 +84,7 @@ export const KeyboardCodes: Map<string, number> = new Map([
|
||||
['BracketRight', 48],
|
||||
['Backslash', 49],
|
||||
['IntlBackslash', 49],
|
||||
['Backquote_azerty', 100],
|
||||
|
||||
['Semicolon', 51],
|
||||
['Quote', 52],
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { AppleOutlined, WindowsOutlined } from '@ant-design/icons';
|
||||
import { ConfigProvider, Segmented, theme } from 'antd';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import Keyboard, { KeyboardButtonTheme } from 'react-simple-keyboard';
|
||||
import { Drawer } from 'vaul';
|
||||
|
||||
@@ -12,7 +10,7 @@ import '@/assets/styles/keyboard.css';
|
||||
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getKeyboardLayout, setKeyboardLayout, getLanguage } from '@/lib/localstorage.ts';
|
||||
import { getKeyboardLayout } from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
|
||||
|
||||
@@ -26,22 +24,50 @@ import {
|
||||
specialKeyMap
|
||||
} from './virtual-keys.ts';
|
||||
|
||||
// Helper function to map keys per active layout
|
||||
function getKeyCode(key: string, layout: string) {
|
||||
// AZERTY: map < > key (next to left-shift) to OEM_102 usage (100)
|
||||
if (layout === 'azerty' && key === 'Backquote_azerty') {
|
||||
return KeyboardCodes.get('Backquote_azerty');
|
||||
}
|
||||
|
||||
// AZERTY: swap A↔Q and Z↔W on French physical positions
|
||||
if (layout === 'azerty' && key.endsWith('_azerty')) {
|
||||
const base = key.replace('_azerty', '');
|
||||
if (base === 'KeyA') return KeyboardCodes.get('KeyQ');
|
||||
if (base === 'KeyQ') return KeyboardCodes.get('KeyA');
|
||||
if (base === 'KeyZ') return KeyboardCodes.get('KeyW');
|
||||
if (base === 'KeyW') return KeyboardCodes.get('KeyZ');
|
||||
// all other labels use their own code
|
||||
return KeyboardCodes.get(base);
|
||||
}
|
||||
|
||||
// default (QWERTY / Mac / Rus)
|
||||
return KeyboardCodes.get(key);
|
||||
}
|
||||
|
||||
export const VirtualKeyboard = () => {
|
||||
const isBigScreen = useMediaQuery({ minWidth: 850 });
|
||||
|
||||
const [isKeyboardOpen, setIsKeyboardOpen] = useAtom(isKeyboardOpenAtom);
|
||||
|
||||
const [layout, setLayout] = useState('default');
|
||||
const [layout, setLayout] = useState(getKeyboardLayout() || 'default');
|
||||
const [activeModifierKeys, setActiveModifierKeys] = useState<string[]>([]);
|
||||
|
||||
const keyboardRef = useRef<any>(null);
|
||||
|
||||
// Update the layout when it changes in settings
|
||||
useEffect(() => {
|
||||
const keyboardLayout = getKeyboardLayout();
|
||||
if (keyboardLayout && ['default', 'mac', 'rus'].includes(keyboardLayout)) {
|
||||
setLayout(keyboardLayout);
|
||||
// Function to check if the layout has changed
|
||||
const checkLayout = () => {
|
||||
const currentLayout = getKeyboardLayout() || 'default';
|
||||
if (currentLayout !== layout) {
|
||||
setLayout(currentLayout);
|
||||
}
|
||||
};
|
||||
|
||||
// Check for layout changes every time the keyboard is opened
|
||||
if (isKeyboardOpen) {
|
||||
checkLayout();
|
||||
}
|
||||
}, []);
|
||||
}, [isKeyboardOpen, layout]);
|
||||
|
||||
function onKeyPress(key: string) {
|
||||
if (modifierKeys.includes(key)) {
|
||||
@@ -67,7 +93,8 @@ export const VirtualKeyboard = () => {
|
||||
|
||||
function sendKeydown(key: string) {
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
const code = KeyboardCodes.get(specialKey ? specialKey : key);
|
||||
const code = getKeyCode(specialKey ? specialKey : key, layout);
|
||||
|
||||
if (!code) {
|
||||
console.log('unknown code: ', key);
|
||||
return;
|
||||
@@ -132,11 +159,6 @@ export const VirtualKeyboard = () => {
|
||||
return theme;
|
||||
}
|
||||
|
||||
function selectLayout(value: string) {
|
||||
setKeyboardLayout(value);
|
||||
setLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer.Root open={isKeyboardOpen} onOpenChange={setIsKeyboardOpen} modal={false}>
|
||||
<Drawer.Portal>
|
||||
@@ -148,22 +170,14 @@ export const VirtualKeyboard = () => {
|
||||
>
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between px-3 py-1">
|
||||
<div className="flex flex-row">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm
|
||||
}}
|
||||
>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'Win', value: 'default', icon: <WindowsOutlined /> },
|
||||
{ label: 'Mac', value: 'mac', icon: <AppleOutlined /> },
|
||||
...(getLanguage() === 'ru' ? [{ label: 'Rus', value: 'rus', icon: <WindowsOutlined /> }] : [])
|
||||
]}
|
||||
value={layout}
|
||||
onChange={selectLayout}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
<div className="keyboard-header text-sm font-medium px-2">
|
||||
{layout === 'default'
|
||||
? 'QWERTY (Win)'
|
||||
: layout === 'mac'
|
||||
? 'QWERTY (Mac)'
|
||||
: layout === 'azerty'
|
||||
? 'AZERTY (Win)'
|
||||
: 'Russian'}
|
||||
</div>
|
||||
|
||||
<div className="flex w-[100px] items-center justify-end">
|
||||
|
||||
@@ -26,6 +26,14 @@ export const keyboardOptions = {
|
||||
'{capslock} RusA RusS RusD RusF RusG RusH RusJ RusK RusL RusSemicolon RusQuote {enter}',
|
||||
'{shiftleft} RusZ RusX RusC RusV RusB RusN RusM RusComma RusPeriod RusSlash {shiftright}',
|
||||
'{controlleft} {winleft} {altleft} {space} {altright} {winright} {menu} {controlright}'
|
||||
],
|
||||
azerty: [
|
||||
'{escape} F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12',
|
||||
'Backquote_azerty Digit1_azerty Digit2_azerty Digit3_azerty Digit4_azerty Digit5_azerty Digit6_azerty Digit7_azerty Digit8_azerty Digit9_azerty Digit0_azerty Minus_azerty Equal_azerty {backspace}',
|
||||
'{tab} KeyA_azerty KeyZ_azerty KeyE_azerty KeyR_azerty KeyT_azerty KeyY_azerty KeyU_azerty KeyI_azerty KeyO_azerty KeyP_azerty BracketLeft_azerty BracketRight_azerty Backslash_azerty',
|
||||
'{capslock} KeyQ_azerty KeyS_azerty KeyD_azerty KeyF_azerty KeyG_azerty KeyH_azerty KeyJ_azerty KeyK_azerty KeyL_azerty Semicolon_azerty Quote_azerty {enter}',
|
||||
'{shiftleft} KeyW_azerty KeyX_azerty KeyC_azerty KeyV_azerty KeyB_azerty KeyN_azerty KeyM_azerty Comma_azerty Period_azerty Slash_azerty {shiftright}',
|
||||
'{controlleft} {winleft} {altleft} {space} {altright} {winright} {menu} {controlright}'
|
||||
]
|
||||
},
|
||||
display: {
|
||||
@@ -134,7 +142,71 @@ export const keyboardOptions = {
|
||||
RusComma: 'Б',
|
||||
RusPeriod: 'Ю',
|
||||
RusSlash: ',<br/>.',
|
||||
}
|
||||
|
||||
// AZERTY specific display keys
|
||||
// Row 1
|
||||
'Backquote_azerty': '<<br/>>',
|
||||
'Digit1_azerty': '&<br/>1',
|
||||
'Digit2_azerty': 'é<br/>2',
|
||||
'Digit3_azerty': '"<br/>#',
|
||||
'Digit4_azerty': '\'<br/>{',
|
||||
'Digit5_azerty': '(<br/>[',
|
||||
'Digit6_azerty': '-<br/>|',
|
||||
'Digit7_azerty': 'è<br/>`',
|
||||
'Digit8_azerty': '_<br/>\\',
|
||||
'Digit9_azerty': 'ç<br/>^',
|
||||
'Digit0_azerty': 'à<br/>@',
|
||||
'Minus_azerty': ')<br/>]',
|
||||
'Equal_azerty': '=<br/>}',
|
||||
|
||||
// Row 2
|
||||
'KeyA_azerty': 'A',
|
||||
'KeyZ_azerty': 'Z',
|
||||
'KeyE_azerty': 'E<br/>€',
|
||||
'KeyR_azerty': 'R',
|
||||
'KeyT_azerty': 'T',
|
||||
'KeyY_azerty': 'Y',
|
||||
'KeyU_azerty': 'U',
|
||||
'KeyI_azerty': 'I',
|
||||
'KeyO_azerty': 'O',
|
||||
'KeyP_azerty': 'P',
|
||||
'BracketLeft_azerty': '¨<br/>^',
|
||||
'BracketRight_azerty': '£<br/>$',
|
||||
'Backslash_azerty': 'µ<br/>*',
|
||||
|
||||
// Row 3
|
||||
'KeyQ_azerty': 'Q',
|
||||
'KeyS_azerty': 'S',
|
||||
'KeyD_azerty': 'D',
|
||||
'KeyF_azerty': 'F',
|
||||
'KeyG_azerty': 'G',
|
||||
'KeyH_azerty': 'H',
|
||||
'KeyJ_azerty': 'J',
|
||||
'KeyK_azerty': 'K',
|
||||
'KeyL_azerty': 'L',
|
||||
'Semicolon_azerty': 'M',
|
||||
'Quote_azerty': '%<br/>ù',
|
||||
|
||||
// Row 4
|
||||
'KeyW_azerty': 'W',
|
||||
'KeyX_azerty': 'X',
|
||||
'KeyC_azerty': 'C',
|
||||
'KeyV_azerty': 'V',
|
||||
'KeyB_azerty': 'B',
|
||||
'KeyN_azerty': 'N',
|
||||
'KeyM_azerty': '?<br/>,',
|
||||
'Comma_azerty': '.<br/>;',
|
||||
'Period_azerty': '/<br/>:',
|
||||
'Slash_azerty': '§<br/>!',
|
||||
},
|
||||
// Enable layout-specific display
|
||||
mergeDisplay: true,
|
||||
layoutCandidates: {
|
||||
default: 'default',
|
||||
shift: 'shift',
|
||||
azerty: 'azerty'
|
||||
},
|
||||
// ...remaining options...
|
||||
};
|
||||
|
||||
// control keys
|
||||
@@ -241,4 +313,4 @@ export const doubleKeys = [
|
||||
'Comma',
|
||||
'Period',
|
||||
'Slash'
|
||||
];
|
||||
];
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Language } from './language.tsx';
|
||||
import { MenuBar } from './menu-bar.tsx';
|
||||
import { WebTitle } from './web-title.tsx';
|
||||
import { KeyboardLayout } from './keyboard-layout.tsx';
|
||||
|
||||
export const Appearance = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -16,6 +17,7 @@ export const Appearance = () => {
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Language />
|
||||
<WebTitle />
|
||||
<KeyboardLayout />
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { KeyboardIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
|
||||
export const KeyboardLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const [layout, setLayout] = useState<string>('default');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const savedLayout = ls.getKeyboardLayout();
|
||||
if (savedLayout) {
|
||||
setLayout(savedLayout);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const options = [
|
||||
{ value: 'default', label: 'QWERTY (US)' },
|
||||
{ value: 'azerty', label: 'AZERTY (FR)' },
|
||||
{ value: 'mac', label: 'QWERTY (Mac)' },
|
||||
{ value: 'rus', label: 'Russian' }
|
||||
];
|
||||
|
||||
function changeLayout(value: string) {
|
||||
if (layout === value) return;
|
||||
setLayout(value);
|
||||
ls.setKeyboardLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<KeyboardIcon size={16} />
|
||||
<span>{t('settings.appearance.keyboardLayout', { defaultValue: 'Keyboard Layout' })}</span>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={layout}
|
||||
style={{ width: 180 }}
|
||||
options={options}
|
||||
loading={isLoading}
|
||||
onChange={changeLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
web/src/pages/settings/keyboard-layout.tsx
Normal file
30
web/src/pages/settings/keyboard-layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getKeyboardLayout, setKeyboardLayout } from '@/lib/localstorage.ts';
|
||||
|
||||
export const KeyboardLayoutSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
const [layout, setLayout] = useState(getKeyboardLayout() || 'default');
|
||||
|
||||
function handleChange(value: string) {
|
||||
setLayout(value);
|
||||
setKeyboardLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">{t('settings.keyboard.layout')}</label>
|
||||
<Select
|
||||
value={layout}
|
||||
onChange={handleChange}
|
||||
options={[
|
||||
{ value: 'default', label: 'QWERTY (Win)' },
|
||||
{ value: 'mac', label: 'QWERTY (Mac)' },
|
||||
{ value: 'azerty', label: 'AZERTY (Win)' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user