Merge branch 'main' into main

This commit is contained in:
wenjie
2025-10-30 10:41:31 +08:00
committed by GitHub
49 changed files with 1157 additions and 57 deletions

View File

@@ -10,7 +10,12 @@ import { Keyboard } from '@/components/keyboard';
import { Menu } from '@/components/menu';
import { Mouse } from '@/components/mouse';
import { VirtualKeyboard } from '@/components/virtual-keyboard';
import { resolutionAtom, serialStateAtom, videoStateAtom } from '@/jotai/device.ts';
import {
resolutionAtom,
serialStateAtom,
videoScaleAtom,
videoStateAtom
} from '@/jotai/device.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { mouseStyleAtom } from '@/jotai/mouse.ts';
import { camera } from '@/libs/camera';
@@ -23,6 +28,7 @@ const App = () => {
const isBigScreen = useMediaQuery({ minWidth: 850 });
const mouseStyle = useAtomValue(mouseStyleAtom);
const videoScale = useAtomValue(videoScaleAtom);
const videoState = useAtomValue(videoStateAtom);
const serialState = useAtomValue(serialStateAtom);
const isKeyboardEnable = useAtomValue(isKeyboardEnableAtom);
@@ -107,7 +113,13 @@ const App = () => {
<video
id="video"
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }}
style={{
transform: `scale(${videoScale})`,
transformOrigin: 'center',
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'scale-down'
}}
autoPlay
playsInline
/>

View File

