Merge pull request #92 from imguoguo/fix-jiggler

fix: mouse jiggler
This commit is contained in:
wenjie
2025-12-16 16:08:22 +08:00
committed by GitHub
14 changed files with 151 additions and 171 deletions

View File

@@ -10,6 +10,7 @@ import {
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.ts';
import { mouseJiggler } from '@/libs/mouse-jiggler';
import * as storage from '@/libs/storage';
import { Direction } from './direction.tsx';
@@ -23,7 +24,7 @@ export const Mouse = () => {
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
const setScrollDirection = useSetAtom(scrollDirectionAtom);
const setScrollInterval = useSetAtom(scrollIntervalAtom);
const [mouseJigglerMode, setMouseJigglerMode] = useAtom(mouseJigglerModeAtom);
const setMouseJigglerMode = useSetAtom(mouseJigglerModeAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -53,9 +54,8 @@ export const Mouse = () => {
}
const jiggler = storage.getMouseJigglerMode();
if (mouseJigglerMode !== jiggler) {
setMouseJigglerMode(jiggler);
}
mouseJiggler.setMode(jiggler);
setMouseJigglerMode(jiggler);
}
const content = (

View File

@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { Popover } from 'antd';
import clsx from 'clsx';
import { useAtom } from 'jotai';
@@ -5,29 +6,35 @@ 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 [mouseJigglerMode, setMouseJigglerMode] = useAtom(mouseJigglerModeAtom);
const mouseJigglerModes = [
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: string) {
setMouseJigglerMode(mode);
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 pl-2 pr-5 hover:bg-neutral-700/50',
mode.value === mouseJigglerMode ? 'text-blue-500' : 'text-neutral-300'
'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)}
>

View File

@@ -1,22 +1,16 @@
import { useEffect, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtomValue } from 'jotai';
import { resolutionAtom } from '@/jotai/device.ts';
import {
mouseJigglerModeAtom,
mouseLastMoveTimeAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.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);
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);
@@ -82,10 +76,7 @@ export const Absolute = () => {
disableEvent(event);
await send(event);
// mouse jiggler record last move time
if (mouseJigglerMode === 'enable') {
setMouseLastMoveTime(Date.now());
}
mouseJiggler.moveEventCallback();
}
// mouse scroll
@@ -157,7 +148,7 @@ export const Absolute = () => {
canvas.removeEventListener('click', disableEvent);
canvas.removeEventListener('contextmenu', disableEvent);
};
}, [resolution, scrollDirection, scrollInterval, mouseJigglerMode, setMouseLastMoveTime]);
}, [resolution, scrollDirection, scrollInterval]);
// disable default events
function disableEvent(event: any) {

View File

@@ -1,15 +1,6 @@
import { useEffect, useRef } from 'react';
import { useAtom, useAtomValue } from 'jotai';
import { useAtomValue } from 'jotai';
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 { mouseModeAtom } from '@/jotai/mouse.ts';
import { Absolute } from './absolute.tsx';
import { Relative } from './relative.tsx';
@@ -17,48 +8,5 @@ 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,17 +1,13 @@
import { useEffect, useRef } from 'react';
import { message } from 'antd';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import { resolutionAtom } from '@/jotai/device.ts';
import {
mouseJigglerModeAtom,
mouseLastMoveTimeAtom,
scrollDirectionAtom,
scrollIntervalAtom
} from '@/jotai/mouse.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();
@@ -20,8 +16,6 @@ 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());
@@ -118,10 +112,7 @@ export const Relative = () => {
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());
}
mouseJiggler.moveEventCallback();
}
// mouse scroll
@@ -150,7 +141,7 @@ export const Relative = () => {
canvas.removeEventListener('wheel', handleWheel);
canvas.removeEventListener('contextmenu', disableEvent);
};
}, [resolution, scrollDirection, scrollInterval, mouseJigglerMode, setMouseLastMoveTime]);
}, [resolution, scrollDirection, scrollInterval]);
async function send(x: number, y: number, scroll: number) {
await device.sendMouseRelativeData(keyRef.current, x, y, scroll);

View File

@@ -69,7 +69,7 @@ const zh = {
slow: '慢',
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
jiggler: {
title: '闲晃动',
title: '闲晃动',
enable: '启用',
disable: '禁用'
}

View File

@@ -9,17 +9,9 @@ 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);
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

@@ -11,8 +11,8 @@ 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';
const SHORTCUTS_KEY = 'nanokvm-usb-shortcuts';
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode';
export function getLanguage() {
return localStorage.getItem(LANGUAGE_KEY);
@@ -124,15 +124,6 @@ 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);
}
export function getShortcuts(): ShortcutProps[] {
const shortcuts = localStorage.getItem(SHORTCUTS_KEY);
if (!shortcuts) return [];
@@ -142,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);
}

View File

