mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
Merge branch 'main' of https://github.com/sipeed/NanoKVM
This commit is contained in:
110
README_JA.md
110
README_JA.md
@@ -1,11 +1,13 @@
|
||||
# NanoKVM
|
||||
NanoKVM
|
||||
======
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html"> クイックスタート </a> |
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> クイックスタート </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> ハードウェア </a>
|
||||
</h3>
|
||||
|
||||
@@ -13,18 +15,29 @@
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> NanoKVM は [RISC-V](https://en.wikipedia.org/wiki/RISC-V) を搭載しています!
|
||||
> 使用中に問題や提案がある場合は、ここで issue を提出するか、[MaixHub Discussion](https://maixhub.com/discussion/nanokvm) でお知らせください。
|
||||
|
||||
## オープンソース & 貢献 & 出荷
|
||||
## オープンソース & コントリビューション
|
||||
|
||||
スターが2Kに近づいています!10月中旬にバックエンドのソースコードを整理してオープンソース化します!(また、10.1前のすべての予約注文が出荷される時期でもあります)
|
||||
### 2025.02.19 更新
|
||||
|
||||
### 10.18 更新
|
||||
NanoKVM のすべてのコンポーネントはオープンソース化されており、[front-end](https://github.com/sipeed/NanoKVM/tree/main/web)、[back-end](https://github.com/sipeed/NanoKVM/tree/main/server)、[kvm_vision](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm)、[kvm_mmf](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm_mmf)、[kvm_system](https://github.com/sipeed/NanoKVM/tree/main/support)、[kvmapp update package](https://github.com/sipeed/NanoKVM-System/tree/main/kvmapp)、[system sdk](https://github.com/sipeed/LicheeRV-Nano-Build/tree/NanoKVM)、[packaging methods](https://github.com/sipeed/LicheeRV-Nano-Build/blob/NanoKVM/kvm/NanoKVM_img.sh)が含まれます。
|
||||
|
||||
10.8にバックエンドコードをオープンソース化しました。すぐにPRを提出してくれたCivilに感謝します。最新のNanoKVM-PCIeを報酬として提供します!
|
||||
10月中旬のバッチはテストとパッケージングを開始しており、10.1前のほとんどの注文は次週と次々週に出荷されます。
|
||||
10.1以降、アリエクスプレスの出荷日は12月に変更されました。これは保守的な納期です。10月の注文は11.15〜12.15の間に発送される予定です。
|
||||
### 2025.02.14 更新
|
||||
|
||||
最新のアプリケーションバージョン v2.1.6 には、多くのセキュリティ強化とバグ修正が追加されているため、一般のユーザーはこのバージョン以降へのアップグレードをお勧めします。
|
||||
|
||||
### 2025.02.05 更新
|
||||
|
||||
セキュリティを懸念しているユーザーは、この issue をお読みください: <https://github.com/sipeed/NanoKVM/issues/301>、セキュリティに関するすべての懸念事項について説明されています。
|
||||
GitHub は、オープンで透明性のあるプラットフォームとして、いわゆる「バックドア」の懸念事項を議論する場を提供しています。オープンソースであることにより、製品のセキュリティがさらに強化されます!
|
||||
|
||||
### 2024.10.18 更新
|
||||
|
||||
10/08にバックエンドコードをオープンソース化しました。すぐにPRを提出してくれたCivilに感謝します。最新のNanoKVM-PCIeを報酬として提供します!
|
||||
10月中旬のバッチはテストとパッケージングを開始しており、10/01以前のほとんどの注文は次週と次々週に出荷されます。
|
||||
10/01以降、アリエクスプレスの出荷日は12月に変更されました。これは保守的な納期です。10月の注文は11/15〜12/15の間に発送される予定です。
|
||||
|
||||
## 紹介
|
||||
|
||||
@@ -33,35 +46,35 @@ Lichee NanoKVMは2つのバージョンがあります:
|
||||
NanoKVM Liteは基本構成で、一定のDIY能力を持つ個人ユーザーや大量のニーズを持つ企業ユーザーに適しています。
|
||||
NanoKVM Fullは完全版で、洗練されたケースと完全なアクセサリーを備え、起動時にすぐに使用できるシステムミラーカードが内蔵されており、個人ユーザーに推奨されます。
|
||||
|
||||

|
||||

|
||||
|
||||
## 技術仕様
|
||||
|
||||
| 製品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
| --- | --- | --- | --- |
|
||||
| 計算ユニット | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 解像度 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| ビデオエンコーディング | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
|
||||
| ビデオ遅延 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 仮想HID | ✓ | ✓ | ✓ |
|
||||
| 仮想CD-ROM | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX電源制御 | なし、ユーザーが自分で接続 | USBインターフェースIO制御ボード | RJ-45インターフェースIO制御ボード |
|
||||
| OLED | なし、ユーザーが自分で接続 | 128x64 0.96" 白 | 128x32 0.91" 白 |
|
||||
| UART | 2 | 2 | 1 |
|
||||
| TFカード | なし | ✓ | ✓ |
|
||||
| 拡張 | なし | PoE | WiFi/LTE |
|
||||
| 消費電力 | 0.2A@5V | 0.2A@5V | ピーク2.6A@5V |
|
||||
| 電源入力 | PC USBで給電可能 | PC USBまたは補助電源 | DC 5V 3A電源が必要 |
|
||||
| 冷却 | 静音ファンレス | 静音ファンレス | ファン冷却 |
|
||||
| サイズ | 23x37x15mm <br> ~1/30 PiKVM V4サイズ | 40x36x36mm <br/> ~1/7 PiKVM V4サイズ | 120x68x44mm |
|
||||
| 製品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
|----------------------- |-------------------------------------- |---------------------------------- |----------------------------------- |
|
||||
| 計算ユニット | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 解像度 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| ビデオエンコーディング | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
|
||||
| ビデオ遅延 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 仮想HID | ✓ | ✓ | ✓ |
|
||||
| 仮想CD-ROM | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX電源制御 | なし、ユーザーが自分で接続 | USBインターフェースIO制御ボード | RJ-45インターフェースIO制御ボード |
|
||||
| OLED | なし、ユーザーが自分で接続 | 128x64 0.96" 白 | 128x32 0.91" 白 |
|
||||
| UART | 2 | 2 | 1 |
|
||||
| TFカード | なし | ✓ | ✓ |
|
||||
| 拡張 | なし | PoE | WiFi/LTE |
|
||||
| 消費電力 | 0.2A@5V | 0.2A@5V | ピーク時 2.6A@5V |
|
||||
| 電源入力 | PC USBで給電可能 | PC USBまたは補助電源 | DC 5V 3A電源が必要 |
|
||||
| 冷却 | 静音ファンレス | 静音ファンレス | ファン冷却 |
|
||||
| サイズ | 23x37x15mm ~1/30 PiKVM V4サイズ | 40x36x36mm ~1/7 PiKVM V4サイズ | 120x68x44mm |
|
||||
|
||||

|
||||

|
||||
|
||||
## ハードウェアプラットフォーム NanoKVM
|
||||
## NanoKVM ハードウェアプラットフォーム
|
||||
|
||||
NanoKVMはSipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html)に基づいており、仕様、回路図、寸法図は[こちら](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)で確認できます。
|
||||
|
||||
@@ -69,12 +82,41 @@ NanoKVM LiteはLicheeRV NanoとHDMItoCSIボードで構成され、NanoKVM FULL
|
||||
|
||||
NanoKVMイメージはLicheeRV Nano SDKおよびMaixCDKに基づいて構築されており、LicheeRV Nanoを使用する資料と互換性があります。逆に、KVMソフトウェアはLicheeRV Nanoや他のSG2002製品では使用できません。LicheeRV NanoやMaixCamでHDMI入力アプリケーションを構築したい場合は、技術サポートを受けるためにお問い合わせください。
|
||||
|
||||
注:SG2002の256MBメモリのうち、現在105MBがマルチメディアサブシステムに割り当てられており、NanoKVMはビデオ画像の取得と処理にこのメモリを使用します。
|
||||
注:SG2002の256MBメモリのうち、現在158MBがマルチメディアサブシステムに割り当てられており、NanoKVMはビデオ画像の取得と処理にこのメモリを使用します。
|
||||
|
||||
+ [NanoKVM-A 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
+ [NanoKVM-B 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf)
|
||||
+ [NanoKVM イメージダウンロード](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## ロードマップ
|
||||
|
||||
### Q1 2025
|
||||
|
||||
**改善:**
|
||||
|
||||
- [x] HDMI モジュールをリファクタリングし、libmaixcam_lib.so の依存関係を削除
|
||||
- [x] SSH をデフォルトで無効にし、Web UI に有効/無効の切り替えを追加
|
||||
- [x] Tailscale をデフォルトで無効にし、Web UI に有効/無効の切り替えを追加
|
||||
- [x] 設定ファイルに JWT 設定オプションを追加
|
||||
- [x] bcrypt を利用したパスワードストレージのセキュリティ保護
|
||||
|
||||
**配布:**
|
||||
|
||||
- [x] GitHub 経由でのアプリケーション配布
|
||||
- [x] オンラインでの更新の際に整合性チェックを実施
|
||||
- [ ] オフラインでの更新をサポート
|
||||
|
||||
**バグ修正:**
|
||||
|
||||
- [x] DNS の問題を修正
|
||||
- [x] CSRF の脆弱性を修正
|
||||
|
||||
**機能**
|
||||
|
||||
- [ ] [79](https://github.com/sipeed/NanoKVM/issues/79) Zerotier をサポート
|
||||
- [ ] [99](https://github.com/sipeed/NanoKVM/issues/99) WireGuard をサポート
|
||||
- [ ] [249](https://github.com/sipeed/NanoKVM/issues/249) Mouse Jiggler を追加
|
||||
|
||||
## 購入方法
|
||||
|
||||
* [Aliexpress(グローバル、米国とロシアを除く)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
|
||||
@@ -16,7 +16,7 @@ server
|
||||
├── middleware // サーバーミドルウェアコンポーネント
|
||||
├── proto // API リクエスト/レスポンス定義
|
||||
├── router // API ルートハンドラ
|
||||
├── service // コアサービスの実<EFBFBD><EFBFBD>
|
||||
├── service // コアサービスの実装
|
||||
├── utils // ユーティリティ関数
|
||||
└── main.go
|
||||
```
|
||||
@@ -39,17 +39,23 @@ cert:
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
|
||||
# 認証設定 (enable/disable)
|
||||
# 注意: 認証を無効にするのは開発環境でのみ行ってください
|
||||
authentication: enable
|
||||
|
||||
# JWT 秘密鍵の設定
|
||||
# 空のままにすると、サーバー起動時にランダムな鍵が生成されます
|
||||
secretKey: ""
|
||||
jwt:
|
||||
# JWT 秘密鍵の設定。 空のままにすると、サーバー起動時にランダムな 64 バイトの鍵が自動的に生成されます。
|
||||
secretKey: ""
|
||||
# JWT トークンの有効期限(秒単位)。 デフォルト: 2678400 (31 日)
|
||||
refreshTokenDuration: 2678400
|
||||
# ユーザーがログアウトすると、すべての JWT トークンが無効になります。 デフォルト: true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# カスタム STUN サーバーのアドレス
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
# カスタム TURN サーバーのアドレスと認証情報
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnUser: example_user
|
||||
@@ -80,3 +86,12 @@ turn:
|
||||
1. デプロイ前に、ブラウザでアプリケーションを最新バージョンに更新します。手順は[こちら](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/updating.html)を参照してください。
|
||||
2. コンパイルして生成された `NanoKVM-Server` ファイルを使用して、NanoKVM の `/kvmapp/server/` ディレクトリ内の元のファイルを置き換えます。
|
||||
3. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
|
||||
## 手動更新
|
||||
|
||||
> ファイルのアップロードには SSH が必要です。Web 設定で有効にしてください: `設定 > SSH`
|
||||
|
||||
1. [GitHub](https://github.com/sipeed/NanoKVM/releases) から最新のアプリケーションをダウンロードします。
|
||||
2. ダウンロードしたファイルを解凍し、解凍したフォルダーの名前を `kvmapp` に変更します。
|
||||
3. NanoKVM 上の既存の `/kvmapp` ディレクトリをバックアップし、新しい `kvmapp` フォルダーに置き換えます。
|
||||
4. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
|
||||
@@ -22,6 +22,8 @@ src
|
||||
|
||||
## ローカル開発
|
||||
|
||||
> 開発には SSH が必要です。Web 設定で有効にすることができます: `設定 > SSH`
|
||||
|
||||
CORS 制限のため、ローカル開発中は認証を無効にする必要があります。
|
||||
|
||||
認証機能を開発するには、プロジェクトをビルドして NanoKVM でテストする必要があります。
|
||||
@@ -55,4 +57,7 @@ pnpm build
|
||||
3. `web` を NanoKVM の `/kvmapp/server/` にアップロードします。
|
||||
4. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
|
||||
また、ブラウザに古いバージョンのキャッシュが残っている可能性があります。ページが開かない場合は、強制リフレッシュまたはキャッシュのクリアを試してください。
|
||||
Tips:
|
||||
|
||||
1. ファイルのアップロードには SSH が必要です。Web 設定で有効にすることができます: `設定 > SSH`
|
||||
2. ブラウザに古いバージョンのキャッシュが残っている可能性があります。ページが開かない場合は、強制リフレッシュまたはキャッシュのクリアを試してください。
|
||||
|
||||
@@ -47,6 +47,7 @@ const en = {
|
||||
finishBtn: 'Finished'
|
||||
},
|
||||
screen: {
|
||||
title: 'Screen',
|
||||
video: 'Video Mode',
|
||||
resolution: 'Resolution',
|
||||
auto: 'Automatic',
|
||||
@@ -65,6 +66,7 @@ const en = {
|
||||
resetHdmi: 'Reset HDMI'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Keyboard',
|
||||
paste: 'Paste',
|
||||
tips: 'Only standard keyboard letters and symbols are supported',
|
||||
placeholder: 'Please input',
|
||||
@@ -73,6 +75,7 @@ const en = {
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
default: 'Default cursor',
|
||||
pointer: 'Pointer cursor',
|
||||
cell: 'Cell cursor',
|
||||
@@ -86,7 +89,7 @@ const en = {
|
||||
resetHid: 'Reset HID'
|
||||
},
|
||||
image: {
|
||||
title: 'Image',
|
||||
title: 'Images',
|
||||
loading: 'Loading...',
|
||||
empty: 'Nothing Found',
|
||||
cdrom: 'Mount the image in CD-ROM mode',
|
||||
@@ -110,7 +113,7 @@ const en = {
|
||||
}
|
||||
},
|
||||
script: {
|
||||
title: 'Script',
|
||||
title: 'Scripts',
|
||||
upload: 'Upload',
|
||||
run: 'Run',
|
||||
runBackground: 'Run Background',
|
||||
@@ -139,7 +142,7 @@ const en = {
|
||||
ok: 'Ok'
|
||||
},
|
||||
download: {
|
||||
title: 'Download Image',
|
||||
title: 'Image Downloader',
|
||||
input: 'Please enter a remote image URL',
|
||||
ok: 'Ok',
|
||||
disabled: '/data partition is RO, so we cannot download the image'
|
||||
@@ -257,8 +260,15 @@ const en = {
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: "We've ran into an issue",
|
||||
refresh: 'Refresh'
|
||||
title: 'We\'ve ran into an issue',
|
||||
refresh: 'Refresh',
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: 'Toggle Fullscreen',
|
||||
},
|
||||
menu: {
|
||||
collapse: 'Collapse Menu',
|
||||
expand: 'Expand Menu',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Divider, Input, Popover } from 'antd';
|
||||
import { Button, Divider, Input, Popover, Tooltip } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -21,6 +21,9 @@ export const DownloadImage = () => {
|
||||
const [diskEnabled, setDiskEnabled] = useState(false);
|
||||
const [popoverKey, setPopoverKey] = useState(0);
|
||||
|
||||
const tooltip = t('download.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
const intervalId = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
@@ -165,11 +168,16 @@ export const DownloadImage = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
onOpenChange={(visible) => {
|
||||
handleOpenChange(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<DownloadIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<DownloadIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MaximizeIcon, MinimizeIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
export const Fullscreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [tooltipValue, setTooltipValue] = useState(t('fullscreen.toggle'));
|
||||
|
||||
useEffect(() => {
|
||||
function onFullscreenChange() {
|
||||
@@ -27,11 +31,13 @@ export const Fullscreen = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white sm:flex"
|
||||
onClick={handleFullscreen}
|
||||
>
|
||||
{isFullscreen ? <MinimizeIcon size={18} /> : <MaximizeIcon size={18} />}
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div
|
||||
className="hidden h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white sm:flex"
|
||||
onClick={handleFullscreen}
|
||||
>
|
||||
{isFullscreen ? <MinimizeIcon size={18} /> : <MaximizeIcon size={18} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,9 @@ export const Image = () => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [cdrom, setCdrom] = useState(false);
|
||||
|
||||
const tooltip = t('image.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
useEffect(() => {
|
||||
getMountedImage().then((rsp) => {
|
||||
if (rsp.code !== 0) return;
|
||||
@@ -60,16 +63,21 @@ export const Image = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
onOpenChange={(visible) => {
|
||||
setIsPopoverOpen(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700',
|
||||
isMounted ? 'text-blue-500' : 'text-neutral-300 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<DiscIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700',
|
||||
isMounted ? 'text-blue-500' : 'text-neutral-300 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<DiscIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Divider } from 'antd';
|
||||
import { Divider, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { MenuIcon, XIcon } from 'lucide-react';
|
||||
@@ -18,6 +18,7 @@ import { Script } from './script';
|
||||
import { Settings } from './settings';
|
||||
import { Terminal } from './terminal';
|
||||
import { Wol } from './wol';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const Menu = () => {
|
||||
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
|
||||
@@ -67,21 +68,25 @@ export const Menu = () => {
|
||||
<Settings />
|
||||
<Fullscreen />
|
||||
|
||||
<div
|
||||
className="mr-1 flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white"
|
||||
onClick={() => setIsMenuOpen((o) => !o)}
|
||||
>
|
||||
<XIcon size={20} />
|
||||
</div>
|
||||
<Tooltip title={t('menu.collapse')} placement="bottom">
|
||||
<div
|
||||
className="mr-1 flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white"
|
||||
onClick={() => setIsMenuOpen((o) => !o)}
|
||||
>
|
||||
<XIcon size={20} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{!isMenuOpen && (
|
||||
<div
|
||||
className="flex h-[30px] w-[50px] items-center justify-center rounded bg-neutral-800/50 text-white/50 hover:bg-neutral-800 hover:text-white"
|
||||
onClick={() => setIsMenuOpen((o) => !o)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
<Tooltip title={t('menu.expand')} placement="bottom">
|
||||
<div
|
||||
className="flex h-[30px] w-[50px] items-center justify-center rounded bg-neutral-800/50 text-white/50 hover:bg-neutral-800 hover:text-white"
|
||||
onClick={() => setIsMenuOpen((o) => !o)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { KeyboardIcon } from 'lucide-react';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { Paste } from './paste.tsx';
|
||||
import { VirtualKeyboard } from './virtual-keyboard.tsx';
|
||||
import { CtrlAltDel } from './ctrl-alt-del.tsx';
|
||||
|
||||
|
||||
export const Keyboard = () => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const tooltip = t('keyboard.title')
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Paste setIsPopoverOpen={setIsPopoverOpen} />
|
||||
@@ -24,11 +29,16 @@ export const Keyboard = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
onOpenChange={(visible) => {
|
||||
setIsPopoverOpen(visible);
|
||||
setTooltipValue(visible ? "" : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<KeyboardIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<KeyboardIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Divider, Popover } from 'antd';
|
||||
import { Divider, Popover, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import {
|
||||
@@ -27,6 +27,9 @@ export const Mouse = () => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const tooltip = t('mouse.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const mouseStyles = [
|
||||
{ name: t('mouse.default'), icon: <MousePointerIcon size={14} />, value: 'cursor-default' },
|
||||
{ name: t('mouse.grab'), icon: <HandIcon size={14} />, value: 'cursor-grab' },
|
||||
@@ -154,11 +157,16 @@ export const Mouse = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
onOpenChange={(visible) => {
|
||||
setIsPopoverOpen(visible);
|
||||
setTooltipValue(visible ? "" : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<MouseIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue}>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<MouseIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover, Slider } from 'antd';
|
||||
import { Popover, Slider, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { CirclePowerIcon, LoaderCircleIcon, PowerIcon, RotateCcwIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +13,9 @@ export const Power = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [powerDuration, setPowerDuration] = useState(8);
|
||||
|
||||
const tooltip = t('power.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
useEffect(() => {
|
||||
getLed();
|
||||
const interval = setInterval(getLed, 5000);
|
||||
@@ -95,19 +98,24 @@ export const Power = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
onOpenChange={(visible) => {
|
||||
setIsPopoverOpen(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700 hover:text-white">
|
||||
<div
|
||||
className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-500')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
) : (
|
||||
<PowerIcon size={18} />
|
||||
)}
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700 hover:text-white">
|
||||
<div
|
||||
className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-500')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
) : (
|
||||
<PowerIcon size={18} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { MonitorIcon } from 'lucide-react';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Quality } from './quality';
|
||||
import { Reset } from './reset.tsx';
|
||||
import { Resolution } from './resolution';
|
||||
import { VideoMode } from './video-mode.tsx';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const Screen = () => {
|
||||
const videoMode = useAtomValue(videoModeAtom);
|
||||
@@ -21,10 +22,12 @@ export const Screen = () => {
|
||||
const [fps, setFps] = useState(30);
|
||||
const [quality, setQuality] = useState(2);
|
||||
|
||||
const tooltip = t('screen.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
useEffect(() => {
|
||||
updateScreen('type', videoMode === 'mjpeg' ? 0 : 1);
|
||||
updateScreen('resolution', resolution!.height);
|
||||
|
||||
updateQuality();
|
||||
updateFps();
|
||||
}, []);
|
||||
@@ -69,10 +72,13 @@ export const Screen = () => {
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
onOpenChange={(visible) => setTooltipValue(visible ? '' : tooltip)}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<MonitorIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue}>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<MonitorIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Popconfirm, Popover } from 'antd';
|
||||
import { Button, Divider, Popconfirm, Popover, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { ChevronRightIcon, FileJsonIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -22,6 +22,9 @@ export const Script = () => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
const tooltip = t('script.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (open) {
|
||||
getScripts();
|
||||
@@ -194,11 +197,16 @@ export const Script = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
onOpenChange={(visible) => {
|
||||
handleOpenChange(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<FileJsonIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<FileJsonIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{isRunning && <Run script={currentScript} setIsRunning={setIsRunning} />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Badge, Modal } from 'antd';
|
||||
import { Badge, Modal, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
BadgeInfoIcon,
|
||||
@@ -32,6 +32,9 @@ export const Settings = () => {
|
||||
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
|
||||
|
||||
const tooltip = t('settings.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'about', icon: <BadgeInfoIcon size={16} />, component: <About /> },
|
||||
{ id: 'appearance', icon: <PaletteIcon size={16} />, component: <Appearance /> },
|
||||
@@ -92,16 +95,18 @@ export const Settings = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<Badge dot={isUpdateAvailable} color="blue" offset={[0, 2]}>
|
||||
<div className="pt-[3px]">
|
||||
<SettingsIcon size={18} />
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div
|
||||
className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<Badge dot={isUpdateAvailable} color="blue" offset={[0, 2]}>
|
||||
<div className="pt-[3px]">
|
||||
<SettingsIcon size={18} />
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
@@ -110,6 +115,7 @@ export const Settings = () => {
|
||||
destroyOnClose={true}
|
||||
styles={{ content: { padding: 0 } }}
|
||||
onCancel={closeModal}
|
||||
afterOpenChange={(visible) => setTooltipValue(visible ? '' : tooltip)}
|
||||
>
|
||||
<div className="flex min-h-[500px] rounded-lg outline outline-1 outline-neutral-700">
|
||||
<div className="flex flex-col space-y-0.5 rounded-l-lg bg-neutral-800 py-5 sm:w-1/5 sm:px-2">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Divider, Popover } from 'antd';
|
||||
import { Divider, Popover, Tooltip } from 'antd';
|
||||
import { SquareTerminalIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -10,6 +10,9 @@ export const Terminal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const tooltip = t('terminal.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const content = (
|
||||
<div className="min-w-[200px]">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
@@ -30,11 +33,16 @@ export const Terminal = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
onOpenChange={(visible) => {
|
||||
setIsPopoverOpen(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<SquareTerminalIcon size={18} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<SquareTerminalIcon size={18} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { Button, Divider, Input, List, Popover } from 'antd';
|
||||
import { Button, Divider, Input, List, Popover, Tooltip } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -22,6 +22,9 @@ export const Wol = () => {
|
||||
const [status, setStatus] = useState('');
|
||||
const [log, setLog] = useState('');
|
||||
|
||||
const tooltip = t('wol.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
|
||||
const [macList, setMacList] = useState<string[]>([]);
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
@@ -153,11 +156,16 @@ export const Wol = () => {
|
||||
trigger="click"
|
||||
arrow={false}
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
onOpenChange={(visible) => {
|
||||
handleOpenChange(visible);
|
||||
setTooltipValue(visible ? '' : tooltip);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<NetworkIcon size={16} />
|
||||
</div>
|
||||
<Tooltip title={tooltipValue} placement="bottom">
|
||||
<div className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700 hover:text-white">
|
||||
<NetworkIcon size={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user