feat: show remote keyboard lock status (#838)

* feat: show remote keyboard lock status

* fix(keyboard-led-status): eliminate polling and improve delivery

* fix(keyboard-led-status): simplify indicator labels
This commit is contained in:
肆月
2026-08-04 09:28:14 +08:00
committed by GitHub
parent 21e834b4c8
commit eb20fb41aa
45 changed files with 1483 additions and 25 deletions

View File

@@ -15,6 +15,11 @@ export function getHidMode() {
return http.get('/api/hid/mode');
}
// get remote keyboard lock LED status
export function getKeyboardLedStatus() {
return http.get('/api/hid/leds');
}
// set hid mode
export function setHidMode(mode: string) {
const data = {

View File

@@ -1,8 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { getMenuDisabledItems, getMenuDisplayMode } from '@/lib/localstorage.ts';
import {
getKeyboardLedStatusVisible,
getMenuDisabledItems,
getMenuDisplayMode
} from '@/lib/localstorage.ts';
import {
keyboardLedStatusVisibleAtom,
menuDisabledItemsAtom,
menuDisplayModeAtom,
submenuOpenCountAtom
@@ -23,6 +28,7 @@ export interface MenuVisibilityState {
export function useMenuVisibility(): MenuVisibilityState {
const [menuDisplayMode, setMenuDisplayMode] = useAtom(menuDisplayModeAtom);
const setMenuDisabledItems = useSetAtom(menuDisabledItemsAtom);
const setKeyboardLedStatusVisible = useSetAtom(keyboardLedStatusVisibleAtom);
const submenuOpenCount = useAtomValue(submenuOpenCountAtom);
const [isMenuExpanded, setIsMenuExpanded] = useState(true);
@@ -62,6 +68,8 @@ export function useMenuVisibility(): MenuVisibilityState {
const items = getMenuDisabledItems();
setMenuDisabledItems(items);
setKeyboardLedStatusVisible(getKeyboardLedStatusVisible());
if (displayMode === 'off') {
setIsMenuExpanded(false);
}

View File

@@ -332,10 +332,26 @@ const ca = {
modeOff: 'Apagat',
modeAuto: 'Ocultació automàtica',
modeAlways: 'Sempre visible',
keyboardLedStatus: 'Indicadors de bloqueig del teclat',
keyboardLedStatusDesc:
'Mostra lestat de Bloq Num, Bloq Maj i Bloq Despl de lordinador remot',
icons: 'Icones del submenú',
iconsDesc: 'Mostra les icones del submenú a la barra de menús'
}
},
keyboardLedStatus: {
groupLabel: 'Estat de bloqueig del teclat remot',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Bloq Num',
numLockShort: 'Num',
capsLock: 'Bloq Maj',
capsLockShort: 'Maj',
scrollLock: 'Bloq Despl',
scrollLockShort: 'Despl',
on: 'Activat',
off: 'Desactivat',
unknown: 'Desconegut'
},
device: {
title: 'Dispositiu',
oled: {

View File

@@ -335,10 +335,26 @@ const cz = {
modeOff: 'Vypnuto',
modeAuto: 'Automatické skrytí',
modeAlways: 'Vždy viditelné',
keyboardLedStatus: 'Indikátory zámku klávesnice',
keyboardLedStatusDesc:
'Zobrazit stav Num Lock, Caps Lock a Scroll Lock vzdáleného počítače',
icons: 'Ikony podnabídky',
iconsDesc: 'Zobrazení ikon podnabídky na liště nabídek'
}
},
keyboardLedStatus: {
groupLabel: 'Stav zámků vzdálené klávesnice',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Zapnuto',
off: 'Vypnuto',
unknown: 'Neznámé'
},
device: {
title: 'Zařízení',
oled: {

View File

@@ -332,10 +332,26 @@ const da = {
modeOff: 'Fra',
modeAuto: 'Skjul automatisk',
modeAlways: 'Altid synlig',
keyboardLedStatus: 'Tastaturlåseindikatorer',
keyboardLedStatusDesc:
'Vis Num Lock-, Caps Lock- og Scroll Lock-status for fjerncomputeren',
icons: 'Undermenuikoner',
iconsDesc: 'Vis undermenuikoner i menulinjen'
}
},
keyboardLedStatus: {
groupLabel: 'Status for låse på fjernkeyboard',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Til',
off: 'Fra',
unknown: 'Ukendt'
},
device: {
title: 'Enhed',
oled: {

View File

@@ -338,10 +338,26 @@ const de = {
modeOff: 'Aus',
modeAuto: 'Automatisch ausblenden',
modeAlways: 'Immer sichtbar',
keyboardLedStatus: 'Tastensperren-Anzeigen',
keyboardLedStatusDesc:
'Num-Lock-, Feststell- und Rollen-Status des Remote-Computers anzeigen',
icons: 'Untermenüsymbole',
iconsDesc: 'Untermenüsymbole in der Menüleiste anzeigen'
}
},
keyboardLedStatus: {
groupLabel: 'Tastensperren-Status der Remote-Tastatur',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num-Taste',
numLockShort: 'Num',
capsLock: 'Feststelltaste',
capsLockShort: 'Fest',
scrollLock: 'Rollen-Taste',
scrollLockShort: 'Roll',
on: 'Ein',
off: 'Aus',
unknown: 'Unbekannt'
},
device: {
title: 'Gerät',
oled: {

View File

@@ -331,10 +331,25 @@ const en = {
modeOff: 'Off',
modeAuto: 'Auto hide',
modeAlways: 'Always visible',
keyboardLedStatus: 'Keyboard lock indicators',
keyboardLedStatusDesc: 'Display remote Num Lock, Caps Lock, and Scroll Lock status',
icons: 'Submenu Icons',
iconsDesc: 'Display submenu icons in the menu bar'
}
},
keyboardLedStatus: {
groupLabel: 'Remote keyboard lock status',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'On',
off: 'Off',
unknown: 'Unknown'
},
device: {
title: 'Device',
oled: {

View File

@@ -335,10 +335,26 @@ const es = {
modeOff: 'Apagado',
modeAuto: 'Ocultar automáticamente',
modeAlways: 'Siempre visible',
keyboardLedStatus: 'Indicadores de bloqueo del teclado',
keyboardLedStatusDesc:
'Mostrar el estado de Bloq Num, Bloq Mayús y Bloq Despl del equipo remoto',
icons: 'Iconos del submenú',
iconsDesc: 'Mostrar iconos de submenú en la barra de menú'
}
},
keyboardLedStatus: {
groupLabel: 'Estado de bloqueos del teclado remoto',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Bloq Num',
numLockShort: 'Num',
capsLock: 'Bloq Mayús',
capsLockShort: 'May',
scrollLock: 'Bloq Despl',
scrollLockShort: 'Despl',
on: 'Activado',
off: 'Desactivado',
unknown: 'Desconocido'
},
device: {
title: 'Dispositivo',
oled: {

View File

@@ -337,10 +337,26 @@ const fr = {
modeOff: 'Désactivé',
modeAuto: 'Masquer automatiquement',
modeAlways: 'Toujours visible',
keyboardLedStatus: 'Indicateurs de verrouillage du clavier',
keyboardLedStatusDesc:
'Afficher létat de Verr Num, Verr Maj et Arrêt défil du poste distant',
icons: 'Icônes du sous-menu',
iconsDesc: 'Afficher les icônes des sous-menus dans la barre de menus'
}
},
keyboardLedStatus: {
groupLabel: 'État des verrouillages du clavier distant',
indicatorLabel: '{{label}} : {{state}}',
numLock: 'Verr Num',
numLockShort: 'Num',
capsLock: 'Verr Maj',
capsLockShort: 'Maj',
scrollLock: 'Arrêt défil',
scrollLockShort: 'Défil',
on: 'Activé',
off: 'Désactivé',
unknown: 'Inconnu'
},
device: {
title: 'Appareil',
oled: {

View File

@@ -336,10 +336,26 @@ const hu = {
modeOff: 'Ki',
modeAuto: 'Automatikus elrejtés',
modeAlways: 'Mindig látható',
keyboardLedStatus: 'Billentyűzár-jelzők',
keyboardLedStatusDesc:
'A távoli számítógép Num Lock, Caps Lock és Scroll Lock állapotának megjelenítése',
icons: 'Almenü ikonok',
iconsDesc: 'Almenüikonok megjelenítése a menüsorban'
}
},
keyboardLedStatus: {
groupLabel: 'Távoli billentyűzárak állapota',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Be',
off: 'Ki',
unknown: 'Ismeretlen'
},
device: {
title: 'Eszköz',
oled: {

View File

@@ -334,10 +334,26 @@ const id = {
modeOff: 'Mati',
modeAuto: 'Sembunyikan otomatis',
modeAlways: 'Selalu terlihat',
keyboardLedStatus: 'Indikator kunci keyboard',
keyboardLedStatusDesc:
'Tampilkan status Num Lock, Caps Lock, dan Scroll Lock komputer jarak jauh',
icons: 'Ikon Submenu',
iconsDesc: 'Menampilkan ikon submenu di bilah menu'
}
},
keyboardLedStatus: {
groupLabel: 'Status kunci keyboard jarak jauh',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Aktif',
off: 'Nonaktif',
unknown: 'Tidak diketahui'
},
device: {
title: 'Perangkat',
oled: {

View File

@@ -336,10 +336,26 @@ const it = {
modeOff: 'Spento',
modeAuto: 'Nascondi automaticamente',
modeAlways: 'Sempre visibile',
keyboardLedStatus: 'Indicatori di blocco della tastiera',
keyboardLedStatusDesc:
'Mostra lo stato di Bloc Num, Bloc Maiusc e Bloc Scorr del computer remoto',
icons: 'Icone dei sottomenu',
iconsDesc: 'Visualizza le icone dei sottomenu nella barra dei menu'
}
},
keyboardLedStatus: {
groupLabel: 'Stato dei blocchi della tastiera remota',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Bloc Num',
numLockShort: 'Num',
capsLock: 'Bloc Maiusc',
capsLockShort: 'Mai',
scrollLock: 'Bloc Scorr',
scrollLockShort: 'Scorr',
on: 'Attivo',
off: 'Disattivo',
unknown: 'Sconosciuto'
},
device: {
title: 'Dispositivo',
oled: {

View File

@@ -335,10 +335,26 @@ const ja = {
modeOff: '閉じる',
modeAuto: '自動非表示',
modeAlways: '常に表示',
keyboardLedStatus: 'キーボードロックの表示',
keyboardLedStatusDesc:
'リモートコンピューターの Num Lock、Caps Lock、Scroll Lock の状態を表示',
icons: 'メニューアイコン',
iconsDesc: 'メニューバーでのサブメニューアイコンの表示'
}
},
keyboardLedStatus: {
groupLabel: 'リモートキーボードのロック状態',
indicatorLabel: '{{label}}{{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'オン',
off: 'オフ',
unknown: '不明'
},
device: {
title: 'デバイス',
oled: {

View File

@@ -330,10 +330,25 @@ const ko = {
modeOff: '꺼짐',
modeAuto: '자동 숨기기',
modeAlways: '항상 보이기',
keyboardLedStatus: '키보드 잠금 표시기',
keyboardLedStatusDesc: '원격 컴퓨터의 Num Lock, Caps Lock, Scroll Lock 상태 표시',
icons: '하위 메뉴 아이콘',
iconsDesc: '메뉴 바에 하위 메뉴 아이콘을 표시합니다'
}
},
keyboardLedStatus: {
groupLabel: '원격 키보드 잠금 상태',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: '켜짐',
off: '꺼짐',
unknown: '알 수 없음'
},
device: {
title: '장치',
oled: {

View File

@@ -333,10 +333,26 @@ const nb = {
modeOff: 'Av',
modeAuto: 'Skjul automatisk',
modeAlways: 'Alltid synlig',
keyboardLedStatus: 'Indikatorer for tastaturlås',
keyboardLedStatusDesc:
'Vis Num Lock-, Caps Lock- og Scroll Lock-status for den eksterne datamaskinen',
icons: 'Undermenyikoner',
iconsDesc: 'Vis undermenyikoner i menylinjen'
}
},
keyboardLedStatus: {
groupLabel: 'Status for låser på eksternt tastatur',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'På',
off: 'Av',
unknown: 'Ukjent'
},
device: {
title: 'Enhet',
oled: {

View File

@@ -336,10 +336,26 @@ const nl = {
modeOff: 'Uit',
modeAuto: 'Automatisch verbergen',
modeAlways: 'Altijd zichtbaar',
keyboardLedStatus: 'Toetsvergrendelingsindicatoren',
keyboardLedStatusDesc:
'Toon de Num Lock-, Caps Lock- en Scroll Lock-status van de externe computer',
icons: 'Submenupictogrammen',
iconsDesc: 'Submenupictogrammen weergeven in de menubalk'
}
},
keyboardLedStatus: {
groupLabel: 'Toetsvergrendelingsstatus van extern toetsenbord',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Aan',
off: 'Uit',
unknown: 'Onbekend'
},
device: {
title: 'Apparaat',
oled: {

View File

@@ -335,10 +335,26 @@ const pl = {
modeOff: 'Wyłączone',
modeAuto: 'Automatyczne ukrywanie',
modeAlways: 'Zawsze widoczny',
keyboardLedStatus: 'Wskaźniki blokad klawiatury',
keyboardLedStatusDesc:
'Wyświetl stan Num Lock, Caps Lock i Scroll Lock zdalnego komputera',
icons: 'Ikony podmenu',
iconsDesc: 'Wyświetla ikony podmenu na pasku menu'
}
},
keyboardLedStatus: {
groupLabel: 'Stan blokad zdalnej klawiatury',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Włączone',
off: 'Wyłączone',
unknown: 'Nieznany'
},
device: {
title: 'Urządzenie',
oled: {

View File

@@ -334,10 +334,26 @@ const pt_br = {
modeOff: 'Desligado',
modeAuto: 'Ocultar automaticamente',
modeAlways: 'Sempre visível',
keyboardLedStatus: 'Indicadores de bloqueio do teclado',
keyboardLedStatusDesc:
'Exibir o estado de Num Lock, Caps Lock e Scroll Lock do computador remoto',
icons: 'Ícones do submenu',
iconsDesc: 'Exibir ícones de submenus na barra de menu'
}
},
keyboardLedStatus: {
groupLabel: 'Estado dos bloqueios do teclado remoto',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Ativado',
off: 'Desativado',
unknown: 'Desconhecido'
},
device: {
title: 'Dispositivo',
oled: {

View File

@@ -335,10 +335,26 @@ const ru = {
modeOff: 'Выкл.',
modeAuto: 'Автоматическое скрытие',
modeAlways: 'Всегда виден',
keyboardLedStatus: 'Индикаторы блокировки клавиатуры',
keyboardLedStatusDesc:
'Показывать состояние Num Lock, Caps Lock и Scroll Lock удалённого компьютера',
icons: 'Значки подменю',
iconsDesc: 'Отображение значков подменю в строке меню'
}
},
keyboardLedStatus: {
groupLabel: 'Состояние блокировок удалённой клавиатуры',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Вкл.',
off: 'Выкл.',
unknown: 'Неизвестно'
},
device: {
title: 'Устройство',
oled: {

View File

@@ -331,10 +331,26 @@ const se = {
modeOff: 'Av',
modeAuto: 'Dölj automatiskt',
modeAlways: 'Alltid synlig',
keyboardLedStatus: 'Indikatorer för tangentbordslås',
keyboardLedStatusDesc:
'Visa Num Lock-, Caps Lock- och Scroll Lock-status för fjärrdatorn',
icons: 'Undermenyikoner',
iconsDesc: 'Visa undermenyikoner i menyraden'
}
},
keyboardLedStatus: {
groupLabel: 'Status för lås på fjärrtangentbord',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'På',
off: 'Av',
unknown: 'Okänd'
},
device: {
title: 'Enhet',
oled: {

View File

@@ -327,10 +327,26 @@ const th = {
modeOff: 'ปิด',
modeAuto: 'ซ่อนอัตโนมัติ',
modeAlways: 'มองเห็นได้เสมอ',
keyboardLedStatus: 'ตัวบ่งชี้ปุ่มล็อกแป้นพิมพ์',
keyboardLedStatusDesc:
'แสดงสถานะ Num Lock, Caps Lock และ Scroll Lock ของคอมพิวเตอร์ระยะไกล',
icons: 'ไอคอนเมนูย่อย',
iconsDesc: 'แสดงไอคอนเมนูย่อยในแถบเมนู'
}
},
keyboardLedStatus: {
groupLabel: 'สถานะปุ่มล็อกแป้นพิมพ์ระยะไกล',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'เปิด',
off: 'ปิด',
unknown: 'ไม่ทราบ'
},
device: {
title: 'อุปกรณ์',
oled: {

View File

@@ -333,10 +333,26 @@ const tr = {
modeOff: 'Kapalı',
modeAuto: 'Otomatik gizle',
modeAlways: 'Her zaman görünür',
keyboardLedStatus: 'Klavye kilidi göstergeleri',
keyboardLedStatusDesc:
'Uzak bilgisayarın Num Lock, Caps Lock ve Scroll Lock durumunu göster',
icons: 'Alt Menü Simgeleri',
iconsDesc: 'Menü çubuğunda alt menü simgelerini görüntüle'
}
},
keyboardLedStatus: {
groupLabel: 'Uzak klavye kilidi durumu',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Açık',
off: 'Kapalı',
unknown: 'Bilinmiyor'
},
device: {
title: 'Cihaz',
oled: {

View File

@@ -334,10 +334,26 @@ const uk = {
modeOff: 'Вимк',
modeAuto: 'Автоматичне приховування',
modeAlways: 'Завжди видно',
keyboardLedStatus: 'Індикатори блокування клавіатури',
keyboardLedStatusDesc:
'Показувати стан Num Lock, Caps Lock і Scroll Lock віддаленого комп’ютера',
icons: 'Значки підменю',
iconsDesc: 'Відображення значків підменю на панелі меню'
}
},
keyboardLedStatus: {
groupLabel: 'Стан блокувань віддаленої клавіатури',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Увімкнено',
off: 'Вимкнено',
unknown: 'Невідомо'
},
device: {
title: 'Пристрій',
oled: {

View File

@@ -331,10 +331,26 @@ const vi = {
modeOff: 'Tắt',
modeAuto: 'Tự động ẩn',
modeAlways: 'Luôn hiển thị',
keyboardLedStatus: 'Chỉ báo khóa bàn phím',
keyboardLedStatusDesc:
'Hiển thị trạng thái Num Lock, Caps Lock và Scroll Lock của máy tính từ xa',
icons: 'Biểu tượng menu con',
iconsDesc: 'Hiển thị biểu tượng menu con trên thanh menu'
}
},
keyboardLedStatus: {
groupLabel: 'Trạng thái khóa bàn phím từ xa',
indicatorLabel: '{{label}}: {{state}}',
numLock: 'Num Lock',
numLockShort: 'Num',
capsLock: 'Caps Lock',
capsLockShort: 'Caps',
scrollLock: 'Scroll Lock',
scrollLockShort: 'Scr',
on: 'Bật',
off: 'Tắt',
unknown: 'Không rõ'
},
device: {
title: 'Thiết bị',
oled: {

View File

@@ -322,10 +322,25 @@ const zh = {
modeOff: '关闭',
modeAuto: '自动隐藏',
modeAlways: '始终显示',
keyboardLedStatus: '键盘锁定状态指示灯',
keyboardLedStatusDesc: '显示远程主机的 Num Lock、Caps Lock 和 Scroll Lock 状态',
icons: '菜单图标',
iconsDesc: '是否在菜单栏中显示子菜单图标'
}
},
keyboardLedStatus: {
groupLabel: '远程键盘锁定状态',
indicatorLabel: '{{label}}{{state}}',
numLock: '数字锁定',
numLockShort: '数',
capsLock: '大写锁定',
capsLockShort: '大',
scrollLock: '滚动锁定',
scrollLockShort: '滚',
on: '开启',
off: '关闭',
unknown: '未知'
},
device: {
title: '设备',
oled: {

View File

@@ -322,10 +322,25 @@ const zh_tw = {
modeOff: '關閉',
modeAuto: '自動隱藏',
modeAlways: '始終顯示',
keyboardLedStatus: '鍵盤鎖定狀態指示燈',
keyboardLedStatusDesc: '顯示遠端電腦的 Num Lock、Caps Lock 與 Scroll Lock 狀態',
icons: '選單圖示',
iconsDesc: '是否在選單欄中顯示子選單圖示'
}
},
keyboardLedStatus: {
groupLabel: '遠端鍵盤鎖定狀態',
indicatorLabel: '{{label}}{{state}}',
numLock: '數字鎖定',
numLockShort: '數',
capsLock: '大寫鎖定',
capsLockShort: '大',
scrollLock: '捲動鎖定',
scrollLockShort: '捲',
on: '開啟',
off: '關閉',
unknown: '未知'
},
device: {
title: '設備',
oled: {

View File

@@ -11,3 +11,6 @@ export const webTitleAtom = atom('');
// menu display mode: 'off' | 'auto' | 'always'
export const menuDisplayModeAtom = atom<string>('auto');
// show the remote keyboard lock-status indicator beside the menu bar
export const keyboardLedStatusVisibleAtom = atom(true);

View File

@@ -18,6 +18,7 @@ const KEYBOARD_LANGUAGE_KEY = 'nano-kvm-keyboard-language';
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
const MENU_AUTO_HIDE_KEY = 'nano-kvm-menu-auto-hide';
const KEYBOARD_LED_STATUS_VISIBLE_KEY = 'nano-kvm-keyboard-led-status-visible';
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
type ItemWithExpiry = {
@@ -222,6 +223,15 @@ export function setMenuDisplayMode(mode: string) {
localStorage.setItem(MENU_AUTO_HIDE_KEY, mode);
}
export function getKeyboardLedStatusVisible(): boolean {
const value = localStorage.getItem(KEYBOARD_LED_STATUS_VISIBLE_KEY);
return value !== 'false';
}
export function setKeyboardLedStatusVisible(visible: boolean) {
localStorage.setItem(KEYBOARD_LED_STATUS_VISIBLE_KEY, String(visible));
}
export function getPowerConfirm() {
const enabled = localStorage.getItem(POWER_CONFIRM_KEY);
return enabled === 'true';

View File

@@ -0,0 +1,68 @@
import { Tooltip } from 'antd';
import clsx from 'clsx';
import { useKeyboardLedStatus } from './use-keyboard-led-status';
const LOCK_INDICATORS = {
numLock: { label: 'Num Lock', shortLabel: 'Num' },
capsLock: { label: 'Caps Lock', shortLabel: 'Caps' },
scrollLock: { label: 'Scroll Lock', shortLabel: 'Scr' }
} as const;
type LockIndicatorProps = {
labelKey: 'numLock' | 'capsLock' | 'scrollLock';
active: boolean;
known: boolean;
};
function LockIndicator({ labelKey, active, known }: LockIndicatorProps) {
const { label, shortLabel } = LOCK_INDICATORS[labelKey];
const state = known ? (active ? 'On' : 'Off') : 'Unknown';
const indicatorLabel = `${label}: ${state}`;
return (
<Tooltip
title={indicatorLabel}
placement="bottom"
mouseEnterDelay={0.6}
>
<div
className="flex h-[8px] items-center gap-1 px-1 text-[8px] font-medium leading-[8px] text-neutral-400"
aria-label={indicatorLabel}
role="img"
>
<span
aria-hidden="true"
className={clsx(
'flex h-2 w-2 items-center justify-center rounded-full text-[7px] leading-none',
known
? active
? 'bg-emerald-400'
: 'bg-neutral-600'
: 'border border-dashed border-neutral-500 text-neutral-300'
)}
>
{!known && '?'}
</span>
<span className="hidden sm:inline">{shortLabel}</span>
</div>
</Tooltip>
);
}
export function KeyboardLedStatus() {
const status = useKeyboardLedStatus();
const known = status?.known ?? false;
return (
<div
className="flex h-full w-[40px] flex-col items-start justify-center rounded bg-neutral-800/80"
aria-label="Keyboard lock status"
role="group"
>
<LockIndicator labelKey="numLock" active={status?.numLock ?? false} known={known} />
<LockIndicator labelKey="capsLock" active={status?.capsLock ?? false} known={known} />
<LockIndicator labelKey="scrollLock" active={status?.scrollLock ?? false} known={known} />
</div>
);
}

View File

@@ -0,0 +1,69 @@
export const KEYBOARD_LED_STATUS_EVENT = 'hid-led-status';
export type KeyboardLedStatus = {
numLock: boolean;
capsLock: boolean;
scrollLock: boolean;
known: boolean;
updatedAt: string;
};
export function parseKeyboardLedStatus(value: unknown): KeyboardLedStatus | null {
if (!value || typeof value !== 'object') {
return null;
}
const status = value as Partial<KeyboardLedStatus>;
if (
typeof status.numLock !== 'boolean' ||
typeof status.capsLock !== 'boolean' ||
typeof status.scrollLock !== 'boolean' ||
typeof status.known !== 'boolean'
) {
return null;
}
return {
numLock: status.numLock,
capsLock: status.capsLock,
scrollLock: status.scrollLock,
known: status.known,
updatedAt: typeof status.updatedAt === 'string' ? status.updatedAt : ''
};
}
export function parseKeyboardLedStatusMessage(message: {
data: unknown;
}): KeyboardLedStatus | null {
if (typeof message.data !== 'string') {
return null;
}
try {
const envelope = JSON.parse(message.data) as { data?: unknown };
if (typeof envelope.data !== 'string') {
return null;
}
return parseKeyboardLedStatus(JSON.parse(envelope.data));
} catch (error) {
console.log(error);
return null;
}
}
export function shouldAcceptKeyboardLedStatus(
current: KeyboardLedStatus | null,
next: KeyboardLedStatus
) {
if (!current) {
return true;
}
return getTimestamp(next) >= getTimestamp(current);
}
function getTimestamp(status: KeyboardLedStatus) {
const timestamp = Date.parse(status.updatedAt);
return Number.isFinite(timestamp) ? timestamp : 0;
}

View File

@@ -0,0 +1,57 @@
import { useEffect, useRef, useState } from 'react';
import { getKeyboardLedStatus } from '@/api/hid.ts';
import { client } from '@/lib/websocket.ts';
import {
KEYBOARD_LED_STATUS_EVENT,
KeyboardLedStatus,
parseKeyboardLedStatus,
parseKeyboardLedStatusMessage,
shouldAcceptKeyboardLedStatus
} from './model';
export function useKeyboardLedStatus() {
const [status, setStatus] = useState<KeyboardLedStatus | null>(null);
const latestStatusRef = useRef<KeyboardLedStatus | null>(null);
useEffect(() => {
let disposed = false;
function update(next: KeyboardLedStatus) {
if (!shouldAcceptKeyboardLedStatus(latestStatusRef.current, next)) {
return;
}
latestStatusRef.current = next;
setStatus(next);
}
const unsubscribe = client.on(KEYBOARD_LED_STATUS_EVENT, (message) => {
const next = parseKeyboardLedStatusMessage(message);
if (next) {
update(next);
}
});
getKeyboardLedStatus()
.then((rsp) => {
if (rsp.code !== 0) {
return;
}
const next = parseKeyboardLedStatus(rsp.data);
if (!disposed && next) {
update(next);
}
})
.catch(() => undefined);
return () => {
disposed = true;
unsubscribe();
};
}, []);
return status;
}

View File

@@ -5,10 +5,11 @@ import { useAtomValue } from 'jotai';
import { GripVerticalIcon } from 'lucide-react';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { menuDisabledItemsAtom } from '@/jotai/settings.ts';
import { keyboardLedStatusVisibleAtom, menuDisabledItemsAtom } from '@/jotai/settings.ts';
import { useMenuBounds } from '@/hooks/useMenuBounds.ts';
import { useMenuVisibility } from '@/hooks/useMenuVisibility.ts';
import { KeyboardLedStatus } from '../keyboard-led-status';
import { DownloadImage } from './download.tsx';
import { Fullscreen } from './fullscreen';
import { Image } from './image';
@@ -27,6 +28,7 @@ export const Menu = () => {
const nodeRef = useRef<HTMLDivElement | null>(null);
const menuDisabledItems = useAtomValue(menuDisabledItemsAtom);
const isKeyboardLedStatusVisible = useAtomValue(keyboardLedStatusVisibleAtom);
const {
isInitialized,
@@ -75,11 +77,21 @@ export const Menu = () => {
<div className="sticky top-[10px] flex w-full justify-center">
<div
className={clsx(
'h-[36px] items-center rounded bg-neutral-800/80 pl-1 pr-2 transition-all duration-300',
'relative h-[36px] items-center rounded bg-neutral-800/80 pl-1 pr-2 transition-all duration-300',
isMenuExpanded ? 'flex' : 'hidden',
isMenuHidden ? '-translate-y-[110%] opacity-80' : 'translate-y-0 opacity-100'
)}
>
{isMenuExpanded && isKeyboardLedStatusVisible && (
<div
className={clsx(
'absolute inset-y-0 right-full mr-1 transition-all duration-300',
isMenuHidden ? 'pointer-events-none opacity-0' : 'opacity-100'
)}
>
<KeyboardLedStatus />
</div>
)}
<strong>
<div className="flex h-[30px] cursor-move select-none items-center justify-center pl-1 text-neutral-500">
<GripVerticalIcon size={18} />

View File

@@ -1,6 +1,7 @@
import { Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { KeyboardLedStatusSetting } from './keyboard-led-status.tsx';
import { Language } from './language.tsx';
import { MenuIcons } from './menu-icons.tsx';
import { MenuMode } from './menu-mode.tsx';
@@ -22,6 +23,7 @@ export const Appearance = () => {
<div className="text-neutral-400">{t('settings.appearance.menuBar.title')}</div>
<MenuMode />
<KeyboardLedStatusSetting />
<MenuIcons />
</>
);

View File

@@ -0,0 +1,31 @@
import { Switch } from 'antd';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import * as storage from '@/lib/localstorage.ts';
import { keyboardLedStatusVisibleAtom } from '@/jotai/settings.ts';
export const KeyboardLedStatusSetting = () => {
const { t } = useTranslation();
const [visible, setVisible] = useAtom(keyboardLedStatusVisibleAtom);
function update(nextVisible: boolean) {
setVisible(nextVisible);
storage.setKeyboardLedStatusVisible(nextVisible);
}
return (
<div className="mt-5 flex w-full items-center justify-between">
<div className="flex flex-col">
<span className="text-neutral-400">
{t('settings.appearance.menuBar.keyboardLedStatus')}
</span>
<span className="text-xs text-neutral-500">
{t('settings.appearance.menuBar.keyboardLedStatusDesc')}
</span>
</div>
<Switch checked={visible} onChange={update} />
</div>
);
};