mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-09-11 05:59:57 -05:00
Merge branch 'main' into main
This commit is contained in:
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM node:24-alpine3.21 AS frontend
|
||||
WORKDIR /app
|
||||
|
||||
COPY browser browser
|
||||
|
||||
RUN yarn global add http-server
|
||||
|
||||
RUN cd browser && \
|
||||
yarn install && \
|
||||
yarn build
|
||||
|
||||
FROM nginx:1.29.1-alpine
|
||||
|
||||
COPY --from=frontend /app/browser/dist /usr/share/nginx/html
|
||||
|
||||
RUN apk add tzdata
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=frontend /app/browser/dist /usr/share/nginx/html
|
||||
RUN ls -alh /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
15
README.md
15
README.md
@@ -18,9 +18,9 @@ NanoKVM-USB captures HDMI video signals and transmits them to the host via USB 3
|
||||
|
||||
| | NanoKVM-USB | Mini-KVM | KIWI-KVM |
|
||||
| --- | :---: | :---: | :---: |
|
||||
| HDMI Input | 4K@30fps | 1080P@60fps | 4K@30fps |
|
||||
| HDMI Loopout | 4K@30fps | None | None |
|
||||
| USB Capture | 2K@30fps | 1080P@60fps | 1080P@60fps |
|
||||
| HDMI Input | 4K@30fps / Pro 4K@60fps | 1080P@60fps | 4K@30fps |
|
||||
| HDMI Loopout | 4K@30fps / Pro 4K@60fps | None | None |
|
||||
| USB Capture | 1080P@60fps / Pro 4K@60fps | 1080P@60fps | 1080P@60fps |
|
||||
| USB Interface | USB3.0 | USB2.0 | USB3.0 |
|
||||
| USB Switch | Yes | Yes | No |
|
||||
| Keyboard & Mouse | Yes | Yes | Yes |
|
||||
@@ -30,7 +30,7 @@ NanoKVM-USB captures HDMI video signals and transmits them to the host via USB 3
|
||||
| Volume | 57x25x23mm | 61x13.5x53mm | 80x80x10mm |
|
||||
| Shell Material | Aluminum Alloy | Aluminum Alloy | Plastics |
|
||||
| Color | Black / Blue / Red | Black | Black |
|
||||
| Price | `$39.9 / $49.9` | `$89 / $109` | `$69 / $99` |
|
||||
| Price | `$39.9/$49.9`, Pro `$59.9/$69.9` | `$89 / $109` | `$69 / $99` |
|
||||
|
||||

