diff --git a/web/src/assets/styles/keyboard.css b/web/src/assets/styles/keyboard.css index 921d507..e8f9060 100644 --- a/web/src/assets/styles/keyboard.css +++ b/web/src/assets/styles/keyboard.css @@ -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; +} diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 2888167..1cf5114 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -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', diff --git a/web/src/lib/localstorage.ts b/web/src/lib/localstorage.ts index a6d6fd2..19ffe10 100644 --- a/web/src/lib/localstorage.ts +++ b/web/src/lib/localstorage.ts @@ -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) { diff --git a/web/src/pages/desktop/keyboard/mappings.ts b/web/src/pages/desktop/keyboard/mappings.ts index 04976aa..11f2b18 100644 --- a/web/src/pages/desktop/keyboard/mappings.ts +++ b/web/src/pages/desktop/keyboard/mappings.ts @@ -84,6 +84,7 @@ export const KeyboardCodes: Map = new Map([ ['BracketRight', 48], ['Backslash', 49], ['IntlBackslash', 49], + ['Backquote_azerty', 100], ['Semicolon', 51], ['Quote', 52], diff --git a/web/src/pages/desktop/keyboard/virtual-keyboard.tsx b/web/src/pages/desktop/keyboard/virtual-keyboard.tsx index b26dc82..383c24c 100644 --- a/web/src/pages/desktop/keyboard/virtual-keyboard.tsx +++ b/web/src/pages/desktop/keyboard/virtual-keyboard.tsx @@ -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([]); - const keyboardRef = useRef(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 ( @@ -148,22 +170,14 @@ export const VirtualKeyboard = () => { > {/* header */}
-
- - }, - { label: 'Mac', value: 'mac', icon: }, - ...(getLanguage() === 'ru' ? [{ label: 'Rus', value: 'rus', icon: }] : []) - ]} - value={layout} - onChange={selectLayout} - /> - +
+ {layout === 'default' + ? 'QWERTY (Win)' + : layout === 'mac' + ? 'QWERTY (Mac)' + : layout === 'azerty' + ? 'AZERTY (Win)' + : 'Russian'}
diff --git a/web/src/pages/desktop/keyboard/virtual-keys.ts b/web/src/pages/desktop/keyboard/virtual-keys.ts index 13c8346..7f218d3 100644 --- a/web/src/pages/desktop/keyboard/virtual-keys.ts +++ b/web/src/pages/desktop/keyboard/virtual-keys.ts @@ -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: ',
.', - } + + // AZERTY specific display keys + // Row 1 + 'Backquote_azerty': '<
>', + 'Digit1_azerty': '&
1', + 'Digit2_azerty': 'é
2', + 'Digit3_azerty': '"
#', + 'Digit4_azerty': '\'
{', + 'Digit5_azerty': '(
[', + 'Digit6_azerty': '-
|', + 'Digit7_azerty': 'è
`', + 'Digit8_azerty': '_
\\', + 'Digit9_azerty': 'ç
^', + 'Digit0_azerty': 'à
@', + 'Minus_azerty': ')
]', + 'Equal_azerty': '=
}', + + // Row 2 + 'KeyA_azerty': 'A', + 'KeyZ_azerty': 'Z', + 'KeyE_azerty': 'E
€', + 'KeyR_azerty': 'R', + 'KeyT_azerty': 'T', + 'KeyY_azerty': 'Y', + 'KeyU_azerty': 'U', + 'KeyI_azerty': 'I', + 'KeyO_azerty': 'O', + 'KeyP_azerty': 'P', + 'BracketLeft_azerty': '¨
^', + 'BracketRight_azerty': '£
$', + 'Backslash_azerty': 'µ
*', + + // 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': '%
ù', + + // Row 4 + 'KeyW_azerty': 'W', + 'KeyX_azerty': 'X', + 'KeyC_azerty': 'C', + 'KeyV_azerty': 'V', + 'KeyB_azerty': 'B', + 'KeyN_azerty': 'N', + 'KeyM_azerty': '?
,', + 'Comma_azerty': '.
;', + 'Period_azerty': '/
:', + 'Slash_azerty': '§
!', + }, + // 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' -]; +]; \ No newline at end of file diff --git a/web/src/pages/desktop/menu/settings/appearance/index.tsx b/web/src/pages/desktop/menu/settings/appearance/index.tsx index 1c24d20..357ed06 100644 --- a/web/src/pages/desktop/menu/settings/appearance/index.tsx +++ b/web/src/pages/desktop/menu/settings/appearance/index.tsx @@ -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 = () => {
+
diff --git a/web/src/pages/desktop/menu/settings/appearance/keyboard-layout.tsx b/web/src/pages/desktop/menu/settings/appearance/keyboard-layout.tsx new file mode 100644 index 0000000..4d6d2b4 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/appearance/keyboard-layout.tsx @@ -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('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 ( +
+
+ + {t('settings.appearance.keyboardLayout', { defaultValue: 'Keyboard Layout' })} +
+ + +
+ ); +};