diff --git a/browser/src/components/menu/mouse/index.tsx b/browser/src/components/menu/mouse/index.tsx index d5ba87a..5455f2f 100644 --- a/browser/src/components/menu/mouse/index.tsx +++ b/browser/src/components/menu/mouse/index.tsx @@ -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 = ( diff --git a/browser/src/components/menu/mouse/jiggler.tsx b/browser/src/components/menu/mouse/jiggler.tsx index b3c74f5..e2dfabd 100644 --- a/browser/src/components/menu/mouse/jiggler.tsx +++ b/browser/src/components/menu/mouse/jiggler.tsx @@ -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) => (
update(mode.value)} > diff --git a/browser/src/components/mouse/absolute.tsx b/browser/src/components/mouse/absolute.tsx index 1adb1f5..fc8a4eb 100644 --- a/browser/src/components/mouse/absolute.tsx +++ b/browser/src/components/mouse/absolute.tsx @@ -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(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) { diff --git a/browser/src/components/mouse/index.tsx b/browser/src/components/mouse/index.tsx index 32e558a..ddbd4ae 100644 --- a/browser/src/components/mouse/index.tsx +++ b/browser/src/components/mouse/index.tsx @@ -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(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 22bf8d6..624d80b 100644 --- a/browser/src/components/mouse/relative.tsx +++ b/browser/src/components/mouse/relative.tsx @@ -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(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); diff --git a/browser/src/i18n/locales/zh.ts b/browser/src/i18n/locales/zh.ts index 5245371..9762439 100644 --- a/browser/src/i18n/locales/zh.ts +++ b/browser/src/i18n/locales/zh.ts @@ -69,7 +69,7 @@ const zh = { slow: '慢', requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。', jiggler: { - title: '闲时晃动', + title: '空闲晃动', enable: '启用', disable: '禁用' } diff --git a/browser/src/jotai/mouse.ts b/browser/src/jotai/mouse.ts index 162484a..c3279de 100644 --- a/browser/src/jotai/mouse.ts +++ b/browser/src/jotai/mouse.ts @@ -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(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'); diff --git a/browser/src/libs/mouse-jiggler/index.ts b/browser/src/libs/mouse-jiggler/index.ts new file mode 100644 index 0000000..886f6dc --- /dev/null +++ b/browser/src/libs/mouse-jiggler/index.ts @@ -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 { + await device.sendMouseRelativeData(EMPTY_KEY, 10, 10, 0); + await device.sendMouseRelativeData(EMPTY_KEY, -10, -10, 0); + } +} + +export const mouseJiggler = new MouseJiggler(); diff --git a/browser/src/libs/storage/index.ts b/browser/src/libs/storage/index.ts index 53749b1..48e291d 100644 --- a/browser/src/libs/storage/index.ts +++ b/browser/src/libs/storage/index.ts @@ -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); +} diff --git a/desktop/src/renderer/src/components/mouse/absolute.tsx b/desktop/src/renderer/src/components/mouse/absolute.tsx index 53f8dae..9f05484 100644 --- a/desktop/src/renderer/src/components/mouse/absolute.tsx +++ b/desktop/src/renderer/src/components/mouse/absolute.tsx @@ -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({ - left: false, - right: false, - mid: false - }) + const keyRef = useRef(new Key()) const lastScrollTimeRef = useRef(0) useEffect(() => { @@ -100,57 +96,50 @@ export const Absolute = (): ReactElement => { } async function send(event: MouseEvent, scroll: number = 0): Promise { - 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 => { diff --git a/desktop/src/renderer/src/components/mouse/relative.tsx b/desktop/src/renderer/src/components/mouse/relative.tsx index a270fec..41dfeed 100644 --- a/desktop/src/renderer/src/components/mouse/relative.tsx +++ b/desktop/src/renderer/src/components/mouse/relative.tsx @@ -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({ - left: false, - right: false, - mid: false - }) + const keyRef = useRef(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 { - 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 => { diff --git a/desktop/src/renderer/src/libs/device/mouse.ts b/desktop/src/renderer/src/libs/device/mouse.ts new file mode 100644 index 0000000..2d2baea --- /dev/null +++ b/desktop/src/renderer/src/libs/device/mouse.ts @@ -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 + } +} diff --git a/desktop/src/renderer/src/libs/mouse-jiggler/index.ts b/desktop/src/renderer/src/libs/mouse-jiggler/index.ts index bfdd4b7..65e4121 100644 --- a/desktop/src/renderer/src/libs/mouse-jiggler/index.ts +++ b/desktop/src/renderer/src/libs/mouse-jiggler/index.ts @@ -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 { - 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() diff --git a/desktop/src/renderer/src/types.ts b/desktop/src/renderer/src/types.ts index 5da20b3..dcf31c6 100644 --- a/desktop/src/renderer/src/types.ts +++ b/desktop/src/renderer/src/types.ts @@ -9,9 +9,3 @@ export type MediaDevice = { audioId?: string audioName?: string } - -export type Mouse = { - left: boolean - right: boolean - mid: boolean -}