From cd0b52cc2acdb200904cda8e7713a7b0b55fc210 Mon Sep 17 00:00:00 2001
From: kevinzhangzj <710895909@qq.com>
Date: Fri, 30 May 2025 14:17:13 +0800
Subject: [PATCH] feat: add mouse jiggler function feat: add mouse jiggler to
menu
---
browser/src/components/menu/mouse/index.tsx | 9 +++
browser/src/components/menu/mouse/jiggler.tsx | 49 ++++++++++++++++
browser/src/components/mouse/absolute.tsx | 18 +++++-
browser/src/components/mouse/index.tsx | 56 ++++++++++++++++++-
browser/src/components/mouse/relative.tsx | 18 +++++-
browser/src/i18n/locales/en.ts | 7 ++-
browser/src/i18n/locales/zh.ts | 7 ++-
browser/src/jotai/mouse.ts | 13 ++++-
browser/src/libs/storage/index.ts | 10 ++++
9 files changed, 176 insertions(+), 11 deletions(-)
create mode 100644 browser/src/components/menu/mouse/jiggler.tsx
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/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/locales/en.ts b/browser/src/i18n/locales/en.ts
index 59ea63b..18be788 100644
--- a/browser/src/i18n/locales/en.ts
+++ b/browser/src/i18n/locales/en.ts
@@ -56,7 +56,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/zh.ts b/browser/src/i18n/locales/zh.ts
index e7f1f85..1e73f1d 100644
--- a/browser/src/i18n/locales/zh.ts
+++ b/browser/src/i18n/locales/zh.ts
@@ -54,7 +54,12 @@ const zh = {
speed: '滚轮速度',
fast: '快',
slow: '慢',
- requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。'
+ requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
+ jiggler: {
+ title: '闲时晃动',
+ enable: '启用',
+ disable: '禁用'
+ }
},
settings: {
language: '语言',
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..6340370 100644
--- a/browser/src/libs/storage/index.ts
+++ b/browser/src/libs/storage/index.ts
@@ -9,6 +9,7 @@ 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);
@@ -107,3 +108,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);
+}