@@ -4,7 +4,7 @@ import { KeyboardIcon } from 'lucide-react';
import { Paste } from './paste.tsx';
import { VirtualKeyboard } from './virtual-keyboard.tsx';
import { CtrlAltDel } from './ctrl-alt-del';
import { KeyboardShortcutsMenu } from './shortcuts-menu.tsx';
export const Keyboard = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -12,8 +12,8 @@ export const Keyboard = () => {
const content = (
<>
<Paste />
<CtrlAltDel />
<VirtualKeyboard />
<KeyboardShortcutsMenu />
</>
);

View File

@@ -1,24 +1,27 @@
import { useState } from 'react';
import { SendHorizonal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { device } from '@/libs/device';
import { Modifiers } from '@/libs/device/keyboard.ts';
import { KeyboardCodes } from '@/libs/keyboard';
export const CtrlAltDel = () => {
const { t } = useTranslation();
interface ShortcutProps {
label: string;
modifiers?: Partial<Modifiers>;
keyCode: string;
}
export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
const [isLoading, setIsLoading] = useState(false);
async function ctrlAltDel(): Promise<void> {
async function handleClick(): Promise<void> {
if (isLoading) return;
setIsLoading(true);
try {
const modifiers = new Modifiers();
modifiers.leftCtrl = true;
modifiers.leftAlt = true;
await send(modifiers, KeyboardCodes.get('Delete')!);
const mods = new Modifiers();
Object.assign(mods, modifiers);
await send(mods, KeyboardCodes.get(keyCode)!);
} catch (e) {
console.log(e);
} finally {
@@ -26,20 +29,19 @@ export const CtrlAltDel = () => {
}
}
async function send(modifiers: Modifiers, code: number) {
async function send(mods: Modifiers, code: number) {
const keys = [0x00, 0x00, code, 0x00, 0x00, 0x00];
await device.sendKeyboardData(modifiers, keys);
await device.sendKeyboardData(mods, keys);
await device.sendKeyboardData(new Modifiers(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
return (
<div
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
onClick={ctrlAltDel}
onClick={handleClick}
>
<SendHorizonal size={18} />
<span>{t('keyboard.ctrlAltDel')}</span>
<span>{label}</span>
</div>
);
};

View File

@@ -0,0 +1,55 @@
import { useState } from 'react';
import { SendHorizonal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Popover } from 'antd';
import { Shortcut } from './shortcut.tsx';
export const KeyboardShortcutsMenu = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
return (
<Popover
content={
<div className="flex flex-col gap-1">
{[
{
label: t('keyboard.ctrlAltDel'),
modifiers: { leftCtrl: true, leftAlt: true },
keyCode: 'Delete',
},
{
label: t('keyboard.ctrlD'),
modifiers: { leftCtrl: true },
keyCode: 'KeyD',
},
{
label: t('keyboard.winTab'),
modifiers: { leftWindows: true },
keyCode: 'Tab',
},
].map((shortcut) => (
<Shortcut
key={shortcut.keyCode}
label={shortcut.label}
modifiers={shortcut.modifiers}
keyCode={shortcut.keyCode}
/>
))}
</div>
}
trigger="click"
placement="rightTop"
align={{ offset: [14, 0] }}
open={open}
onOpenChange={setOpen}
arrow={false}
>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
<SendHorizonal size={18} />
<span>{t('keyboard.shortcuts')}</span>
</div>
</Popover>
);
};

View File

@@ -4,6 +4,7 @@ import { useAtom, useSetAtom } from 'jotai';
import { MouseIcon } from 'lucide-react';
import {
mouseJigglerModeAtom,
mouseModeAtom,
mouseStyleAtom,
scrollDirectionAtom,
@@ -12,6 +13,7 @@ import {
import * as storage from '@/libs/storage';
import { Direction } from './direction.tsx';
import { Jiggler } from './jiggler.tsx';
import { Mode } from './mode.tsx';
import { Speed } from './speed.tsx';
import { Style } from './style.tsx';
@@ -21,6 +23,7 @@ export const Mouse = () => {
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
const setScrollDirection = useSetAtom(scrollDirectionAtom);
const setScrollInterval = useSetAtom(scrollIntervalAtom);
const [mouseJigglerMode, setMouseJigglerMode] = useAtom(mouseJigglerModeAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -48,6 +51,11 @@ export const Mouse = () => {
if (interval) {
setScrollInterval(interval);
}
const jiggler = storage.getMouseJigglerMode();
if (mouseJigglerMode !== jiggler) {
setMouseJigglerMode(jiggler);
}
}
const content = (
@@ -56,6 +64,7 @@ export const Mouse = () => {
<Mode />
<Direction />
<Speed />
<Jiggler />
</>
);

View File

@@ -0,0 +1,49 @@
import { Popover } from 'antd';
import clsx from 'clsx';
import { useAtom } from 'jotai';
import { MousePointerIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { mouseJigglerModeAtom } from '@/jotai/mouse.ts';
import * as storage from '@/libs/storage';
export const Jiggler = () => {
const { t } = useTranslation();
const [mouseJigglerMode, setMouseJigglerMode] = useAtom(mouseJigglerModeAtom);
const mouseJigglerModes = [
{ name: t('mouse.jiggler.enable'), value: 'enable' },
{ name: t('mouse.jiggler.disable'), value: 'disable' }
];
function update(mode: string) {
setMouseJigglerMode(mode);
storage.setMouseJigglerMode(mode);
}
const content = (
<>
{mouseJigglerModes.map((mode) => (
<div
key={mode.value}
className={clsx(
'my-1 flex cursor-pointer items-center space-x-1 rounded py-1 pl-2 pr-5 hover:bg-neutral-700/50',
mode.value === mouseJigglerMode ? 'text-blue-500' : 'text-neutral-300'
)}
onClick={() => update(mode.value)}
>
{mode.name}
</div>
))}
</>
);
return (
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [13, 0] }}>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50">
<div className="flex h-[14px] w-[20px] items-end">
<MousePointerIcon size={16} />
</div>
<span>{t('mouse.jiggler.title')}</span>
</div>
</Popover>
);
};

View File

@@ -3,11 +3,13 @@ import { MonitorIcon } from 'lucide-react';
import { Device } from './device.tsx';
import { Resolution } from './resolution.tsx';
import { Scale } from './scale.tsx';
export const Video = () => {
const content = (
<div className="flex flex-col space-y-1">
<Resolution />
<Scale />
<Device />
</div>
);

View File

@@ -0,0 +1,58 @@
import { ReactElement, useEffect } from 'react'
import { Popover, Slider } from 'antd'
import { useAtom } from 'jotai'
import { ScalingIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { videoScaleAtom } from '@/jotai/device.ts';
import * as storage from '@/libs/storage';
export const Scale = (): ReactElement => {
const { t } = useTranslation()
const [videoScale, setVideoScale] = useAtom(videoScaleAtom)
useEffect(() => {
const scale = storage.getVideoScale()
if (scale) {
setVideoScale(scale)
}
}, [])
async function updateScale(scale: number): Promise<void> {
setVideoScale(scale)
storage.setVideoScale(scale)
}
const content = (
<div className="h-[150px] w-[60px] py-3">
<Slider
vertical
marks={{
0.5: <span>x0.5</span>,
1: <span>x1.0</span>,
1.5: <span>x1.5</span>,
2: <span>x2.0</span>
}}
range={false}
included={false}
min={0.5}
max={2}
step={0.1}
defaultValue={videoScale}
onChange={updateScale}
/>
</div>
)
return (
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [13, 0] }}>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50">
<div className="flex h-[14px] w-[20px] items-end">
<ScalingIcon size={16} />
</div>
<span>{t('video.scale')}</span>
</div>
</Popover>
)
}

View File

@@ -1,8 +1,13 @@
import { useEffect, useRef } from 'react';
import { useAtomValue } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { resolutionAtom } from '@/jotai/device.ts';
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
import {
mouseJigglerModeAtom,
mouseLastMoveTimeAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.ts';
import { device } from '@/libs/device';
import { Key } from '@/libs/device/mouse.ts';
@@ -10,6 +15,8 @@ export const Absolute = () => {
const resolution = useAtomValue(resolutionAtom);
const scrollDirection = useAtomValue(scrollDirectionAtom);
const scrollInterval = useAtomValue(scrollIntervalAtom);
const mouseJigglerMode = useAtomValue(mouseJigglerModeAtom);
const setMouseLastMoveTime = useSetAtom(mouseLastMoveTimeAtom);
const keyRef = useRef<Key>(new Key());
const lastScrollTimeRef = useRef(0);
@@ -74,6 +81,11 @@ export const Absolute = () => {
async function handleMouseMove(event: any) {
disableEvent(event);
await send(event);
// mouse jiggler record last move time
if (mouseJigglerMode === 'enable') {
setMouseLastMoveTime(Date.now());
}
}
// mouse scroll
@@ -109,7 +121,7 @@ export const Absolute = () => {
canvas.removeEventListener('click', disableEvent);
canvas.removeEventListener('contextmenu', disableEvent);
};
}, [resolution, scrollDirection, scrollInterval]);
}, [resolution, scrollDirection, scrollInterval, mouseJigglerMode, setMouseLastMoveTime]);
// disable default events
function disableEvent(event: any) {

View File

@@ -1,6 +1,15 @@
import { useAtomValue } from 'jotai';
import { useEffect, useRef } from 'react';
import { useAtom, useAtomValue } from 'jotai';
import { mouseModeAtom } from '@/jotai/mouse.ts';
import {
mouseJigglerIntervalAtom,
mouseJigglerModeAtom,
mouseJigglerTimerAtom,
mouseLastMoveTimeAtom,
mouseModeAtom
} from '@/jotai/mouse.ts';
import { device } from '@/libs/device/index.ts';
import { Key } from '@/libs/device/mouse.ts';
import { Absolute } from './absolute.tsx';
import { Relative } from './relative.tsx';
@@ -8,5 +17,48 @@ import { Relative } from './relative.tsx';
export const Mouse = () => {
const mouseMode = useAtomValue(mouseModeAtom);
// mouse jiggler
const mouseJigglerMode = useAtomValue(mouseJigglerModeAtom);
const [mouseJigglerTimer, setMouseJigglerTimer] = useAtom(mouseJigglerTimerAtom);
const mouseJigglerInterval = useAtomValue(mouseJigglerIntervalAtom);
const mouseLastMoveTime = useAtomValue(mouseLastMoveTimeAtom);
const mouseLastMoveTimeRef = useRef(mouseLastMoveTime);
const emptyKeyRef = useRef<Key>(new Key());
useEffect(() => {
// sync mouseLastMoveTime through ref
mouseLastMoveTimeRef.current = mouseLastMoveTime;
}, [mouseLastMoveTime]);
useEffect(() => {
async function jigglerTimerCallback() {
if (Date.now() - mouseLastMoveTimeRef.current < mouseJigglerInterval) {
return;
}
const rect = document.getElementById('video')!.getBoundingClientRect();
await device.sendMouseAbsoluteData(
emptyKeyRef.current,
rect.width,
rect.height,
rect.width / 2,
rect.height / 2,
0
);
}
// configure interval timer
if (mouseJigglerMode === 'enable') {
if (mouseJigglerTimer === null) {
const timer = setInterval(jigglerTimerCallback, mouseJigglerInterval);
setMouseJigglerTimer(timer);
}
} else {
if (mouseJigglerTimer) {
clearInterval(mouseJigglerTimer);
setMouseJigglerTimer(null);
}
}
}, [mouseJigglerMode]);
return <>{mouseMode === 'relative' ? <Relative /> : <Absolute />}</>;
};

View File

@@ -1,10 +1,15 @@
import { useEffect, useRef } from 'react';
import { message } from 'antd';
import { useAtomValue } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import { resolutionAtom } from '@/jotai/device.ts';
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
import {
mouseJigglerModeAtom,
mouseLastMoveTimeAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.ts';
import { device } from '@/libs/device';
import { Key } from '@/libs/device/mouse.ts';
@@ -15,6 +20,8 @@ export const Relative = () => {
const resolution = useAtomValue(resolutionAtom);
const scrollDirection = useAtomValue(scrollDirectionAtom);
const scrollInterval = useAtomValue(scrollIntervalAtom);
const mouseJigglerMode = useAtomValue(mouseJigglerModeAtom);
const setMouseLastMoveTime = useSetAtom(mouseLastMoveTimeAtom);
const isLockedRef = useRef(false);
const keyRef = useRef<Key>(new Key());
@@ -110,6 +117,11 @@ export const Relative = () => {
if (x === 0 && y === 0) return;
await send(Math.abs(x) < 10 ? x * 2 : x, Math.abs(y) < 10 ? y * 2 : y, 0);
// mouse jiggler record last move time
if (mouseJigglerMode === 'enable') {
setMouseLastMoveTime(Date.now());
}
}
// mouse scroll
@@ -138,7 +150,7 @@ export const Relative = () => {
canvas.removeEventListener('wheel', handleWheel);
canvas.removeEventListener('contextmenu', disableEvent);
};
}, [resolution, scrollDirection, scrollInterval]);
}, [resolution, scrollDirection, scrollInterval, mouseJigglerMode, setMouseLastMoveTime]);
async function send(x: number, y: number, scroll: number) {
await device.sendMouseRelativeData(keyRef.current, x, y, scroll);

View File

@@ -2,9 +2,12 @@ const languages = [
{ key: 'en', name: 'English' },
{ key: 'ru', name: 'Русский' },
{ key: 'zh', name: '中文' },
{ key: 'zh-TW', name: '繁體中文' }, // 新增繁體中文
{ key: 'de', name: 'Deutsch' },
{ key: 'nl', name: 'Nederlands' },
{ key: 'be', name: 'België' },
{ key: 'ko', name: '한국어' },
{ key: 'pt_BR', name: 'Português (Brasil)' },
{ ket: 'pl', name: 'Polski' }
];

View File

@@ -24,6 +24,7 @@ const be = {
},
video: {
resolution: 'Resolutie',
scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Toestel',
custom: {
@@ -37,7 +38,10 @@ const be = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel klavier',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcuts: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -62,5 +66,5 @@ const be = {
}
}
};
export default be;
export default be;

View File

@@ -24,6 +24,7 @@ const de = {
},
video: {
resolution: 'Auflösung',
scale: 'Skalierung',
customResolution: 'Benutzerdefiniert',
device: 'Gerät',
custom: {
@@ -37,7 +38,10 @@ const de = {
keyboard: {
paste: 'Einfügen',
virtualKeyboard: 'Virtuelle Tastatur',
ctrlAltDel: 'Strg + Alt + Entfernen'
shortcuts: 'Tastenkürzel',
ctrlAltDel: 'Strg + Alt + Entfernen',
ctrlD: 'Strg + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -53,6 +57,9 @@ const de = {
direction: 'Scrollrichtung',
scrollUp: 'Hochscrollen',
scrollDown: 'Runterscrollen',
speed: 'Scrollgeschwindigkeit',
fast: 'Schnell',
slow: 'Langsam',
requestPointer: 'Benutze relativen Modus. Bitte auf den Desktop klicken, um den Mauszeiger anzuzeigen.'
},
settings: {
@@ -62,5 +69,5 @@ const de = {
}
}
};
export default de;
export default de;

View File

@@ -24,6 +24,7 @@ const en = {
},
video: {
resolution: 'Resolution',
scale: 'Scale',
customResolution: 'Custom',
device: 'Device',
custom: {
@@ -37,7 +38,10 @@ const en = {
keyboard: {
paste: 'Paste',
virtualKeyboard: 'Keyboard',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcuts: 'Shortcuts',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -56,7 +60,12 @@ const en = {
speed: 'Wheel speed',
fast: 'Fast',
slow: 'Slow',
requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.'
requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.',
jiggler: {
title: 'Mouse Jiggler',
enable: 'Enable',
disable: 'Disable'
}
},
settings: {
language: 'Language',

View File

@@ -0,0 +1,70 @@
const ko = {
translation: {
serial: {
notSupported:
'시리얼이 지원되지 않습니다. 마우스와 키보드를 사용하려면 Chrome 브라우저를 사용하세요.',
failed: '시리얼 연결에 실패했습니다. 다시 시도해 주세요.'
},
camera: {
tip: '권한을 기다리는 중...',
denied: '권한이 거부되었습니다.',
authorize:
'Target PC 연결에 카메라 권한이 필요합니다. 브라우저 설정에서 카메라 권한을 허용해 주세요.',
failed: '카메라 연결에 실패했습니다. 다시 시도해 주세요.'
},
modal: {
title: 'USB 장치 선택',
selectVideo: '비디오 입력 장치를 선택해 주세요.',
selectSerial: '시리얼 장치를 선택해 주세요.'
},
menu: {
serial: '시리얼',
keyboard: '키보드',
mouse: '마우스'
},
video: {
resolution: '해상도',
scale: '배율',
customResolution: '사용자 정의',
device: '장치',
custom: {
title: '사용자 정의 해상도',
width: '가로',
height: '세로',
confirm: '확인',
cancel: '취소'
}
},
keyboard: {
paste: '붙여넣기',
virtualKeyboard: '가상 키보드',
ctrlAltDel: 'Ctrl + Alt + Delete'
},
mouse: {
cursor: {
title: '커서 모양',
pointer: '포인터',
grab: '손',
cell: '플러스',
hide: '숨기기'
},
mode: '마우스 모드',
absolute: '절대 모드',
relative: '상대 모드',
direction: '휠 방향',
scrollUp: '위로 스크롤',
scrollDown: '아래로 스크롤',
speed: '휠 속도',
fast: '빠르게',
slow: '느리게',
requestPointer: '상대 모드를 사용 중입니다. 마우스 포인터를 가져오려면 스크린을 클릭하세요.'
},
settings: {
language: '언어',
document: '문서',
download: '다운로드'
}
}
};
export default ko;

View File

@@ -24,6 +24,7 @@ const nl = {
},
video: {
resolution: 'Resolutie',
scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Apparaat',
custom: {
@@ -37,7 +38,10 @@ const nl = {
keyboard: {
paste: 'Plakken',
virtualKeyboard: 'Virtueel toetsenbord',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcuts: 'Sneltoetsen',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -62,5 +66,5 @@ const nl = {
}
}
};
export default nl;
export default nl;

View File

@@ -0,0 +1,69 @@
const pt_BR = {
translation: {
serial: {
notSupported:
'Serial não suportado. Use o navegador Chrome para habilitar o mouse e teclado.',
failed: 'Falha ao se conectar na porta serial. Tente novamente'
},
camera: {
tip: 'Esperando autorização...',
denied: 'Permissão negada',
authorize:
'A área de trabalho remota requer permissão de uso da câmera. Por favor, conceda as permissões de câmera nos ajustes do navegador.',
failed: 'Falha ao se conectar à câmera. Tente novamente.'
},
modal: {
title: 'Selecione o dispositivo USB',
selectVideo: 'Selecione um dispositivo de vídeo de entrada',
selectSerial: 'Selecione um dispositivo serial'
},
menu: {
serial: 'Serial',
keyboard: 'Teclado',
mouse: 'Mouse'
},
video: {
resolution: 'Resolução',
customResolution: 'Personalizada',
device: 'Dispositivo',
custom: {
title: 'Resolução Personalizada',
width: 'Largura',
height: 'Altura',
confirm: 'Ok',
cancel: 'Cancelar'
}
},
keyboard: {
paste: 'Colar',
virtualKeyboard: 'Teclado Virtual',
ctrlAltDel: 'Ctrl + Alt + Delete'
},
mouse: {
cursor: {
title: 'Cursor',
pointer: 'Ponteiro',
grab: 'Mão',
cell: 'Célula',
hide: 'Esconder'
},
mode: 'Modo do mouse',
absolute: 'Modo absoluto',
relative: 'Modo relativo',
direction: 'Direção do scroll',
scrollUp: 'Scroll para cima',
scrollDown: 'Scroll para baixo',
speed: 'Velocidade do scroll',
fast: 'Rápido',
slow: 'Lento',
requestPointer: 'Usando modo relativo. Por favor, clique na área de trabalho para restaurar o ponteiro do mouse.'
},
settings: {
language: 'Linguagem',
document: 'Documentação',
download: 'Download'
}
}
};
export default pt_BR;

View File

@@ -24,6 +24,7 @@ const ru = {
},
video: {
resolution: 'Разрешение',
scale: 'Масштаб',
customResolution: 'Пользовательское',
device: 'Видеоустройство',
custom: {
@@ -37,7 +38,10 @@ const ru = {
keyboard: {
paste: 'Вставить текст',
virtualKeyboard: 'Виртуальная клавиатура',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcuts: 'Сочетания клавиш',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -53,6 +57,9 @@ const ru = {
direction: 'Направление прокрутки',
scrollUp: 'Обычное',
scrollDown: 'Инвертированное',
speed: 'Скорость прокрутки',
fast: 'Быстро',
slow: 'Медленно',
requestPointer: 'Используется относительное позиционирование мыши. Чтобы захватить курсор, щелкните по видео на экране'
},
settings: {

View File

@@ -0,0 +1,67 @@
const zh-TW = {
translation: {
serial: {
notSupported: '當前瀏覽器不支援序列埠,無法使用鍵鼠。請使用桌面版 Chrome 瀏覽器。',
failed: '序列埠連線失敗,請重試。'
},
camera: {
tip: '等待授權中...',
denied: '權限不足',
authorize: '遠端桌面需要取得攝影機權限,請在瀏覽器設定中允許使用攝影機。',
failed: '攝影機連線失敗,請重試。'
},
modal: {
title: '選擇 USB 裝置',
selectVideo: '請選擇視訊輸入裝置',
selectSerial: '選擇序列埠裝置'
},
menu: {
serial: '序列埠',
keyboard: '鍵盤',
mouse: '滑鼠'
},
video: {
resolution: '解析度',
customResolution: '自訂',
device: '裝置',
custom: {
title: '自訂解析度',
width: '寬度',
height: '高度',
confirm: '確定',
cancel: '取消'
}
},
keyboard: {
paste: '貼上',
virtualKeyboard: '虛擬鍵盤',
ctrlAltDel: 'Ctrl + Alt + Delete'
},
mouse: {
cursor: {
title: '滑鼠指標',
pointer: '箭頭',
grab: '抓取',
cell: '格線',
hide: '隱藏'
},
mode: '滑鼠模式',
absolute: '絕對模式',
relative: '相對模式',
direction: '滾輪方向',
scrollUp: '向上',
scrollDown: '向下',
speed: '滾輪速度',
fast: '快',
slow: '慢',
requestPointer: '正在使用滑鼠相對模式,請點擊桌面取得滑鼠指標。'
},
settings: {
language: '語言',
document: '文件',
download: '下載'
}
}
};
export default zh-TW;

View File

@@ -22,6 +22,7 @@ const zh = {
},
video: {
resolution: '分辨率',
scale: '缩放',
customResolution: '自定义',
device: '设备',
custom: {
@@ -35,7 +36,10 @@ const zh = {
keyboard: {
paste: '粘贴',
virtualKeyboard: '虚拟键盘',
ctrlAltDel: 'Ctrl + Alt + Delete'
shortcuts: '快捷键',
ctrlAltDel: 'Ctrl + Alt + Delete',
ctrlD: 'Ctrl + D',
winTab: 'Win + Tab',
},
mouse: {
cursor: {
@@ -54,7 +58,12 @@ const zh = {
speed: '滚轮速度',
fast: '快',
slow: '慢',
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。'
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
jiggler: {
title: '闲时晃动',
enable: '启用',
disable: '禁用'
}
},
settings: {
language: '语言',

View File

@@ -10,6 +10,8 @@ export const resolutionAtom = atom<Resolution>({
height: 1080
});
export const videoScaleAtom = atom<number>(1.0)
export const videoDeviceIdAtom = atom('');
export const videoStateAtom = atom<VideoState>('disconnected');

View File

@@ -9,6 +9,17 @@ export const mouseModeAtom = atom('absolute');
// mouse scroll direction: 1 or -1
export const scrollDirectionAtom = atom(1);
// mouse scroll interval (unit: ms)
// mouse scroll interval (unit: ms)
export const scrollIntervalAtom = atom(0);
// mouse jiggler mode: enable or disable
export const mouseJigglerModeAtom = atom('disable');
// mouse jiggler timer id
export const mouseJigglerTimerAtom = atom<number | null>(null);
// mouse jiggler interval (unit: ms)
export const mouseJigglerIntervalAtom = atom(15_000);
// mouse jiggler last move time
export const mouseLastMoveTimeAtom = atom(0);

View File

@@ -4,11 +4,13 @@ const LANGUAGE_KEY = 'nanokvm-usb-language';
const VIDEO_DEVICE_ID_KEY = 'nanokvm-usb-video-device-id';
const VIDEO_RESOLUTION_KEY = 'nanokvm-usb-video-resolution';
const CUSTOM_RESOLUTION_KEY = 'nanokvm-usb-custom-resolution';
const VIDEO_SCALE_KEY = 'nanokvm-usb-video-scale'
const IS_MENU_OPEN_KEY = 'nanokvm-is-menu-open';
const MOUSE_STYLE_KEY = 'nanokvm-usb-mouse-style';
const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode';
const MOUSE_SCROLL_DIRECTION_KEY = 'nanokvm-usb-mouse-scroll-direction';
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-usb-mouse-scroll-interval';
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode';
export function getLanguage() {
return localStorage.getItem(LANGUAGE_KEY);
@@ -56,6 +58,18 @@ export function removeCustomResolutions() {
localStorage.removeItem(CUSTOM_RESOLUTION_KEY);
}
export function getVideoScale(): number | null {
const scale = localStorage.getItem(VIDEO_SCALE_KEY)
if (scale && Number(scale)) {
return Number(scale)
}
return null
}
export function setVideoScale(scale: number): void {
localStorage.setItem(VIDEO_SCALE_KEY, String(scale))
}
export function getIsMenuOpen(): boolean {
const state = localStorage.getItem(IS_MENU_OPEN_KEY);
if (!state) {
@@ -107,3 +121,12 @@ export function getMouseScrollInterval(): number | null {
export function setMouseScrollInterval(interval: number): void {
localStorage.setItem(MOUSE_SCROLL_INTERVAL_KEY, String(interval));
}
export function getMouseJigglerMode(): string {
const jiggler = localStorage.getItem(MOUSE_JIGGLER_MODE_KEY);
return jiggler && jiggler === 'enable' ? 'enable' : 'disable';
}
export function setMouseJigglerMode(jiggler: string): void {
localStorage.setItem(MOUSE_JIGGLER_MODE_KEY, jiggler);
}