|
||||
|
||||
@@ -51,8 +51,11 @@ For self-deployment, download the `NanoKVM-USB-xxx-browser.zip` and serve it. Re
|
||||
Download the appropriate package for your operating system and install it.
|
||||
|
||||
> For Linux users, a permission error may occur when connecting to the serial port.
|
||||
> To resolve this, run `sudo usermod -a -G dialout $USER`, then log out and log back in or restart your system.
|
||||
|
||||
> To resolve this run the commands below matching your system, then log out and log back in or restart your system.
|
||||
> #### Debian
|
||||
> `sudo usermod -a -G dialout $USER`
|
||||
> #### Arch
|
||||
> `sudo usermod -a -G uucp $USER`
|
||||
## Where to Buy
|
||||
|
||||
* [AliExpress Store]() (To be released)
|
||||
|
||||
748
browser/pnpm-lock.yaml
generated
748
browser/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
15
browser/pnpm-workspace.yaml
Normal file
15
browser/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
overrides:
|
||||
'@babel/helpers@<7.26.10': '>=7.26.10'
|
||||
'@babel/runtime@<7.26.10': '>=7.26.10'
|
||||
'@eslint/plugin-kit@<0.3.4': '>=0.3.4'
|
||||
brace-expansion@>=1.0.0 <=1.1.11: '>=1.1.12'
|
||||
brace-expansion@>=2.0.0 <=2.0.1: '>=2.0.2'
|
||||
esbuild@<=0.24.2: '>=0.25.0'
|
||||
vite@>=6.0.0 <6.0.12: '>=6.0.12'
|
||||
vite@>=6.0.0 <6.0.13: '>=6.0.13'
|
||||
vite@>=6.0.0 <6.0.14: '>=6.0.14'
|
||||
vite@>=6.0.0 <6.0.15: '>=6.0.15'
|
||||
vite@>=6.0.0 <=6.0.8: '>=6.0.9'
|
||||
vite@>=6.0.0 <=6.1.5: '>=6.1.6'
|
||||
vite@>=6.0.0 <=6.3.5: '>=6.3.6'
|
||||
vite@>=6.0.0 <=6.4.0: '>=6.4.1'
|
||||
@@ -10,7 +10,12 @@ import { Keyboard } from '@/components/keyboard';
|
||||
import { Menu } from '@/components/menu';
|
||||
import { Mouse } from '@/components/mouse';
|
||||
import { VirtualKeyboard } from '@/components/virtual-keyboard';
|
||||
import { resolutionAtom, serialStateAtom, videoStateAtom } from '@/jotai/device.ts';
|
||||
import {
|
||||
resolutionAtom,
|
||||
serialStateAtom,
|
||||
videoScaleAtom,
|
||||
videoStateAtom
|
||||
} from '@/jotai/device.ts';
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
import { mouseStyleAtom } from '@/jotai/mouse.ts';
|
||||
import { camera } from '@/libs/camera';
|
||||
@@ -23,6 +28,7 @@ const App = () => {
|
||||
const isBigScreen = useMediaQuery({ minWidth: 850 });
|
||||
|
||||
const mouseStyle = useAtomValue(mouseStyleAtom);
|
||||
const videoScale = useAtomValue(videoScaleAtom);
|
||||
const videoState = useAtomValue(videoStateAtom);
|
||||
const serialState = useAtomValue(serialStateAtom);
|
||||
const isKeyboardEnable = useAtomValue(isKeyboardEnableAtom);
|
||||
@@ -107,7 +113,13 @@ const App = () => {
|
||||
<video
|
||||
id="video"
|
||||
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }}
|
||||
style={{
|
||||
transform: `scale(${videoScale})`,
|
||||
transformOrigin: 'center',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'scale-down'
|
||||
}}
|
||||
autoPlay
|
||||
playsInline
|
||||
/>
|
||||
|
||||
@@ -86,6 +86,10 @@ export const Keyboard = () => {
|
||||
modifiers.leftWindows = pressedKeysRef.current.has('MetaLeft');
|
||||
modifiers.rightWindows = pressedKeysRef.current.has('MetaRight');
|
||||
}
|
||||
if (event.getModifierState('AltGraph')) {
|
||||
modifiers.leftCtrl = true;
|
||||
modifiers.rightAlt = true;
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { KeyboardIcon } from 'lucide-react';
|
||||
|
||||
import { Paste } from './paste.tsx';
|
||||
import { VirtualKeyboard } from './virtual-keyboard.tsx';
|
||||
import { CtrlAltDel } from './ctrl-alt-del';
|
||||
import { KeyboardShortcutsMenu } from './shortcuts-menu.tsx';
|
||||
|
||||
export const Keyboard = () => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
@@ -12,8 +12,8 @@ export const Keyboard = () => {
|
||||
const content = (
|
||||
<>
|
||||
<Paste />
|
||||
<CtrlAltDel />
|
||||
<VirtualKeyboard />
|
||||
<KeyboardShortcutsMenu />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { SendHorizonal } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { device } from '@/libs/device';
|
||||
import { Modifiers } from '@/libs/device/keyboard.ts';
|
||||
import { KeyboardCodes } from '@/libs/keyboard';
|
||||
|
||||
export const CtrlAltDel = () => {
|
||||
const { t } = useTranslation();
|
||||
interface ShortcutProps {
|
||||
label: string;
|
||||
modifiers?: Partial<Modifiers>;
|
||||
keyCode: string;
|
||||
}
|
||||
|
||||
export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function ctrlAltDel(): Promise<void> {
|
||||
async function handleClick(): Promise<void> {
|
||||
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 (
|
||||
<div
|
||||
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
|
||||
onClick={ctrlAltDel}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<SendHorizonal size={18} />
|
||||
<span>{t('keyboard.ctrlAltDel')}</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
55
browser/src/components/menu/keyboard/shortcuts-menu.tsx
Normal file
55
browser/src/components/menu/keyboard/shortcuts-menu.tsx
Normal file
@@ -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 (
|
||||
<Popover
|
||||
content={
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Shortcut
|
||||
key={shortcut.keyCode}
|
||||
label={shortcut.label}
|
||||
modifiers={shortcut.modifiers}
|
||||
keyCode={shortcut.keyCode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="rightTop"
|
||||
align={{ offset: [14, 0] }}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
arrow={false}
|
||||
>
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
|
||||
<SendHorizonal size={18} />
|
||||
<span>{t('keyboard.shortcuts')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
<Mode />
|
||||
<Direction />
|
||||
<Speed />
|
||||
<Jiggler />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
49
browser/src/components/menu/mouse/jiggler.tsx
Normal file
49
browser/src/components/menu/mouse/jiggler.tsx
Normal file
@@ -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) => (
|
||||
<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'
|
||||
)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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 = (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Resolution />
|
||||
<Scale />
|
||||
<Device />
|
||||
</div>
|
||||
);
|
||||
|
||||
58
browser/src/components/menu/video/scale.tsx
Normal file
58
browser/src/components/menu/video/scale.tsx
Normal file
@@ -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<void> {
|
||||
setVideoScale(scale)
|
||||
storage.setVideoScale(scale)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className="h-[150px] w-[60px] py-3">
|
||||
<Slider
|
||||
vertical
|
||||
marks={{
|
||||
0.5: <span>x0.5</span>,
|
||||
1: <span>x1.0</span>,
|
||||
1.5: <span>x1.5</span>,
|
||||
2: <span>x2.0</span>
|
||||
}}
|
||||
range={false}
|
||||
included={false}
|
||||
min={0.5}
|
||||
max={2}
|
||||
step={0.1}
|
||||
defaultValue={videoScale}
|
||||
onChange={updateScale}
|
||||
/>
|
||||
</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">
|
||||
<ScalingIcon size={16} />
|
||||
</div>
|
||||
<span>{t('video.scale')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -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<Key>(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) {
|
||||
|
||||
@@ -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<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 />}</>;
|
||||
};
|
||||
|
||||
@@ -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<Key>(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);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
const languages = [
|
||||
{ key: 'en', name: 'English' },
|
||||
{ key: 'ru', name: 'Русский' },
|
||||
{ key: 'zh', name: '中文' },
|
||||
{ key: 'zh', name: '简体中文' },
|
||||
{ key: 'zh_tw', name: '繁體中文' },
|
||||
{ key: 'de', name: 'Deutsch' },
|
||||
{ key: 'nl', name: 'Nederlands' },
|
||||
{ key: 'be', name: 'België' }
|
||||
{ key: 'be', name: 'België' },
|
||||
{ key: 'ko', name: '한국어' },
|
||||
{ key: 'pt_br', name: 'Português (Brasil)' },
|
||||
{ key: 'pl', name: 'Polski' }
|
||||
];
|
||||
|
||||
languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }));
|
||||
|
||||
@@ -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;
|
||||
|
||||
export default be;
|
||||
|
||||
@@ -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;
|
||||
|
||||
export default de;
|
||||
|
||||
@@ -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',
|
||||
|
||||
70
browser/src/i18n/locales/ko.ts
Normal file
70
browser/src/i18n/locales/ko.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
export default nl;
|
||||
|
||||
70
browser/src/i18n/locales/pl.ts
Normal file
70
browser/src/i18n/locales/pl.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
const pl = {
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Przeglądarka nie obsługuje portu szeregowego. Proszę użyć przeglądarki Chrome (desktop) aby włączyć obsługę myszy i klawiatury.',
|
||||
failed: 'Nie udało połączyć się z portem szeregowym. Spróbuj ponownie.'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Oczekiwanie na autoryzację...',
|
||||
denied: 'Brak uprawnień',
|
||||
authorize:
|
||||
'Zdalny pulpit wymaga uprawnienia dostępu do kamery. Zezwól na dostęp do kamery w ustawieniach przeglądarki.',
|
||||
failed: 'Nie udało połączyć się z kamerą. Spróbuj ponownie.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Wybierz urządzenie USB',
|
||||
selectVideo: 'Wybierz urządzenie wejścia wideo',
|
||||
selectSerial: 'Wybierz urządzenie portu szeregowego'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Port szeregowy',
|
||||
keyboard: 'Klawiatura',
|
||||
mouse: 'Mysz'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Rozdzielczość',
|
||||
customResolution: 'Niestandardowa',
|
||||
device: 'Urządzenie',
|
||||
custom: {
|
||||
title: 'Niestandardowa rozdzielczość',
|
||||
width: 'Szerokość',
|
||||
height: 'Wysokość',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Anuluj'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Wklej',
|
||||
virtualKeyboard: 'Klawiatura',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete'
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Kursor',
|
||||
pointer: 'Wskaźnik',
|
||||
grab: 'Chwyć',
|
||||
cell: 'Zaznacz',
|
||||
hide: 'Ukryj'
|
||||
},
|
||||
mode: 'Tryb myszy',
|
||||
absolute: 'Tryb absolutny',
|
||||
relative: 'Tryb względny',
|
||||
direction: 'Kierunek kółka przewijania',
|
||||
scrollUp: 'Przewijanie w górę',
|
||||
scrollDown: 'Przewijanie w dół',
|
||||
speed: 'Szybkość kółka',
|
||||
fast: 'Szybko',
|
||||
slow: 'Powoli',
|
||||
requestPointer:
|
||||
'Użycie trybu względnego. Proszę kliknij na pulpicie aby aktywować wskaźnik myszy.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Język',
|
||||
document: 'Dokumentacja',
|
||||
download: 'Pobieranie'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default pl;
|
||||
69
browser/src/i18n/locales/pt_br.ts
Normal file
69
browser/src/i18n/locales/pt_br.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
const pt_br = {
|
||||
translation: {
|
||||
serial: {
|
||||
notSupported:
|
||||
'Serial não suportado. Use o navegador Chrome para habilitar o mouse e teclado.',
|
||||
failed: 'Falha ao se conectar na porta serial. Tente novamente'
|
||||
},
|
||||
camera: {
|
||||
tip: 'Esperando autorização...',
|
||||
denied: 'Permissão negada',
|
||||
authorize:
|
||||
'A área de trabalho remota requer permissão de uso da câmera. Por favor, conceda as permissões de câmera nos ajustes do navegador.',
|
||||
failed: 'Falha ao se conectar à câmera. Tente novamente.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Selecione o dispositivo USB',
|
||||
selectVideo: 'Selecione um dispositivo de vídeo de entrada',
|
||||
selectSerial: 'Selecione um dispositivo serial'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serial',
|
||||
keyboard: 'Teclado',
|
||||
mouse: 'Mouse'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolução',
|
||||
customResolution: 'Personalizada',
|
||||
device: 'Dispositivo',
|
||||
custom: {
|
||||
title: 'Resolução Personalizada',
|
||||
width: 'Largura',
|
||||
height: 'Altura',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Cancelar'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Colar',
|
||||
virtualKeyboard: 'Teclado Virtual',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete'
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Cursor',
|
||||
pointer: 'Ponteiro',
|
||||
grab: 'Mão',
|
||||
cell: 'Célula',
|
||||
hide: 'Esconder'
|
||||
},
|
||||
mode: 'Modo do mouse',
|
||||
absolute: 'Modo absoluto',
|
||||
relative: 'Modo relativo',
|
||||
direction: 'Direção do scroll',
|
||||
scrollUp: 'Scroll para cima',
|
||||
scrollDown: 'Scroll para baixo',
|
||||
speed: 'Velocidade do scroll',
|
||||
fast: 'Rápido',
|
||||
slow: 'Lento',
|
||||
requestPointer: 'Usando modo relativo. Por favor, clique na área de trabalho para restaurar o ponteiro do mouse.'
|
||||
},
|
||||
settings: {
|
||||
language: 'Linguagem',
|
||||
document: 'Documentação',
|
||||
download: 'Download'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default pt_br;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: '语言',
|
||||
|
||||
67
browser/src/i18n/locales/zh_tw.ts
Normal file
67
browser/src/i18n/locales/zh_tw.ts
Normal file
@@ -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;
|
||||
@@ -10,6 +10,8 @@ export const resolutionAtom = atom<Resolution>({
|
||||
height: 1080
|
||||
});
|
||||
|
||||
export const videoScaleAtom = atom<number>(1.0)
|
||||
|
||||
export const videoDeviceIdAtom = atom('');
|
||||
export const videoStateAtom = atom<VideoState>('disconnected');
|
||||
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
|
||||
// mouse jiggler interval (unit: ms)
|
||||
export const mouseJigglerIntervalAtom = atom(15_000);
|
||||
|
||||
// mouse jiggler last move time
|
||||
export const mouseLastMoveTimeAtom = atom(0);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"electron": "^34.2.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-vite": "^3.0.0",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
@@ -65,6 +65,24 @@
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0",
|
||||
"vite@>=6.1.0 <6.1.2": ">=6.1.2",
|
||||
"vite@>=6.1.0 <6.1.3": ">=6.1.3",
|
||||
"vite@>=6.1.0 <6.1.5": ">=6.1.5",
|
||||
"@babel/runtime@<7.26.10": ">=7.26.10",
|
||||
"@babel/helpers@<7.26.10": ">=7.26.10",
|
||||
"vite@>=6.0.0 <=6.1.5": ">=6.1.6",
|
||||
"vite@>=6.1.0 <6.1.4": ">=6.1.4",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=1.1.12",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=2.0.2",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"@eslint/plugin-kit@<0.3.4": ">=0.3.4",
|
||||
"tmp@<=0.2.3": ">=0.2.4",
|
||||
"electron@<35.7.5": ">=35.7.5",
|
||||
"vite@>=6.0.0 <=6.3.5": ">=6.3.6",
|
||||
"vite@>=6.0.0 <=6.4.0": ">=6.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1063
desktop/pnpm-lock.yaml
generated
1063
desktop/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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 => {
|
||||
<video
|
||||
id="video"
|
||||
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }}
|
||||
style={{
|
||||
transform: `scale(${videoScale})`,
|
||||
transformOrigin: 'center',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'scale-down'
|
||||
}}
|
||||
autoPlay
|
||||
playsInline
|
||||
/>
|
||||
|
||||
@@ -64,14 +64,15 @@ export const Keyboard = (): ReactElement => {
|
||||
}
|
||||
|
||||
function getModifier(e: KeyboardEvent): number {
|
||||
const altGraphKey = e.getModifierState('AltGraph')
|
||||
const pressedKeys = [
|
||||
e.ctrlKey && pressedKeysRef.current.has('ControlLeft'),
|
||||
altGraphKey || (e.ctrlKey && pressedKeysRef.current.has('ControlLeft')),
|
||||
e.shiftKey && pressedKeysRef.current.has('ShiftLeft'),
|
||||
e.altKey && pressedKeysRef.current.has('AltLeft'),
|
||||
e.metaKey && pressedKeysRef.current.has('MetaLeft'),
|
||||
e.ctrlKey && pressedKeysRef.current.has('ControlRight'),
|
||||
e.shiftKey && pressedKeysRef.current.has('ShiftRight'),
|
||||
e.altKey && pressedKeysRef.current.has('AltRight'),
|
||||
altGraphKey || (e.altKey && pressedKeysRef.current.has('AltRight')),
|
||||
e.metaKey && pressedKeysRef.current.has('MetaRight')
|
||||
]
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
<Mode />
|
||||
<Direction />
|
||||
<Speed />
|
||||
<Jiggler />
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
56
desktop/src/renderer/src/components/menu/mouse/jiggler.tsx
Normal file
56
desktop/src/renderer/src/components/menu/mouse/jiggler.tsx
Normal file
@@ -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) => (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -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 = (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Resolution />
|
||||
<Scale />
|
||||
<Device />
|
||||
</div>
|
||||
)
|
||||
|
||||
58
desktop/src/renderer/src/components/menu/video/scale.tsx
Normal file
58
desktop/src/renderer/src/components/menu/video/scale.tsx
Normal file
@@ -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<void> {
|
||||
setVideoScale(scale)
|
||||
storage.setVideoScale(scale)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className="h-[150px] w-[60px] py-3">
|
||||
<Slider
|
||||
vertical
|
||||
marks={{
|
||||
0.5: <span>x0.5</span>,
|
||||
1: <span>x1.0</span>,
|
||||
1.5: <span>x1.5</span>,
|
||||
2: <span>x2.0</span>
|
||||
}}
|
||||
range={false}
|
||||
included={false}
|
||||
min={0.5}
|
||||
max={2}
|
||||
step={0.1}
|
||||
defaultValue={videoScale}
|
||||
onChange={updateScale}
|
||||
/>
|
||||
</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">
|
||||
<ScalingIcon size={16} />
|
||||
</div>
|
||||
<span>{t('video.scale')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -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<void> {
|
||||
disableEvent(event)
|
||||
await send(event)
|
||||
|
||||
mouseJiggler.moveEventCallback()
|
||||
}
|
||||
|
||||
// mouse scroll
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
const languages = [
|
||||
{ key: 'en', name: 'English' },
|
||||
{ key: 'ru', name: 'Русский' },
|
||||
{ key: 'zh', name: '中文' },
|
||||
{ key: 'zh', name: '简体中文' },
|
||||
{ key: 'zh_tw', name: '繁體中文' },
|
||||
{ key: 'de', name: 'Deutsch' },
|
||||
{ key: 'nl', name: 'Nederlands' },
|
||||
{ key: 'be', name: 'België' }
|
||||
{ key: 'be', name: 'België' },
|
||||
{ key: 'ko', name: '한국어' },
|
||||
{ key: 'pt_br', name: 'Português (Brasil)' },
|
||||
{ key: 'pl', name: 'Polski' }
|
||||
]
|
||||
|
||||
languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
|
||||
|
||||
@@ -26,6 +26,7 @@ const be = {
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Toestel',
|
||||
custom: {
|
||||
@@ -94,5 +95,5 @@ const be = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default be
|
||||
|
||||
export default be
|
||||
|
||||
@@ -26,6 +26,7 @@ const de = {
|
||||
},
|
||||
video: {
|
||||
resolution: 'Auflösung',
|
||||
scale: 'Skalierung',
|
||||
customResolution: 'Benutzerdefiniert',
|
||||
device: 'Gerät',
|
||||
custom: {
|
||||
@@ -55,6 +56,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: {
|
||||
@@ -94,5 +98,5 @@ const de = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default de
|
||||
|
||||
export default de
|
||||
|
||||
@@ -26,6 +26,7 @@ const en = {
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolution',
|
||||
scale: 'Scale',
|
||||
customResolution: 'Custom',
|
||||
device: 'Device',
|
||||
custom: {
|
||||
@@ -58,7 +59,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',
|
||||
|
||||
84
desktop/src/renderer/src/i18n/locales/ko.ts
Normal file
84
desktop/src/renderer/src/i18n/locales/ko.ts
Normal file
@@ -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
|
||||
@@ -26,6 +26,7 @@ const nl = {
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolutie',
|
||||
scale: 'Schaal',
|
||||
customResolution: 'Aangepast',
|
||||
device: 'Apparaat',
|
||||
custom: {
|
||||
@@ -94,5 +95,5 @@ const nl = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default nl
|
||||
|
||||
export default nl
|
||||
|
||||
84
desktop/src/renderer/src/i18n/locales/pl.ts
Normal file
84
desktop/src/renderer/src/i18n/locales/pl.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
const pl = {
|
||||
translation: {
|
||||
camera: {
|
||||
tip: 'Oczekiwanie na autoryzację...',
|
||||
denied: 'Brak uprawnień',
|
||||
authorize:
|
||||
'Zdalny pulpit wymaga uprawnienia dostępu do kamery. Zezwól na dostęp do kamery w ustawieniach.',
|
||||
failed: 'Nie udało połączyć się z kamerą. Spróbuj ponownie.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Wybierz urządzenie USB',
|
||||
selectVideo: 'Wybierz urządzenie wejścia wideo',
|
||||
selectSerial: 'Wybierz urządzenie portu szeregowego'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Port szeregowy',
|
||||
keyboard: 'Klawiatura',
|
||||
mouse: 'Mysz'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Rozdzielczość',
|
||||
customResolution: 'Niestandardowa',
|
||||
device: 'Urządzenie',
|
||||
custom: {
|
||||
title: 'Niestandardowa rozdzielczość',
|
||||
width: 'Szerokość',
|
||||
height: 'Wysokość',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Anuluj'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Wklej',
|
||||
virtualKeyboard: 'Klawiatura',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete'
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Kursor',
|
||||
pointer: 'Wskaźnik',
|
||||
grab: 'Chwyć',
|
||||
cell: 'Zaznacz',
|
||||
hide: 'Ukryj'
|
||||
},
|
||||
mode: 'Tryb myszy',
|
||||
absolute: 'Tryb absolutny',
|
||||
relative: 'Tryb względny',
|
||||
direction: 'Kierunek kółka przewijania',
|
||||
scrollUp: 'Przewijanie w górę',
|
||||
scrollDown: 'Przewijanie w dół',
|
||||
speed: 'Szybkość kółka',
|
||||
fast: 'Szybko',
|
||||
slow: 'Powoli',
|
||||
requestPointer:
|
||||
'Użycie trybu względnego. Proszę kliknij na pulpicie aby aktywować wskaźnik myszy.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Ustawienia',
|
||||
appearance: {
|
||||
title: 'Wygląd',
|
||||
language: 'Język',
|
||||
menu: 'Menu',
|
||||
menuTips: 'Rozwiń menu przy starcie'
|
||||
},
|
||||
update: {
|
||||
title: 'Sprawdź dostępność aktualizacji',
|
||||
latest: 'Używana wersja jest najnowsza.',
|
||||
outdated: 'Aktualizacja jest dostępna. Czy chcesz teraz wykonać aktualizację?',
|
||||
downloading: 'Pobieranie...',
|
||||
installing: 'Instalacja...',
|
||||
failed: 'Aktualizacja nieudana. Spróbuj ponownie.',
|
||||
confirm: 'Potwierdź',
|
||||
cancel: 'Anuluj'
|
||||
},
|
||||
about: {
|
||||
title: 'O aplikacji',
|
||||
version: 'Wersja',
|
||||
community: 'Społeczność'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default pl
|
||||
84
desktop/src/renderer/src/i18n/locales/pt_br.ts
Normal file
84
desktop/src/renderer/src/i18n/locales/pt_br.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
const pt_br = {
|
||||
translation: {
|
||||
camera: {
|
||||
tip: 'Esperando autorização...',
|
||||
denied: 'Falha na autorização',
|
||||
authorize:
|
||||
'A área de trabalho remota requer permissão de uso da câmera. Por favor, conceda as permissões de câmera nos ajustes.',
|
||||
failed: 'Falha ao se conectar à câmera. Tente novamente.'
|
||||
},
|
||||
modal: {
|
||||
title: 'Selecione o dispositivo USB',
|
||||
selectVideo: 'Selecione um dispositivo de vídeo de entrada',
|
||||
selectSerial: 'Selecione um dispositivo serial'
|
||||
},
|
||||
menu: {
|
||||
serial: 'Serial',
|
||||
keyboard: 'Teclado',
|
||||
mouse: 'Mouse'
|
||||
},
|
||||
video: {
|
||||
resolution: 'Resolução',
|
||||
customResolution: 'Personalizada',
|
||||
device: 'Dispositivo',
|
||||
custom: {
|
||||
title: 'Resolução Personalizada',
|
||||
width: 'Largura',
|
||||
height: 'Altura',
|
||||
confirm: 'Ok',
|
||||
cancel: 'Cancelar'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Colar',
|
||||
virtualKeyboard: 'Teclado Virtual',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete'
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
title: 'Cursor',
|
||||
pointer: 'Ponteiro',
|
||||
grab: 'Mão',
|
||||
cell: 'Célula',
|
||||
hide: 'Esconder'
|
||||
},
|
||||
mode: 'Modo do mouse',
|
||||
absolute: 'Modo absoluto',
|
||||
relative: 'Modo relativo',
|
||||
direction: 'Direção do scroll',
|
||||
scrollUp: 'Scroll para cima',
|
||||
scrollDown: 'Scroll para baixo',
|
||||
speed: 'Velocidade do scroll',
|
||||
fast: 'Rápido',
|
||||
slow: 'Lento',
|
||||
requestPointer:
|
||||
'Usando modo relativo. Por favor, clique na área de trabalho para restaurar o ponteiro do mouse.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Ajustes',
|
||||
appearance: {
|
||||
title: 'Aparência',
|
||||
language: 'Linguagem',
|
||||
menu: 'Barra de menu',
|
||||
menuTips: 'Abrir a barra de menu quando iniciar'
|
||||
},
|
||||
update: {
|
||||
title: 'Verificar atualizações',
|
||||
latest: 'Você já tem a versão mais recente.',
|
||||
outdated: 'Uma atualização está disponível. Deseja atualizar agora?',
|
||||
downloading: 'Fazendo download...',
|
||||
installing: 'Instalando...',
|
||||
failed: 'Falha na atualização. Por favor, tente novamente.',
|
||||
confirm: 'Confirmar',
|
||||
cancel: 'Cancelar'
|
||||
},
|
||||
about: {
|
||||
title: 'Sobre',
|
||||
version: 'Versão',
|
||||
community: 'Comunidade'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default pt_br
|
||||
@@ -26,6 +26,7 @@ const ru = {
|
||||
},
|
||||
video: {
|
||||
resolution: 'Разрешение',
|
||||
scale: 'Масштаб',
|
||||
customResolution: 'Пользовательское',
|
||||
device: 'Видеоустройство',
|
||||
custom: {
|
||||
@@ -38,7 +39,8 @@ const ru = {
|
||||
},
|
||||
keyboard: {
|
||||
paste: 'Вставить текст',
|
||||
virtualKeyboard: 'Виртуальная клавиатура'
|
||||
virtualKeyboard: 'Виртуальная клавиатура',
|
||||
ctrlAltDel: 'Ctrl + Alt + Delete'
|
||||
},
|
||||
mouse: {
|
||||
cursor: {
|
||||
@@ -54,6 +56,9 @@ const ru = {
|
||||
direction: 'Направление прокрутки',
|
||||
scrollUp: 'Обычное',
|
||||
scrollDown: 'Инвертированное',
|
||||
speed: 'Скорость прокрутки',
|
||||
fast: 'Быстро',
|
||||
slow: 'Медленно',
|
||||
requestPointer:
|
||||
'Используется относительное позиционирование мыши. Чтобы захватить курсор, щелкните по видео на экране'
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ const zh = {
|
||||
},
|
||||
video: {
|
||||
resolution: '分辨率',
|
||||
scale: '缩放',
|
||||
customResolution: '自定义',
|
||||
device: '设备',
|
||||
custom: {
|
||||
@@ -57,7 +58,12 @@ const zh = {
|
||||
speed: '滚轮速度',
|
||||
fast: '快',
|
||||
slow: '慢',
|
||||
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。'
|
||||
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
|
||||
jiggler: {
|
||||
title: '空闲晃动',
|
||||
enable: '启用',
|
||||
disable: '禁用'
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
|
||||
67
desktop/src/renderer/src/i18n/locales/zh_tw.ts
Normal file
67
desktop/src/renderer/src/i18n/locales/zh_tw.ts
Normal file
@@ -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
|
||||
@@ -10,6 +10,8 @@ export const resolutionAtom = atom<Resolution>({
|
||||
height: 1080
|
||||
})
|
||||
|
||||
export const videoScaleAtom = atom<number>(1.0)
|
||||
|
||||
export const videoDeviceIdAtom = atom('')
|
||||
export const videoStateAtom = atom<VideoState>('disconnected')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
50
desktop/src/renderer/src/libs/mouse-jiggler/index.ts
Normal file
50
desktop/src/renderer/src/libs/mouse-jiggler/index.ts
Normal file
@@ -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<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)
|
||||
}
|
||||
}
|
||||
export const mouseJiggler = new MouseJiggler()
|
||||
@@ -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'
|
||||
@@ -14,6 +15,7 @@ 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 BAUD_RATE_KEY = 'nanokvm-usb-baud-rate'
|
||||
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode'
|
||||
|
||||
export function getLanguage(): string | null {
|
||||
return localStorage.getItem(LANGUAGE_KEY)
|
||||
@@ -64,6 +66,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)
|
||||
}
|
||||
@@ -159,4 +173,11 @@ export function clearAllSettings(): void {
|
||||
localStorage.removeItem(SKIP_UPDATE_KEY)
|
||||
localStorage.removeItem(MOUSE_SCROLL_INTERVAL_KEY)
|
||||
localStorage.removeItem(BAUD_RATE_KEY)
|
||||
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)
|
||||
}
|
||||
|
||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
nanokvm-usb:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
image: sipeed/nanokvm-usb:latest
|
||||
container_name: nanokvm-usb
|
||||
ports:
|
||||
- "9000:80"
|
||||
38
nginx.conf
Normal file
38
nginx.conf
Normal file
@@ -0,0 +1,38 @@
|
||||
worker_processes 1;
|
||||
|
||||
error_log /dev/stdout info;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.html;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user