fix(browser): mouse jiggler

Mouse jiggler feature of browser version has been reimplemented based on the desktop version.

Revert Pull Request: https://github.com/sipeed/NanoKVM-USB/pull/46
Reimplemented Reference: https://github.com/sipeed/NanoKVM-USB/pull/47
This commit is contained in:
Guoguo
2025-12-15 15:53:41 +08:00
parent a6fbdc801b
commit bfe8d172cb
9 changed files with 147 additions and 2 deletions

View File

@@ -4,14 +4,17 @@ import { useAtom, useSetAtom } from 'jotai';
import { MouseIcon } from 'lucide-react';
import {
mouseJigglerModeAtom,
mouseModeAtom,
mouseStyleAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.ts';
import { mouseJiggler } from '@/libs/mouse-jiggler';
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 +24,7 @@ export const Mouse = () => {
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
const setScrollDirection = useSetAtom(scrollDirectionAtom);
const setScrollInterval = useSetAtom(scrollIntervalAtom);
const setMouseJigglerMode = useSetAtom(mouseJigglerModeAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -48,6 +52,10 @@ export const Mouse = () => {
if (interval) {
setScrollInterval(interval);
}
const jiggler = storage.getMouseJigglerMode();
mouseJiggler.setMode(jiggler);
setMouseJigglerMode(jiggler);
}
const content = (
@@ -56,6 +64,7 @@ export const Mouse = () => {
<Mode />
<Direction />
<Speed />
<Jiggler />
</>
);

View File

@@ -0,0 +1,56 @@
import { 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 '@/jotai/mouse.ts';
import { mouseJiggler } from '@/libs/mouse-jiggler';
import * as storage from '@/libs/storage';
export const Jiggler = () => {
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) => (
<div
key={mode.value}
className={clsx(
'my-1 flex cursor-pointer items-center space-x-1 rounded py-1 pr-5 pl-2 hover:bg-neutral-700/50',
mode.value === jigglerMode ? '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

@@ -5,6 +5,7 @@ import { resolutionAtom } from '@/jotai/device.ts';
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
import { device } from '@/libs/device';
import { Key } from '@/libs/device/mouse.ts';
import { mouseJiggler } from '@/libs/mouse-jiggler';
export const Absolute = () => {
const resolution = useAtomValue(resolutionAtom);
@@ -74,6 +75,8 @@ export const Absolute = () => {
async function handleMouseMove(event: any) {
disableEvent(event);
await send(event);
mouseJiggler.moveEventCallback();
}
// mouse scroll

View File

@@ -7,6 +7,7 @@ import { resolutionAtom } from '@/jotai/device.ts';
import { scrollDirectionAtom, scrollIntervalAtom } from '@/jotai/mouse.ts';
import { device } from '@/libs/device';
import { Key } from '@/libs/device/mouse.ts';
import { mouseJiggler } from '@/libs/mouse-jiggler';
export const Relative = () => {
const { t } = useTranslation();
@@ -110,6 +111,8 @@ 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);
mouseJiggler.moveEventCallback();
}
// mouse scroll

View File

@@ -69,7 +69,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

@@ -67,7 +67,12 @@ const zh = {
speed: '滚轮速度',
fast: '快',
slow: '慢',
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。'
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
jiggler: {
title: '空闲晃动',
enable: '启用',
disable: '禁用'
}
},
settings: {
language: '语言',

View File

@@ -12,3 +12,6 @@ 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<'enable' | 'disable'>('disable');

View File

@@ -0,0 +1,51 @@
import { device } from '@/libs/device';
import { Key } from '@/libs/device/mouse.ts';
const MOUSE_JIGGLER_INTERVAL = 15_000;
const EMPTY_KEY: Key = new Key(false, false, false);
class MouseJiggler {
private lastMoveTime: number;
private timer: number | 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<void> {
await device.sendMouseRelativeData(EMPTY_KEY, 10, 10, 0);
await device.sendMouseRelativeData(EMPTY_KEY, -10, -10, 0);
}
}
export const mouseJiggler = new MouseJiggler();

View File

@@ -12,6 +12,7 @@ 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 SHORTCUTS_KEY = 'nanokvm-usb-shortcuts';
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode';
export function getLanguage() {
return localStorage.getItem(LANGUAGE_KEY);
@@ -132,3 +133,12 @@ export function getShortcuts(): ShortcutProps[] {
export function setShortcuts(shortcuts: ShortcutProps[]): void {
localStorage.setItem(SHORTCUTS_KEY, window.JSON.stringify(shortcuts));
}
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);
}