mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
add the power operation confirm selection in WEB UI
This commit is contained in:
@@ -173,7 +173,8 @@ const en = {
|
||||
reset: 'Reset',
|
||||
power: 'Power',
|
||||
powerShort: 'Power (short click)',
|
||||
powerLong: 'Power (long click)'
|
||||
powerLong: 'Power (long click)',
|
||||
powerConfirm: 'Are you sure you want to perform this operation?'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
@@ -225,9 +226,14 @@ const en = {
|
||||
tip: 'Enable the default swap partition(128M)'
|
||||
},
|
||||
mouseJiggler: {
|
||||
title: "Mouse Jiggler",
|
||||
title: 'Mouse Jiggler',
|
||||
description: 'Enable the mouse jiggler',
|
||||
tip: "Automatically jiggles the mouse when inactive for 60 seconds to prevent system sleep"
|
||||
tip: 'Automatically jiggles the mouse when inactive for 60 seconds to prevent system sleep'
|
||||
},
|
||||
powerConfirm: {
|
||||
title: 'Power Operation Reconfirmation',
|
||||
description: 'Enable power operation reconfirmation',
|
||||
tip: 'When operating the power, a double confirmation dialog will pop up to prevent accidental touches'
|
||||
},
|
||||
mdns: {
|
||||
description: 'Enable mDNS discovery service',
|
||||
|
||||
@@ -23,6 +23,8 @@ const zh = {
|
||||
illegalUsername: '用户名中包含非法字符',
|
||||
illegalPassword: '密码中包含非法字符',
|
||||
forgetPassword: '忘记密码',
|
||||
reboot: '重启',
|
||||
rebootDesc: '你确定要重启NanoKVM吗?',
|
||||
ok: '确定',
|
||||
cancel: '取消',
|
||||
loginButtonText: '登录',
|
||||
@@ -157,7 +159,8 @@ const zh = {
|
||||
reset: '重启',
|
||||
power: '电源',
|
||||
powerShort: '电源(短按)',
|
||||
powerLong: '电源(长按)'
|
||||
powerLong: '电源(长按)',
|
||||
powerConfirm: '你确定要执行此操作吗?'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
@@ -213,6 +216,11 @@ const zh = {
|
||||
description: '启用鼠标抖动',
|
||||
tip: '在鼠标离开60s未操作时自动进行抖动以防止系统睡眠'
|
||||
},
|
||||
powerConfirm:{
|
||||
title: '电源操作确认',
|
||||
description: '启用电源操作二次确认',
|
||||
tip: '在操作电源时弹出二次确认框以防误触'
|
||||
},
|
||||
mdns: {
|
||||
description: '启用 mDNS 发现服务',
|
||||
tip: '如果您未使用此功能,建议将其关闭'
|
||||
@@ -221,7 +229,9 @@ const zh = {
|
||||
disk: '虚拟U盘',
|
||||
diskDesc: '在远程主机中挂载虚拟U盘',
|
||||
network: '虚拟网卡',
|
||||
networkDesc: '在远程主机中挂载虚拟网卡'
|
||||
networkDesc: '在远程主机中挂载虚拟网卡',
|
||||
okBtn: '是',
|
||||
cancelBtn: '否'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
|
||||
@@ -13,6 +13,7 @@ const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
||||
const KEYBOARD_LAYOUT_KEY = 'nano-kvm-keyboard-layout';
|
||||
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
|
||||
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
|
||||
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
|
||||
|
||||
type ItemWithExpiry = {
|
||||
value: string;
|
||||
@@ -165,3 +166,12 @@ export function getMenuDisabledItems(): string[] {
|
||||
const value = localStorage.getItem(MENU_DISABLED_ITEMS_KEY);
|
||||
return value ? JSON.parse(value) : [];
|
||||
}
|
||||
|
||||
export function getPowerConfirm(): string | null {
|
||||
return getWithExpiry(POWER_CONFIRM_KEY);
|
||||
}
|
||||
|
||||
export function setPowerConfirm(value: boolean) {
|
||||
const expiry = 365 * 24 * 60 * 60 * 1000;
|
||||
setWithExpiry(POWER_CONFIRM_KEY, String(value), expiry);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Slider } from 'antd';
|
||||
import { Popconfirm, Slider } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { CirclePowerIcon, LoaderCircleIcon, PowerIcon, RotateCcwIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getPowerConfirm } from '../../../../lib/localstorage'
|
||||
|
||||
import * as api from '@/api/vm';
|
||||
import { MenuItem } from '@/components/menu-item.tsx';
|
||||
@@ -13,10 +14,10 @@ export const Power = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [powerDuration, setPowerDuration] = useState(8);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getLed();
|
||||
const interval = setInterval(getLed, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
@@ -33,7 +34,10 @@ export const Power = () => {
|
||||
});
|
||||
}
|
||||
|
||||
function click(button: 'reset' | 'power', duration?: number) {
|
||||
function click(button: 'reset' | 'power',type: string, duration?: number) {
|
||||
|
||||
if(getPowerConfirm() == 'true' && type == 'first') return;
|
||||
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -48,6 +52,8 @@ export const Power = () => {
|
||||
setPowerDuration(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const icon = (
|
||||
<div className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-500')}>
|
||||
{isLoading ? (
|
||||
@@ -60,30 +66,63 @@ export const Power = () => {
|
||||
|
||||
const content = (
|
||||
<div className="flex flex-col space-y-1">
|
||||
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('power.reset')}
|
||||
description={t('power.powerConfirm')}
|
||||
okText={t('settings.device.okBtn')}
|
||||
cancelText={t('settings.device.cancelBtn')}
|
||||
onConfirm={() => click('reset', 'twice', 0.8)}
|
||||
disabled={!(getPowerConfirm() == 'true')}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('reset', 0.8)}
|
||||
onClick={() => click('reset', 'first', 0.8)}
|
||||
>
|
||||
<RotateCcwIcon size={16} />
|
||||
<span>{t('power.reset')}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('power.powerShort')}
|
||||
description={t('power.powerConfirm')}
|
||||
okText={t('settings.device.okBtn')}
|
||||
cancelText={t('settings.device.cancelBtn')}
|
||||
onConfirm={() => click('power', 'twice', 0.8)}
|
||||
disabled={!(getPowerConfirm() == 'true')}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('power', 0.8)}
|
||||
onClick={() => click('power', 'first', 0.8)}
|
||||
>
|
||||
<PowerIcon size={16} />
|
||||
<span>{t('power.powerShort')}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
|
||||
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('power.powerLong')}
|
||||
description={t('power.powerConfirm')}
|
||||
okText={t('settings.device.okBtn')}
|
||||
cancelText={t('settings.device.cancelBtn')}
|
||||
onConfirm={() => click('power', 'twice')}
|
||||
disabled={!(getPowerConfirm() == 'true')}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 hover:bg-neutral-700/70"
|
||||
onClick={() => click('power')}
|
||||
onClick={() => click('power', 'first')}
|
||||
>
|
||||
<CirclePowerIcon size={16} />
|
||||
<span>{t('power.powerLong')}</span>
|
||||
<div className="flex h-full items-start text-xs text-neutral-500">{`${powerDuration}s`}</div>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
defaultValue={8}
|
||||
|
||||
57
web/src/pages/desktop/menu/settings/device/PowerConfirm.tsx
Normal file
57
web/src/pages/desktop/menu/settings/device/PowerConfirm.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import { CircleAlertIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setPowerConfirm, getPowerConfirm } from '../../../../../lib/localstorage';
|
||||
|
||||
export const PowerConfirm = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
const cookieValue = getPowerConfirm();
|
||||
if (cookieValue === 'true') {
|
||||
setIsEnabled(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
}, []);
|
||||
|
||||
function update() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const newValue = !isEnabled;
|
||||
setPowerConfirm(newValue);
|
||||
|
||||
setIsLoading(false);
|
||||
setIsEnabled(!isEnabled);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t('settings.device.powerConfirm.title')}</span>
|
||||
|
||||
<Tooltip
|
||||
title={t('settings.device.powerConfirm.tip')}
|
||||
className="cursor-pointer"
|
||||
placement="bottom"
|
||||
overlayStyle={{ maxWidth: '300px' }}
|
||||
>
|
||||
<CircleAlertIcon size={15} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="text-xs text-neutral-500">{t('settings.device.powerConfirm.description')}</span>
|
||||
</div>
|
||||
|
||||
<Switch checked={isEnabled} loading={isLoading} onChange={update} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { VirtualDevices } from './virtual-devices.tsx';
|
||||
import { Wifi } from './wifi.tsx';
|
||||
import { Swap } from './swap.tsx';
|
||||
import { MouseJiggler } from './mouse-jiggler.tsx';
|
||||
import { PowerConfirm } from './PowerConfirm.tsx';
|
||||
|
||||
export const Device = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -42,6 +43,7 @@ export const Device = () => {
|
||||
<Swap />
|
||||
<MouseJiggler />
|
||||
{hidMode === 'normal' ? <VirtualDevices /> : <HidMode />}
|
||||
<PowerConfirm />
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user