{
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 (
- {t('keyboard.ctrlAltDel')}
+ {label}
);
};
diff --git a/browser/src/components/menu/keyboard/shortcuts-menu.tsx b/browser/src/components/menu/keyboard/shortcuts-menu.tsx
new file mode 100644
index 0000000..6f960cf
--- /dev/null
+++ b/browser/src/components/menu/keyboard/shortcuts-menu.tsx
@@ -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 (
+
+ {[
+ {
+ 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) => (
+
+ ))}
+
+ }
+ trigger="click"
+ placement="rightTop"
+ align={{ offset: [14, 0] }}
+ open={open}
+ onOpenChange={setOpen}
+ arrow={false}
+ >
+
+
+ {t('keyboard.shortcuts')}
+
+
+ );
+};
diff --git a/browser/src/components/menu/mouse/index.tsx b/browser/src/components/menu/mouse/index.tsx
index 4c0bbf4..d5ba87a 100644
--- a/browser/src/components/menu/mouse/index.tsx
+++ b/browser/src/components/menu/mouse/index.tsx
@@ -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 = () => {
+
>
);
diff --git a/browser/src/components/menu/mouse/jiggler.tsx b/browser/src/components/menu/mouse/jiggler.tsx
new file mode 100644
index 0000000..b3c74f5
--- /dev/null
+++ b/browser/src/components/menu/mouse/jiggler.tsx
@@ -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) => (
+ update(mode.value)}
+ >
+ {mode.name}
+
+ ))}
+ >
+ );
+ return (
+
+
+
+
+
+
{t('mouse.jiggler.title')}
+
+
+ );
+};
diff --git a/browser/src/components/menu/video/index.tsx b/browser/src/components/menu/video/index.tsx
index 8b78ed9..dd44766 100644
--- a/browser/src/components/menu/video/index.tsx
+++ b/browser/src/components/menu/video/index.tsx
@@ -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 = (
+
);
diff --git a/browser/src/components/menu/video/scale.tsx b/browser/src/components/menu/video/scale.tsx
new file mode 100644
index 0000000..d21f5e3
--- /dev/null
+++ b/browser/src/components/menu/video/scale.tsx
@@ -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 {
+ setVideoScale(scale)
+ storage.setVideoScale(scale)
+ }
+
+ const content = (
+
+ x0.5,
+ 1: x1.0,
+ 1.5: x1.5,
+ 2: x2.0
+ }}
+ range={false}
+ included={false}
+ min={0.5}
+ max={2}
+ step={0.1}
+ defaultValue={videoScale}
+ onChange={updateScale}
+ />
+
+ )
+
+ return (
+
+
+
+
+
+
{t('video.scale')}
+
+
+ )
+}
diff --git a/browser/src/components/mouse/absolute.tsx b/browser/src/components/mouse/absolute.tsx
index d5a9bd0..7f4ab4b 100644
--- a/browser/src/components/mouse/absolute.tsx
+++ b/browser/src/components/mouse/absolute.tsx
@@ -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(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) {
diff --git a/browser/src/components/mouse/index.tsx b/browser/src/components/mouse/index.tsx
index ddbd4ae..32e558a 100644
--- a/browser/src/components/mouse/index.tsx
+++ b/browser/src/components/mouse/index.tsx
@@ -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(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' ? : }>;
};
diff --git a/browser/src/components/mouse/relative.tsx b/browser/src/components/mouse/relative.tsx
index 0caecba..22bf8d6 100644
--- a/browser/src/components/mouse/relative.tsx
+++ b/browser/src/components/mouse/relative.tsx
@@ -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(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);
diff --git a/browser/src/i18n/languages.ts b/browser/src/i18n/languages.ts
index 65ae314..3ed0bcb 100644
--- a/browser/src/i18n/languages.ts
+++ b/browser/src/i18n/languages.ts
@@ -2,10 +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: 'pt_BR', name: 'Português (Brasil)' },
+ { key: 'ko', name: '한국어' },
+ { key: 'pt_BR', name: 'Português (Brasil)' }
];
languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
diff --git a/browser/src/i18n/locales/be.ts b/browser/src/i18n/locales/be.ts
index 094db16..d08ed39 100644
--- a/browser/src/i18n/locales/be.ts
+++ b/browser/src/i18n/locales/be.ts
@@ -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;
\ No newline at end of file
+
+ export default be;
diff --git a/browser/src/i18n/locales/de.ts b/browser/src/i18n/locales/de.ts
index 79dbb70..9d23237 100644
--- a/browser/src/i18n/locales/de.ts
+++ b/browser/src/i18n/locales/de.ts
@@ -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;
\ No newline at end of file
+
+ export default de;
diff --git a/browser/src/i18n/locales/en.ts b/browser/src/i18n/locales/en.ts
index 59ea63b..daa9767 100644
--- a/browser/src/i18n/locales/en.ts
+++ b/browser/src/i18n/locales/en.ts
@@ -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',
diff --git a/browser/src/i18n/locales/ko.ts b/browser/src/i18n/locales/ko.ts
new file mode 100644
index 0000000..d1be2ed
--- /dev/null
+++ b/browser/src/i18n/locales/ko.ts
@@ -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;
diff --git a/browser/src/i18n/locales/nl.ts b/browser/src/i18n/locales/nl.ts
index abab0ea..2235d17 100644
--- a/browser/src/i18n/locales/nl.ts
+++ b/browser/src/i18n/locales/nl.ts
@@ -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;
\ No newline at end of file
+
+ export default nl;
diff --git a/browser/src/i18n/locales/ru.ts b/browser/src/i18n/locales/ru.ts
index c1ee4c8..0e8a299 100644
--- a/browser/src/i18n/locales/ru.ts
+++ b/browser/src/i18n/locales/ru.ts
@@ -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: {
diff --git a/browser/src/i18n/locales/zh-TW.ts b/browser/src/i18n/locales/zh-TW.ts
new file mode 100644
index 0000000..3270f50
--- /dev/null
+++ b/browser/src/i18n/locales/zh-TW.ts
@@ -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;
diff --git a/browser/src/i18n/locales/zh.ts b/browser/src/i18n/locales/zh.ts
index e7f1f85..51a5ce9 100644
--- a/browser/src/i18n/locales/zh.ts
+++ b/browser/src/i18n/locales/zh.ts
@@ -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: '语言',
diff --git a/browser/src/jotai/device.ts b/browser/src/jotai/device.ts
index a768ee3..195dc28 100644
--- a/browser/src/jotai/device.ts
+++ b/browser/src/jotai/device.ts
@@ -10,6 +10,8 @@ export const resolutionAtom = atom({
height: 1080
});
+export const videoScaleAtom = atom(1.0)
+
export const videoDeviceIdAtom = atom('');
export const videoStateAtom = atom('disconnected');
diff --git a/browser/src/jotai/mouse.ts b/browser/src/jotai/mouse.ts
index 7678c93..162484a 100644
--- a/browser/src/jotai/mouse.ts
+++ b/browser/src/jotai/mouse.ts
@@ -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(null);
+
+// mouse jiggler interval (unit: ms)
+export const mouseJigglerIntervalAtom = atom(15_000);
+
+// mouse jiggler last move time
+export const mouseLastMoveTimeAtom = atom(0);
diff --git a/browser/src/libs/storage/index.ts b/browser/src/libs/storage/index.ts
index 9397942..01543d7 100644
--- a/browser/src/libs/storage/index.ts
+++ b/browser/src/libs/storage/index.ts
@@ -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);
+}
diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx
index 97f9bb4..7ab90ac 100644
--- a/desktop/src/renderer/src/App.tsx
+++ b/desktop/src/renderer/src/App.tsx
@@ -11,7 +11,12 @@ import { Keyboard } from '@renderer/components/keyboard'
import { Menu } from '@renderer/components/menu'
import { Mouse } from '@renderer/components/mouse'
import { VirtualKeyboard } from '@renderer/components/virtual-keyboard'
-import { resolutionAtom, serialPortStateAtom, videoStateAtom } from '@renderer/jotai/device'
+import {
+ resolutionAtom,
+ serialPortStateAtom,
+ videoScaleAtom,
+ videoStateAtom
+} from '@renderer/jotai/device'
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
import { mouseStyleAtom } from '@renderer/jotai/mouse'
import { camera } from '@renderer/libs/camera'
@@ -24,6 +29,7 @@ const App = (): ReactElement => {
const { t } = useTranslation()
const isBigScreen = useMediaQuery({ minWidth: 850 })
+ const videoScale = useAtomValue(videoScaleAtom)
const videoState = useAtomValue(videoStateAtom)
const serialPortState = useAtomValue(serialPortStateAtom)
const mouseStyle = useAtomValue(mouseStyleAtom)
@@ -115,7 +121,13 @@ const App = (): ReactElement => {
diff --git a/desktop/src/renderer/src/components/menu/mouse/index.tsx b/desktop/src/renderer/src/components/menu/mouse/index.tsx
index 46dc0ce..12e7329 100644
--- a/desktop/src/renderer/src/components/menu/mouse/index.tsx
+++ b/desktop/src/renderer/src/components/menu/mouse/index.tsx
@@ -4,14 +4,17 @@ import { useAtom, useSetAtom } from 'jotai'
import { MouseIcon } from 'lucide-react'
import {
+ mouseJigglerModeAtom,
mouseModeAtom,
mouseStyleAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@renderer/jotai/mouse'
+import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
import * as storage from '@renderer/libs/storage'
import { Direction } from './direction'
+import { Jiggler } from './jiggler'
import { Mode } from './mode'
import { Speed } from './speed'
import { Style } from './style'
@@ -21,6 +24,7 @@ export const Mouse = (): ReactElement => {
const setMouseMode = useSetAtom(mouseModeAtom)
const setScrollDirection = useSetAtom(scrollDirectionAtom)
const setScrollInterval = useSetAtom(scrollIntervalAtom)
+ const setMouseJigglerMode = useSetAtom(mouseJigglerModeAtom)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
@@ -44,6 +48,9 @@ export const Mouse = (): ReactElement => {
if (interval) {
setScrollInterval(interval)
}
+ const jiggler = storage.getMouseJigglerMode()
+ mouseJiggler.setMode(jiggler)
+ setMouseJigglerMode(jiggler)
}, [])
const content = (
@@ -52,6 +59,7 @@ export const Mouse = (): ReactElement => {
+
)
diff --git a/desktop/src/renderer/src/components/menu/mouse/jiggler.tsx b/desktop/src/renderer/src/components/menu/mouse/jiggler.tsx
new file mode 100644
index 0000000..5d9bcbf
--- /dev/null
+++ b/desktop/src/renderer/src/components/menu/mouse/jiggler.tsx
@@ -0,0 +1,56 @@
+import { ReactElement, useEffect } from 'react'
+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 '@renderer/jotai/mouse'
+import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
+import * as storage from '@renderer/libs/storage'
+
+export const Jiggler = (): ReactElement => {
+ const { t } = useTranslation()
+ const [jigglerMode, setJigglerMode] = useAtom(mouseJigglerModeAtom)
+
+ const mouseJigglerModes: { name: string; value: 'enable' | 'disable' }[] = [
+ { name: t('mouse.jiggler.enable'), value: 'enable' },
+ { name: t('mouse.jiggler.disable'), value: 'disable' }
+ ]
+
+ function update(mode: 'enable' | 'disable'): void {
+ storage.setMouseJigglerMode(mode)
+ setJigglerMode(mode)
+ }
+
+ useEffect(() => {
+ mouseJiggler.setMode(jigglerMode)
+ }, [jigglerMode])
+
+ const content = (
+ <>
+ {mouseJigglerModes.map((mode) => (
+ update(mode.value)}
+ >
+ {mode.name}
+
+ ))}
+ >
+ )
+ return (
+
+
+
+
+
+
{t('mouse.jiggler.title')}
+
+
+ )
+}
diff --git a/desktop/src/renderer/src/components/menu/video/index.tsx b/desktop/src/renderer/src/components/menu/video/index.tsx
index 45896b5..8bcf218 100644
--- a/desktop/src/renderer/src/components/menu/video/index.tsx
+++ b/desktop/src/renderer/src/components/menu/video/index.tsx
@@ -4,11 +4,13 @@ import { MonitorIcon } from 'lucide-react'
import { Device } from './device'
import { Resolution } from './resolution'
+import { Scale } from './scale'
export const Video = (): ReactElement => {
const content = (
+
)
diff --git a/desktop/src/renderer/src/components/menu/video/scale.tsx b/desktop/src/renderer/src/components/menu/video/scale.tsx
new file mode 100644
index 0000000..7c39b15
--- /dev/null
+++ b/desktop/src/renderer/src/components/menu/video/scale.tsx
@@ -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 '@renderer/jotai/device'
+import * as storage from '@renderer/libs/storage'
+
+export const Scale = (): ReactElement => {
+ const { t } = useTranslation()
+
+ const [videoScale, setVideoScale] = useAtom(videoScaleAtom)
+
+ useEffect(() => {
+ const scale = storage.getVideoScale()
+ if (scale) {
+ setVideoScale(scale)
+ }
+ }, [setVideoScale])
+
+ async function updateScale(scale: number): Promise {
+ setVideoScale(scale)
+ storage.setVideoScale(scale)
+ }
+
+ const content = (
+
+ x0.5,
+ 1: x1.0,
+ 1.5: x1.5,
+ 2: x2.0
+ }}
+ range={false}
+ included={false}
+ min={0.5}
+ max={2}
+ step={0.1}
+ defaultValue={videoScale}
+ onChange={updateScale}
+ />
+
+ )
+
+ return (
+
+
+
+
+
+
{t('video.scale')}
+
+
+ )
+}
diff --git a/desktop/src/renderer/src/components/mouse/absolute.tsx b/desktop/src/renderer/src/components/mouse/absolute.tsx
index 4a2c671..525035d 100644
--- a/desktop/src/renderer/src/components/mouse/absolute.tsx
+++ b/desktop/src/renderer/src/components/mouse/absolute.tsx
@@ -4,6 +4,7 @@ import { useAtomValue } from 'jotai'
import { IpcEvents } from '@common/ipc-events'
import { resolutionAtom } from '@renderer/jotai/device'
import { scrollDirectionAtom, scrollIntervalAtom } from '@renderer/jotai/mouse'
+import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
import type { Mouse as MouseKey } from '@renderer/types'
export const Absolute = (): ReactElement => {
@@ -77,6 +78,8 @@ export const Absolute = (): ReactElement => {
async function handleMouseMove(event: MouseEvent): Promise {
disableEvent(event)
await send(event)
+
+ mouseJiggler.moveEventCallback()
}
// mouse scroll
diff --git a/desktop/src/renderer/src/components/mouse/relative.tsx b/desktop/src/renderer/src/components/mouse/relative.tsx
index 5bdd7b1..a270fec 100644
--- a/desktop/src/renderer/src/components/mouse/relative.tsx
+++ b/desktop/src/renderer/src/components/mouse/relative.tsx
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
import { IpcEvents } from '@common/ipc-events'
import { resolutionAtom } from '@renderer/jotai/device'
import { scrollDirectionAtom, scrollIntervalAtom } from '@renderer/jotai/mouse'
+import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
import type { Mouse as MouseKey } from '@renderer/types'
export const Relative = (): ReactElement => {
@@ -110,6 +111,8 @@ export const Relative = (): ReactElement => {
if (x === 0 && y === 0) return
await send(Math.abs(x) < 10 ? x * 2 : x, Math.abs(y) < 10 ? y * 2 : y, 0)
+
+ mouseJiggler.moveEventCallback()
}
async function handleWheel(event: WheelEvent): Promise {
diff --git a/desktop/src/renderer/src/i18n/languages.ts b/desktop/src/renderer/src/i18n/languages.ts
index 260de67..b068a57 100644
--- a/desktop/src/renderer/src/i18n/languages.ts
+++ b/desktop/src/renderer/src/i18n/languages.ts
@@ -2,12 +2,14 @@ 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: 'pt_BR', name: 'Português (Brasil)' },
+ { key: 'ko', name: '한국어' },
+ { key: 'pt_BR', name: 'Português (Brasil)' }
]
-languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
+languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
-export default languages
+export default languages;
diff --git a/desktop/src/renderer/src/i18n/locales/be.ts b/desktop/src/renderer/src/i18n/locales/be.ts
index ce82906..5858b17 100644
--- a/desktop/src/renderer/src/i18n/locales/be.ts
+++ b/desktop/src/renderer/src/i18n/locales/be.ts
@@ -19,6 +19,7 @@ const be = {
},
video: {
resolution: 'Resolutie',
+ scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Toestel',
custom: {
@@ -76,5 +77,5 @@ const be = {
}
}
}
-
- export default be
\ No newline at end of file
+
+ export default be
diff --git a/desktop/src/renderer/src/i18n/locales/de.ts b/desktop/src/renderer/src/i18n/locales/de.ts
index 8a1ac47..a80f385 100644
--- a/desktop/src/renderer/src/i18n/locales/de.ts
+++ b/desktop/src/renderer/src/i18n/locales/de.ts
@@ -19,6 +19,7 @@ const de = {
},
video: {
resolution: 'Auflösung',
+ scale: 'Skalierung',
customResolution: 'Benutzerdefiniert',
device: 'Gerät',
custom: {
@@ -48,6 +49,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: {
@@ -76,5 +80,5 @@ const de = {
}
}
}
-
- export default de
\ No newline at end of file
+
+ export default de
diff --git a/desktop/src/renderer/src/i18n/locales/en.ts b/desktop/src/renderer/src/i18n/locales/en.ts
index 707c660..033e42d 100644
--- a/desktop/src/renderer/src/i18n/locales/en.ts
+++ b/desktop/src/renderer/src/i18n/locales/en.ts
@@ -19,6 +19,7 @@ const en = {
},
video: {
resolution: 'Resolution',
+ scale: 'Scale',
customResolution: 'Custom',
device: 'Device',
custom: {
@@ -51,7 +52,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: {
title: 'Settings',
diff --git a/desktop/src/renderer/src/i18n/locales/ko.ts b/desktop/src/renderer/src/i18n/locales/ko.ts
new file mode 100644
index 0000000..6b58ec8
--- /dev/null
+++ b/desktop/src/renderer/src/i18n/locales/ko.ts
@@ -0,0 +1,84 @@
+const ko = {
+ translation: {
+ 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: {
+ title: '설정',
+ appearance: {
+ title: '화면 설정',
+ language: '언어',
+ menu: '메뉴 바',
+ menuTips: '시작 시 메뉴 바 열기'
+ },
+ update: {
+ title: '업데이트 확인',
+ latest: '최신 버전을 사용 중입니다.',
+ outdated: '업데이트가 가능합니다. 지금 업데이트하시겠습니까?',
+ downloading: '다운로드 중...',
+ installing: '설치 중...',
+ failed: '업데이트에 실패했습니다. 다시 시도해 주세요.',
+ confirm: '확인',
+ cancel: '취소'
+ },
+ about: {
+ title: '정보',
+ version: '버전',
+ community: '커뮤니티'
+ }
+ }
+ }
+}
+
+export default ko
diff --git a/desktop/src/renderer/src/i18n/locales/nl.ts b/desktop/src/renderer/src/i18n/locales/nl.ts
index f2dd737..395ff32 100644
--- a/desktop/src/renderer/src/i18n/locales/nl.ts
+++ b/desktop/src/renderer/src/i18n/locales/nl.ts
@@ -19,6 +19,7 @@ const nl = {
},
video: {
resolution: 'Resolutie',
+ scale: 'Schaal',
customResolution: 'Aangepast',
device: 'Apparaat',
custom: {
@@ -76,5 +77,5 @@ const nl = {
}
}
}
-
- export default nl
\ No newline at end of file
+
+ export default nl
diff --git a/desktop/src/renderer/src/i18n/locales/ru.ts b/desktop/src/renderer/src/i18n/locales/ru.ts
index 4e50e6d..259ab3c 100644
--- a/desktop/src/renderer/src/i18n/locales/ru.ts
+++ b/desktop/src/renderer/src/i18n/locales/ru.ts
@@ -19,6 +19,7 @@ const ru = {
},
video: {
resolution: 'Разрешение',
+ scale: 'Масштаб',
customResolution: 'Пользовательское',
device: 'Видеоустройство',
custom: {
@@ -31,7 +32,8 @@ const ru = {
},
keyboard: {
paste: 'Вставить текст',
- virtualKeyboard: 'Виртуальная клавиатура'
+ virtualKeyboard: 'Виртуальная клавиатура',
+ ctrlAltDel: 'Ctrl + Alt + Delete'
},
mouse: {
cursor: {
@@ -47,6 +49,9 @@ const ru = {
direction: 'Направление прокрутки',
scrollUp: 'Обычное',
scrollDown: 'Инвертированное',
+ speed: 'Скорость прокрутки',
+ fast: 'Быстро',
+ slow: 'Медленно',
requestPointer:
'Используется относительное позиционирование мыши. Чтобы захватить курсор, щелкните по видео на экране'
},
diff --git a/desktop/src/renderer/src/i18n/locales/zh-TW.ts b/desktop/src/renderer/src/i18n/locales/zh-TW.ts
new file mode 100644
index 0000000..3270f50
--- /dev/null
+++ b/desktop/src/renderer/src/i18n/locales/zh-TW.ts
@@ -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;
diff --git a/desktop/src/renderer/src/i18n/locales/zh.ts b/desktop/src/renderer/src/i18n/locales/zh.ts
index f6ea3f1..8adde4f 100644
--- a/desktop/src/renderer/src/i18n/locales/zh.ts
+++ b/desktop/src/renderer/src/i18n/locales/zh.ts
@@ -18,6 +18,7 @@ const zh = {
},
video: {
resolution: '分辨率',
+ scale: '缩放',
customResolution: '自定义',
device: '设备',
custom: {
@@ -50,7 +51,12 @@ const zh = {
speed: '滚轮速度',
fast: '快',
slow: '慢',
- requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。'
+ requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
+ jiggler: {
+ title: '空闲晃动',
+ enable: '启用',
+ disable: '禁用'
+ }
},
settings: {
title: '设置',
diff --git a/desktop/src/renderer/src/jotai/device.ts b/desktop/src/renderer/src/jotai/device.ts
index c42460f..84dbee6 100644
--- a/desktop/src/renderer/src/jotai/device.ts
+++ b/desktop/src/renderer/src/jotai/device.ts
@@ -10,6 +10,8 @@ export const resolutionAtom = atom({
height: 1080
})
+export const videoScaleAtom = atom(1.0)
+
export const videoDeviceIdAtom = atom('')
export const videoStateAtom = atom('disconnected')
diff --git a/desktop/src/renderer/src/jotai/mouse.ts b/desktop/src/renderer/src/jotai/mouse.ts
index 4a33252..01f3620 100644
--- a/desktop/src/renderer/src/jotai/mouse.ts
+++ b/desktop/src/renderer/src/jotai/mouse.ts
@@ -10,4 +10,7 @@ export const mouseModeAtom = atom('absolute')
export const scrollDirectionAtom = atom(1)
// mouse scroll interval (unit: ms)
-export const scrollIntervalAtom = atom(0);
+export const scrollIntervalAtom = atom(0)
+
+// mouse jiggler mode: enable or disable
+export const mouseJigglerModeAtom = atom<'enable' | 'disable'>('disable')
diff --git a/desktop/src/renderer/src/libs/mouse-jiggler/index.ts b/desktop/src/renderer/src/libs/mouse-jiggler/index.ts
new file mode 100644
index 0000000..bfdd4b7
--- /dev/null
+++ b/desktop/src/renderer/src/libs/mouse-jiggler/index.ts
@@ -0,0 +1,50 @@
+import { IpcEvents } from '@common/ipc-events'
+import type { Mouse as MouseKey } from '@renderer/types'
+
+const MOUSE_JIGGLER_INTERVAL = 15_000
+const EMPTY_KEY: MouseKey = { left: false, right: false, mid: false }
+
+class MouseJiggler {
+ private lastMoveTime: number
+ private timer: NodeJS.Timeout | null
+ private mode: 'enable' | 'disable'
+
+ constructor() {
+ this.lastMoveTime = Date.now()
+ this.timer = null
+ this.mode = 'disable'
+ }
+
+ // enable or disable mouse jiggler
+ setMode(mode: 'enable' | 'disable'): void {
+ this.mode = mode
+ if (mode === 'disable' && this.timer !== null) {
+ clearInterval(this.timer)
+ this.timer = null
+ } else if (mode === 'enable' && this.timer === null) {
+ this.timer = setInterval(() => {
+ this.timeoutCallback()
+ }, MOUSE_JIGGLER_INTERVAL / 5)
+ }
+ }
+
+ // addEventListener to canvas on 'mousemove' event
+ moveEventCallback(): void {
+ if (this.mode === 'enable') {
+ this.lastMoveTime = Date.now()
+ }
+ }
+
+ timeoutCallback(): void {
+ if (Date.now() - this.lastMoveTime > MOUSE_JIGGLER_INTERVAL) {
+ this.lastMoveTime = Date.now() - 1_000
+ this.sendJiggle()
+ }
+ }
+
+ async sendJiggle(): Promise {
+ await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, EMPTY_KEY, 10, 10, 0)
+ await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, EMPTY_KEY, -10, -10, 0)
+ }
+}
+export const mouseJiggler = new MouseJiggler()
diff --git a/desktop/src/renderer/src/libs/storage/index.ts b/desktop/src/renderer/src/libs/storage/index.ts
index 0024a9d..98c0961 100644
--- a/desktop/src/renderer/src/libs/storage/index.ts
+++ b/desktop/src/renderer/src/libs/storage/index.ts
@@ -5,6 +5,7 @@ import { getWithExpiry, setWithExpiry } from './expiry'
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 VIDEO_SCALE_KEY = 'nanokvm-usb-video-scale'
const CUSTOM_RESOLUTION_KEY = 'nanokvm-usb-custom-resolution'
const SERIAL_PORT_KEY = 'nanokvm-serial-port'
const IS_MENU_OPEN_KEY = 'nanokvm-is-menu-open'
@@ -13,6 +14,7 @@ const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode'
const MOUSE_SCROLL_DIRECTION_KEY = 'nanokvm-usb-mouse-scroll-direction'
const SKIP_UPDATE_KEY = 'nano-kvm-check-update'
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-usb-mouse-scroll-interval'
+const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode'
export function getLanguage(): string | null {
return localStorage.getItem(LANGUAGE_KEY)
@@ -63,6 +65,18 @@ export function removeCustomResolutions(): void {
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 getSerialPort(): string | null {
return localStorage.getItem(SERIAL_PORT_KEY)
}
@@ -132,3 +146,12 @@ export function setSkipUpdate(skip: boolean): void {
const expiry = 3 * 24 * 60 * 60 * 1000
setWithExpiry(SKIP_UPDATE_KEY, String(skip), expiry)
}
+
+export function getMouseJigglerMode(): 'enable' | 'disable' {
+ const jiggler = localStorage.getItem(MOUSE_JIGGLER_MODE_KEY)
+ return jiggler && jiggler === 'enable' ? 'enable' : 'disable'
+}
+
+export function setMouseJigglerMode(jiggler: 'enable' | 'disable'): void {
+ localStorage.setItem(MOUSE_JIGGLER_MODE_KEY, jiggler)
+}