update menu

- update frame detect icons
- rename mouse translation
- support pasting from local to remote
This commit is contained in:
wj-xiao
2024-09-06 10:54:53 +08:00
parent eb772ae33d
commit e34f085976
18 changed files with 270 additions and 71 deletions

11
web/src/api/hid.ts Normal file
View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: '로그인 성공',

View File

@@ -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: 'Курсор: крест',

View File

@@ -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: '单元指针',

View File

@@ -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: <MousePointerIcon size={14} />, value: 'cursor-default' },
{ name: t('cursor.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
{ name: t('cursor.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
{ name: t('cursor.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
{ name: t('cursor.hide'), icon: <EyeOffIcon size={14} />, value: 'cursor-none' }
{ name: t('mouse.default'), icon: <MousePointerIcon size={14} />, value: 'cursor-default' },
{ name: t('mouse.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
{ name: t('mouse.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
{ name: t('mouse.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
{ name: t('mouse.hide'), icon: <EyeOffIcon size={14} />, 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 = () => {
<div className="flex h-[14px] w-[20px] items-end">
<SquareMousePointerIcon size={14} />
</div>
<span>{t('cursor.mode')}</span>
<span>{t('mouse.mode')}</span>
</div>
</Popover>
@@ -132,7 +132,7 @@ export const Mouse = () => {
<div className="flex h-[14px] w-[20px] items-end">
<RefreshCwIcon size={14} />
</div>
<span>{t('cursor.resetHid')}</span>
<span>{t('mouse.resetHid')}</span>
</div>
</>
);

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 (
<>
<div
className="flex h-[30px] cursor-pointer items-center justify-center rounded px-2 text-neutral-300 hover:bg-neutral-700"
onClick={() => setIsKeyboardOpen((o) => !o)}
>
<KeyboardIcon size={18} />
</div>
</>
);
};

View File

@@ -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 = (
<>
<Paste setIsPopoverOpen={setIsPopoverOpen} />
<VirtualKeyboard />
</>
);
return (
<Popover
content={content}
placement="bottomLeft"
trigger="click"
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
>
<div className="flex h-[30px] cursor-pointer items-center rounded px-2 text-neutral-300 hover:bg-neutral-700">
<KeyboardIcon size={18} />
</div>
</Popover>
);
};

View File

@@ -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<InputRef>(null);
function openModal() {
setIsPopoverOpen(false);
setIsModalOpen(true);
}
function onChange(e: ChangeEvent<HTMLTextAreaElement>) {
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 (
<>
<div
className={clsx(
'flex cursor-pointer select-none items-center space-x-2 rounded py-1 pl-2 pr-5 hover:bg-neutral-600'
)}
onClick={openModal}
>
<ClipboardIcon size={18} />
<span>{t('keyboard.paste')}</span>
</div>
<Modal
open={isModalOpen}
centered={false}
title={t('keyboard.paste')}
footer={null}
onCancel={() => setIsModalOpen(false)}
afterOpenChange={afterOpenChange}
>
<div className="pb-3 text-xs text-neutral-500">{t('keyboard.tips')}</div>
<TextArea
ref={inputRef}
value={inputValue}
status={status}
showCount
maxLength={1024}
autoSize={{ minRows: 5, maxRows: 12 }}
placeholder={t('keyboard.placeholder')}
onChange={onChange}
/>
{errMsg && <div className="pt-1 text-sm text-red-500">{errMsg}</div>}
<div className="flex justify-center py-3">
<Button type="primary" loading={isLoading} htmlType="submit" onClick={submit}>
{t('keyboard.submit')}
</Button>
</div>
</Modal>
</>
);
};

View File

@@ -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 (
<div
className={clsx(
'flex cursor-pointer select-none items-center space-x-2 rounded py-1 pl-2 pr-5 hover:bg-neutral-600'
)}
onClick={() => setIsKeyboardOpen((o) => !o)}
>
<KeyboardIcon size={18} />
<span>{t('keyboard.virtual')}</span>
</div>
);
};

View File

@@ -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: <MousePointerIcon size={14} />, value: 'cursor-default' },
{ name: t('cursor.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
{ name: t('cursor.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
{ name: t('cursor.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
{ name: t('cursor.hide'), icon: <EyeOffIcon size={14} />, value: 'cursor-none' }
{ name: t('mouse.default'), icon: <MousePointerIcon size={14} />, value: 'cursor-default' },
{ name: t('mouse.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
{ name: t('mouse.cell'), icon: <PlusIcon size={14} />, value: 'cursor-cell' },
{ name: t('mouse.text'), icon: <TextCursorIcon size={14} />, value: 'cursor-text' },
{ name: t('mouse.hide'), icon: <EyeOffIcon size={14} />, 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 = () => {
<div className="flex h-[14px] w-[20px] items-end">
<SquareMousePointerIcon size={14} />
</div>
<span>{t('cursor.mode')}</span>
<span>{t('mouse.mode')}</span>
</div>
</Popover>
@@ -132,7 +132,7 @@ export const Mouse = () => {
<div className="flex h-[14px] w-[20px] items-end">
<RefreshCwIcon size={14} />
</div>
<span>{t('cursor.resetHid')}</span>
<span>{t('mouse.resetHid')}</span>
</div>
</>
);

View File

@@ -36,15 +36,21 @@ export const FrameDetect = () => {
return (
<Tooltip placement="rightTop" title={t('screen.frameDetectTip')} color="#262626" arrow>
<div
className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700"
className="group flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700"
onClick={update}
>
{isLoading ? (
<LoaderCircleIcon className="animate-spin" size={18} />
) : isEnabled ? (
<Tally4Icon color="#22c55e" size={18} />
<div>
<Tally4Icon color="#22c55e" size={18} className="block group-hover:hidden" />
<Tally5Icon color="#ef4444" size={18} className="hidden group-hover:block" />
</div>
) : (
<Tally5Icon size={18} />
<div>
<Tally5Icon size={18} className="block group-hover:hidden" />
<Tally4Icon color="#22c55e" size={18} className="hidden group-hover:block" />
</div>
)}
<span className="select-none text-sm">{t('screen.frameDetect')}</span>
</div>

View File

@@ -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';

View File

@@ -24,7 +24,7 @@ export const Relative = () => {
messageApi.open({
type: 'info',
content: t('cursor.requestPointer'),
content: t('mouse.requestPointer'),
duration: 3,
style: {
marginTop: '40vh'