mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
update menu
- update frame detect icons - rename mouse translation - support pasting from local to remote
This commit is contained in:
11
web/src/api/hid.ts
Normal file
11
web/src/api/hid.ts
Normal 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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '로그인 성공',
|
||||
|
||||
@@ -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: 'Курсор: крест',
|
||||
|
||||
@@ -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: '单元指针',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
31
web/src/pages/desktop/menu/keyboard/index.tsx
Normal file
31
web/src/pages/desktop/menu/keyboard/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
122
web/src/pages/desktop/menu/keyboard/paste.tsx
Normal file
122
web/src/pages/desktop/menu/keyboard/paste.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
23
web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx
Normal file
23
web/src/pages/desktop/menu/keyboard/virtual-keyboard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const Relative = () => {
|
||||
|
||||
messageApi.open({
|
||||
type: 'info',
|
||||
content: t('cursor.requestPointer'),
|
||||
content: t('mouse.requestPointer'),
|
||||
duration: 3,
|
||||
style: {
|
||||
marginTop: '40vh'
|
||||
|
||||
Reference in New Issue
Block a user