diff --git a/web/src/api/hid.ts b/web/src/api/hid.ts
new file mode 100644
index 0000000..b760b63
--- /dev/null
+++ b/web/src/api/hid.ts
@@ -0,0 +1,11 @@
+import { http } from '@/lib/http.ts';
+
+// paste
+export function paste(content: string) {
+ return http.post('/api/hid/paste', { content });
+}
+
+// reset hid
+export function reset() {
+ return http.post('/api/hid/reset');
+}
diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts
index cc51170..9feeb77 100644
--- a/web/src/api/storage.ts
+++ b/web/src/api/storage.ts
@@ -17,8 +17,3 @@ export function mountImage(file?: string) {
};
return http.post('/api/storage/image/mount', data);
}
-
-// reset hid
-export function resetHid() {
- return http.post('/api/storage/hid/reset');
-}
diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts
index 4838316..ff4f9c6 100644
--- a/web/src/i18n/locales/en.ts
+++ b/web/src/i18n/locales/en.ts
@@ -3,7 +3,6 @@ const en = {
language: 'Language',
changePassword: 'Change Password',
logout: 'Logout',
- keyboard: 'Keyboard',
settings: 'Settings',
showMouse: 'Show mouse',
hideMouse: 'Hide mouse',
@@ -53,9 +52,16 @@ const en = {
frameDetect: 'Frame Detect',
frameDetectTip:
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
- fitInWindow: "Fit in window",
+ fitInWindow: 'Fit in window'
},
- cursor: {
+ keyboard: {
+ paste: 'Paste',
+ tips: 'Only standard keyboard letters and symbols are supported',
+ placeholder: 'Please input',
+ submit: 'Submit',
+ virtual: 'Keyboard'
+ },
+ mouse: {
default: 'Default cursor',
pointer: 'Pointer cursor',
cell: 'Cell cursor',
diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts
index 5a04b41..64cbf4a 100644
--- a/web/src/i18n/locales/fr.ts
+++ b/web/src/i18n/locales/fr.ts
@@ -3,7 +3,6 @@ const fr = {
language: 'Langue',
changePassword: 'Changer le mot de passe',
logout: 'Déconnexion',
- keyboard: 'Clavier',
settings: 'Réglages',
showMouse: 'Afficher la souris',
hideMouse: 'Masquer la souris',
@@ -56,9 +55,16 @@ const fr = {
frameDetect: 'Frame Detect',
frameDetectTip:
"Calcule la différence entre les images. Arrête la transmission du flux vidéo lorsqu'aucun changement n'est détecté sur l'écran de l'hôte distant",
- fitInWindow: "Ajuster à la fenêtre"
+ fitInWindow: 'Ajuster à la fenêtre'
},
- cursor: {
+ keyboard: {
+ paste: 'Coller',
+ tips: 'Seuls les caractères et symboles standard du clavier sont pris en charge',
+ placeholder: 'Veuillez saisir',
+ submit: 'Soumettre',
+ virtual: 'Clavier'
+ },
+ mouse: {
default: 'Curseur par défaut',
pointer: 'Curseur de la souris',
cell: 'Curseur de cellule',
diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts
index 4f0ba88..8501c44 100644
--- a/web/src/i18n/locales/ko.ts
+++ b/web/src/i18n/locales/ko.ts
@@ -3,7 +3,6 @@ const ko = {
language: '언어',
changePassword: '비밀번호 변경',
logout: '로그아웃',
- keyboard: '키보드',
settings: '설정',
showMouse: '마우스 보이기',
hideMouse: '마우스 숨기기',
@@ -29,7 +28,8 @@ const ko = {
placeholderPassword2: '비밀번호를 다시 입력하세요.',
noEmptyUsername: '유저 이름은 비어있을 수 없습니다.',
noEmptyPassword: '비밀번호는 비어있을 수 없습니다.',
- noAccount: '유저 정보를 불러오는데 실패하였습니다. 새로고침하거나, 비밀번호를 초기화 해주세요.',
+ noAccount:
+ '유저 정보를 불러오는데 실패하였습니다. 새로고침하거나, 비밀번호를 초기화 해주세요.',
invalidUser: '유저 이름이나 비밀번호가 틀렸습니다.',
error: '정의되지 않은 에러',
changePassword: '비밀번호 변경',
@@ -52,10 +52,17 @@ const ko = {
quality: '품질',
frameDetect: '프레임 탐지',
frameDetectTip:
- "프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.",
- fitInWindow: "창에 맞추기"
+ '프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.',
+ fitInWindow: '창에 맞추기'
},
- cursor: {
+ keyboard: {
+ paste: '붙여넣기',
+ tips: '표준 키보드 문자 및 기호만 지원됩니다',
+ placeholder: '입력하세요',
+ submit: '제출하다',
+ virtual: '키보드'
+ },
+ mouse: {
default: '기본 커서',
pointer: '포인터 커서',
cell: '셀 커서',
@@ -154,8 +161,7 @@ const ko = {
upTailscale: 'tailscale을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/bin/',
upTailscaled: 'tailscaled을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/sbin/',
refresh: '현재 페이지 새로고침',
- notLogin:
- '이 기기는 현재 연동 되지 않았습니다. 로그인해서 계정에 이 장치를 연동하세요.',
+ notLogin: '이 기기는 현재 연동 되지 않았습니다. 로그인해서 계정에 이 장치를 연동하세요.',
urlPeriod: '이 주소는 10분간 유효합니다.',
login: '로그인',
loginSuccess: '로그인 성공',
diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts
index 66a7201..1071d79 100644
--- a/web/src/i18n/locales/ru.ts
+++ b/web/src/i18n/locales/ru.ts
@@ -3,7 +3,6 @@ const ru = {
language: 'Язык',
changePassword: 'Изменить пароль',
logout: 'Выйти',
- keyboard: 'Клавиатура',
images: 'Образы',
loading: 'Загрузка',
empty: 'Пусто',
@@ -58,9 +57,16 @@ const ru = {
frameDetect: 'Экономия трафика',
frameDetectTip:
'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.',
- fitInWindow: "Вписать в окно",
+ fitInWindow: 'Вписать в окно'
},
- cursor: {
+ keyboard: {
+ paste: 'вставить',
+ tips: 'Только стандартные буквы и символы клавиатуры поддерживаются',
+ placeholder: 'Пожалуйста, введите',
+ submit: 'подтверждать',
+ virtual: 'Клавиатура'
+ },
+ mouse: {
default: 'Курсор по умолчанию',
pointer: 'Курсор: указатель',
cell: 'Курсор: крест',
diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts
index f5d42a6..4399bff 100644
--- a/web/src/i18n/locales/zh.ts
+++ b/web/src/i18n/locales/zh.ts
@@ -4,7 +4,6 @@ const zh = {
latestVersion: '最新版本:',
changePassword: '修改密码',
logout: '登出',
- keyboard: '键盘',
settings: '设置',
showMouse: '显示鼠标',
hideMouse: '隐藏鼠标',
@@ -54,7 +53,14 @@ const zh = {
frameDetectTip: '计算帧之间的差异,当检测到远程主机画面不变时,停止传输视频流',
fitInWindow: '适应窗口'
},
- cursor: {
+ keyboard: {
+ paste: '粘贴',
+ tips: '仅支持标准键盘的字母和符号',
+ placeholder: '请输入内容',
+ submit: '确定',
+ virtual: '虚拟键盘'
+ },
+ mouse: {
default: '默认指针',
pointer: '悬浮指针',
cell: '单元指针',
diff --git a/web/src/pages/desktop/menu-phone/mouse.tsx b/web/src/pages/desktop/menu-phone/mouse.tsx
index 759a554..320119a 100644
--- a/web/src/pages/desktop/menu-phone/mouse.tsx
+++ b/web/src/pages/desktop/menu-phone/mouse.tsx
@@ -15,7 +15,7 @@ import {
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import * as api from '@/api/storage';
+import * as api from '@/api/hid';
import * as ls from '@/lib/localstorage';
import { client } from '@/lib/websocket';
import { mouseModeAtom, mouseStyleAtom } from '@/jotai/mouse';
@@ -27,16 +27,16 @@ export const Mouse = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const mouseStyles = [
- { name: t('cursor.default'), icon: , value: 'cursor-default' },
- { name: t('cursor.grab'), icon: , value: 'cursor-grab' },
- { name: t('cursor.cell'), icon: , value: 'cursor-cell' },
- { name: t('cursor.text'), icon: , value: 'cursor-text' },
- { name: t('cursor.hide'), icon: , value: 'cursor-none' }
+ { name: t('mouse.default'), icon: , value: 'cursor-default' },
+ { name: t('mouse.grab'), icon: , value: 'cursor-grab' },
+ { name: t('mouse.cell'), icon: , value: 'cursor-cell' },
+ { name: t('mouse.text'), icon: , value: 'cursor-text' },
+ { name: t('mouse.hide'), icon: , value: 'cursor-none' }
];
const mouseModes = [
- { name: t('cursor.absolute'), value: 'absolute' },
- { name: t('cursor.relative'), value: 'relative' }
+ { name: t('mouse.absolute'), value: 'absolute' },
+ { name: t('mouse.relative'), value: 'relative' }
];
useEffect(() => {
@@ -73,7 +73,7 @@ export const Mouse = () => {
client.close();
- api.resetHid().finally(() => {
+ api.reset().finally(() => {
client.connect();
});
}
@@ -121,7 +121,7 @@ export const Mouse = () => {
- {t('cursor.mode')}
+ {t('mouse.mode')}
@@ -132,7 +132,7 @@ export const Mouse = () => {
- {t('cursor.resetHid')}
+ {t('mouse.resetHid')}
>
);
diff --git a/web/src/pages/desktop/menu-phone/settings/tailscale/index.tsx b/web/src/pages/desktop/menu-phone/settings/tailscale/index.tsx
index 3e05859..5e32af7 100644
--- a/web/src/pages/desktop/menu-phone/settings/tailscale/index.tsx
+++ b/web/src/pages/desktop/menu-phone/settings/tailscale/index.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Modal } from 'antd';
-import { useSetAtom } from 'jotai/index';
+import { useSetAtom } from 'jotai';
import { LoaderCircleIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
diff --git a/web/src/pages/desktop/menu/index.tsx b/web/src/pages/desktop/menu/index.tsx
index b5ca483..812a39b 100644
--- a/web/src/pages/desktop/menu/index.tsx
+++ b/web/src/pages/desktop/menu/index.tsx
@@ -5,7 +5,7 @@ import { MenuIcon, XIcon } from 'lucide-react';
import { Fullscreen } from './fullscreen.tsx';
import { Image } from './image';
-import { Keyboard } from './keyboard.tsx';
+import { Keyboard } from './keyboard';
import { Mouse } from './mouse.tsx';
import { Power } from './power.tsx';
import { Screen } from './screen';
diff --git a/web/src/pages/desktop/menu/keyboard.tsx b/web/src/pages/desktop/menu/keyboard.tsx
deleted file mode 100644
index 70c7f44..0000000
--- a/web/src/pages/desktop/menu/keyboard.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useSetAtom } from 'jotai';
-import { KeyboardIcon } from 'lucide-react';
-
-import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
-
-export const Keyboard = () => {
- const setIsKeyboardOpen = useSetAtom(isKeyboardOpenAtom);
-
- return (
- <>
- setIsKeyboardOpen((o) => !o)}
- >
-
-
- >
- );
-};
diff --git a/web/src/pages/desktop/menu/keyboard/index.tsx b/web/src/pages/desktop/menu/keyboard/index.tsx
new file mode 100644
index 0000000..e5a4d21
--- /dev/null
+++ b/web/src/pages/desktop/menu/keyboard/index.tsx
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Popover } from 'antd';
+import { KeyboardIcon } from 'lucide-react';
+
+import { Paste } from './paste.tsx';
+import { VirtualKeyboard } from './virtual-keyboard.tsx';
+
+export const Keyboard = () => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const content = (
+ <>
+
+
+ >
+ );
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/web/src/pages/desktop/menu/keyboard/paste.tsx b/web/src/pages/desktop/menu/keyboard/paste.tsx
new file mode 100644
index 0000000..a2efa54
--- /dev/null
+++ b/web/src/pages/desktop/menu/keyboard/paste.tsx
@@ -0,0 +1,122 @@
+import { ChangeEvent, useRef, useState } from 'react';
+import { Button, Input, Modal, type InputRef } from 'antd';
+import clsx from 'clsx';
+import { useSetAtom } from 'jotai';
+import { ClipboardIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import { paste } from '@/api/hid';
+import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
+
+const { TextArea } = Input;
+
+type PasteProps = {
+ setIsPopoverOpen: (open: boolean) => void;
+};
+
+export const Paste = ({ setIsPopoverOpen }: PasteProps) => {
+ const { t } = useTranslation();
+ const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const [status, setStatus] = useState<'' | 'error'>('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [errMsg, setErrMsg] = useState('');
+
+ const inputRef = useRef(null);
+
+ function openModal() {
+ setIsPopoverOpen(false);
+
+ setIsModalOpen(true);
+ }
+
+ function onChange(e: ChangeEvent) {
+ const value = e.target.value;
+ setStatus(isASCII(value) ? '' : 'error');
+ setInputValue(value);
+ }
+
+ function submit() {
+ if (isLoading) return;
+ setIsLoading(true);
+
+ paste(inputValue)
+ .then((rsp) => {
+ if (rsp.code !== 0) {
+ setErrMsg(rsp.msg);
+ return;
+ }
+
+ setInputValue('');
+ setStatus('');
+ setErrMsg('');
+ setIsModalOpen(false);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }
+
+ function afterOpenChange(open: boolean) {
+ if (open) {
+ inputRef.current?.focus();
+ }
+
+ setIsKeyboardEnable(!open);
+ }
+
+ function isASCII(value: string) {
+ for (let i = 0; i < value.length; i++) {
+ if (value.charCodeAt(i) > 127) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return (
+ <>
+
+
+ {t('keyboard.paste')}
+
+
+ setIsModalOpen(false)}
+ afterOpenChange={afterOpenChange}
+ >
+ {t('keyboard.tips')}
+
+
+
+ {errMsg && {errMsg}
}
+
+
+
+
+
+ >
+ );
+};
diff --git a/web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx b/web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx
new file mode 100644
index 0000000..8437f73
--- /dev/null
+++ b/web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx
@@ -0,0 +1,23 @@
+import clsx from 'clsx';
+import { useSetAtom } from 'jotai';
+import { KeyboardIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
+
+export const VirtualKeyboard = () => {
+ const { t } = useTranslation();
+ const setIsKeyboardOpen = useSetAtom(isKeyboardOpenAtom);
+
+ return (
+ setIsKeyboardOpen((o) => !o)}
+ >
+
+ {t('keyboard.virtual')}
+
+ );
+};
diff --git a/web/src/pages/desktop/menu/mouse.tsx b/web/src/pages/desktop/menu/mouse.tsx
index 158a817..d14bf2e 100644
--- a/web/src/pages/desktop/menu/mouse.tsx
+++ b/web/src/pages/desktop/menu/mouse.tsx
@@ -15,7 +15,7 @@ import {
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import * as api from '@/api/storage';
+import * as api from '@/api/hid';
import * as ls from '@/lib/localstorage';
import { client } from '@/lib/websocket';
import { mouseModeAtom, mouseStyleAtom } from '@/jotai/mouse';
@@ -27,16 +27,16 @@ export const Mouse = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const mouseStyles = [
- { name: t('cursor.default'), icon: , value: 'cursor-default' },
- { name: t('cursor.grab'), icon: , value: 'cursor-grab' },
- { name: t('cursor.cell'), icon: , value: 'cursor-cell' },
- { name: t('cursor.text'), icon: , value: 'cursor-text' },
- { name: t('cursor.hide'), icon: , value: 'cursor-none' }
+ { name: t('mouse.default'), icon: , value: 'cursor-default' },
+ { name: t('mouse.grab'), icon: , value: 'cursor-grab' },
+ { name: t('mouse.cell'), icon: , value: 'cursor-cell' },
+ { name: t('mouse.text'), icon: , value: 'cursor-text' },
+ { name: t('mouse.hide'), icon: , value: 'cursor-none' }
];
const mouseModes = [
- { name: t('cursor.absolute'), value: 'absolute' },
- { name: t('cursor.relative'), value: 'relative' }
+ { name: t('mouse.absolute'), value: 'absolute' },
+ { name: t('mouse.relative'), value: 'relative' }
];
useEffect(() => {
@@ -73,7 +73,7 @@ export const Mouse = () => {
client.close();
- api.resetHid().finally(() => {
+ api.reset().finally(() => {
client.connect();
});
}
@@ -121,7 +121,7 @@ export const Mouse = () => {
- {t('cursor.mode')}
+ {t('mouse.mode')}
@@ -132,7 +132,7 @@ export const Mouse = () => {
- {t('cursor.resetHid')}
+ {t('mouse.resetHid')}
>
);
diff --git a/web/src/pages/desktop/menu/screen/frame-detect.tsx b/web/src/pages/desktop/menu/screen/frame-detect.tsx
index da6c3a4..a750e21 100644
--- a/web/src/pages/desktop/menu/screen/frame-detect.tsx
+++ b/web/src/pages/desktop/menu/screen/frame-detect.tsx
@@ -36,15 +36,21 @@ export const FrameDetect = () => {
return (
{isLoading ? (
) : isEnabled ? (
-
+
+
+
+
) : (
-
+
+
+
+
)}
{t('screen.frameDetect')}
diff --git a/web/src/pages/desktop/menu/settings/tailscale/index.tsx b/web/src/pages/desktop/menu/settings/tailscale/index.tsx
index 7a81174..4a8764f 100644
--- a/web/src/pages/desktop/menu/settings/tailscale/index.tsx
+++ b/web/src/pages/desktop/menu/settings/tailscale/index.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Modal } from 'antd';
-import { useSetAtom } from 'jotai/index';
+import { useSetAtom } from 'jotai';
import { LoaderCircleIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
diff --git a/web/src/pages/desktop/mouse/relative.tsx b/web/src/pages/desktop/mouse/relative.tsx
index 867f981..9c13451 100644
--- a/web/src/pages/desktop/mouse/relative.tsx
+++ b/web/src/pages/desktop/mouse/relative.tsx
@@ -24,7 +24,7 @@ export const Relative = () => {
messageApi.open({
type: 'info',
- content: t('cursor.requestPointer'),
+ content: t('mouse.requestPointer'),
duration: 3,
style: {
marginTop: '40vh'