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')}
+ +