@@ -4,19 +4,15 @@ import { useAtomValue } from 'jotai'
import { IpcEvents } from '@common/ipc-events'
import { resolutionAtom } from '@renderer/jotai/device'
import { scrollDirectionAtom, scrollIntervalAtom } from '@renderer/jotai/mouse'
import { Key } from '@renderer/libs/device/mouse'
import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
import type { Mouse as MouseKey } from '@renderer/types'
export const Absolute = (): ReactElement => {
const resolution = useAtomValue(resolutionAtom)
const scrollDirection = useAtomValue(scrollDirectionAtom)
const scrollInterval = useAtomValue(scrollIntervalAtom)
const keyRef = useRef<MouseKey>({
left: false,
right: false,
mid: false
})
const keyRef = useRef<Key>(new Key())
const lastScrollTimeRef = useRef(0)
useEffect(() => {
@@ -100,57 +96,50 @@ export const Absolute = (): ReactElement => {
}
async function send(event: MouseEvent, scroll: number = 0): Promise<void> {
const key =
(keyRef.current.left ? 1 : 0) |
(keyRef.current.right ? 2 : 0) |
(keyRef.current.mid ? 4 : 0)
const {x, y} = getCorrectedCoords(event.clientX, event.clientY);
const { x, y } = getCorrectedCoords(event.clientX, event.clientY)
await window.electron.ipcRenderer.invoke(
IpcEvents.SEND_MOUSE_ABSOLUTE,
key,
1, 1, x, y, scroll
keyRef.current.encode(), 1, 1, x, y, scroll
)
}
function getCorrectedCoords(clientX: number, clientY: number) {
if (!canvas) {
return { x: 0, y: 0 };
return { x: 0, y: 0 }
}
const rect = canvas.getBoundingClientRect();
const videoElement = canvas as HTMLVideoElement;
const rect = canvas.getBoundingClientRect()
const videoElement = canvas as HTMLVideoElement
if (!videoElement.videoWidth || !videoElement.videoHeight) {
const x = (clientX - rect.left) / rect.width;
const y = (clientY - rect.top) / rect.height;
return { x, y };
const x = (clientX - rect.left) / rect.width
const y = (clientY - rect.top) / rect.height
return { x, y }
}
const videoRatio = videoElement.videoWidth / videoElement.videoHeight;
const elementRatio = rect.width / rect.height;
const videoRatio = videoElement.videoWidth / videoElement.videoHeight
const elementRatio = rect.width / rect.height
let renderedWidth = rect.width;
let renderedHeight = rect.height;
let offsetX = 0;
let offsetY = 0;
let renderedWidth = rect.width
let renderedHeight = rect.height
let offsetX = 0
let offsetY = 0
if (videoRatio > elementRatio) {
renderedHeight = rect.width / videoRatio;
offsetY = (rect.height - renderedHeight) / 2;
renderedHeight = rect.width / videoRatio
offsetY = (rect.height - renderedHeight) / 2
} else {
renderedWidth = rect.height * videoRatio;
offsetX = (rect.width - renderedWidth) / 2;
renderedWidth = rect.height * videoRatio
offsetX = (rect.width - renderedWidth) / 2
}
const x = (clientX - rect.left - offsetX) / renderedWidth;
const y = (clientY - rect.top - offsetY) / renderedHeight;
const x = (clientX - rect.left - offsetX) / renderedWidth
const y = (clientY - rect.top - offsetY) / renderedHeight
const finalX = Math.max(0, Math.min(1, x));
const finalY = Math.max(0, Math.min(1, y));
const finalX = Math.max(0, Math.min(1, x))
const finalY = Math.max(0, Math.min(1, y))
return { x: finalX, y: finalY };
return { x: finalX, y: finalY }
}
return (): void => {

View File

@@ -6,8 +6,8 @@ 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 { Key } from '@renderer/libs/device/mouse'
import { mouseJiggler } from '@renderer/libs/mouse-jiggler'
import type { Mouse as MouseKey } from '@renderer/types'
export const Relative = (): ReactElement => {
const { t } = useTranslation()
@@ -18,11 +18,7 @@ export const Relative = (): ReactElement => {
const scrollInterval = useAtomValue(scrollIntervalAtom)
const isLockedRef = useRef(false)
const keyRef = useRef<MouseKey>({
left: false,
right: false,
mid: false
})
const keyRef = useRef<Key>(new Key())
const lastScrollTimeRef = useRef(0)
useEffect(() => {
@@ -132,12 +128,7 @@ export const Relative = (): ReactElement => {
}
async function send(x: number, y: number, scroll: number): Promise<void> {
const key =
(keyRef.current.left ? 1 : 0) |
(keyRef.current.right ? 2 : 0) |
(keyRef.current.mid ? 4 : 0)
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, key, x, y, scroll)
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, keyRef.current.encode(), x, y, scroll)
}
return (): void => {

View File

@@ -0,0 +1,26 @@
import { setBit } from './utils'
export enum Mode {
RELATIVE = 0x01,
ABSOLUTE = 0x02
}
export class Key {
left: boolean
right: boolean
mid: boolean
constructor(left: boolean = false, right: boolean = false, mid: boolean = false) {
this.left = left
this.right = right
this.mid = mid
}
public encode(): number {
let b = 0x00
b = setBit(b, 0, this.left)
b = setBit(b, 1, this.right)
b = setBit(b, 2, this.mid)
return b
}
}

View File

@@ -1,8 +1,8 @@
import { IpcEvents } from '@common/ipc-events'
import type { Mouse as MouseKey } from '@renderer/types'
import { Key } from '@renderer/libs/device/mouse'
const MOUSE_JIGGLER_INTERVAL = 15_000
const EMPTY_KEY: MouseKey = { left: false, right: false, mid: false }
const EMPTY_KEY: Key = new Key(false, false, false)
class MouseJiggler {
private lastMoveTime: number
@@ -43,8 +43,8 @@ class MouseJiggler {
}
async sendJiggle(): Promise<void> {
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)
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, EMPTY_KEY.encode(), 10, 10, 0)
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE_RELATIVE, EMPTY_KEY.encode(), -10, -10, 0)
}
}
export const mouseJiggler = new MouseJiggler()

View File

@@ -9,9 +9,3 @@ export type MediaDevice = {
audioId?: string
audioName?: string
}
export type Mouse = {
left: boolean
right: boolean
mid: boolean
}