This commit is contained in:
BuGu
2025-03-10 18:16:06 +08:00
41 changed files with 824 additions and 622 deletions

View File

@@ -21,7 +21,7 @@ var defaultConfig = &Config{
},
Stun: "stun.l.google.com:19302",
Turn: Turn{
TurnAddr: "turn.cloudflare.com:3478",
TurnAddr: "",
TurnUser: "",
TurnCred: "",
},

View File

@@ -83,3 +83,7 @@ type GetOLEDRsp struct {
type GetSSHStateRsp struct {
Enabled bool `json:"enabled"`
}
type GetMdnsStateRsp struct {
Enabled bool `json:"enabled"`
}

View File

@@ -39,4 +39,8 @@ func vmRouter(r *gin.Engine) {
api.GET("/vm/ssh", service.GetSSHState) // get SSH state
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS
}

View File

@@ -16,6 +16,8 @@ const (
TailscalePath = "/usr/bin/tailscale"
TailscaledPath = "/usr/sbin/tailscaled"
ConfigPath = "etc/sysctl.d/99-tailscale.conf"
GoMemLimit int64 = 75
)
var StateMap = map[string]proto.TailscaleState{
@@ -69,7 +71,7 @@ func (s *Service) Start(c *gin.Context) {
}
if !utils.IsGoMemLimitExist() {
_ = utils.SetGoMemLimit(50)
_ = utils.SetGoMemLimit(GoMemLimit)
}
rsp.OkRsp(c)
@@ -165,7 +167,7 @@ func (s *Service) Login(c *gin.Context) {
}
if !utils.IsGoMemLimitExist() {
_ = utils.SetGoMemLimit(50)
_ = utils.SetGoMemLimit(GoMemLimit)
}
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{

View File

@@ -33,7 +33,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
return
}
command := fmt.Sprintf("ether-wake %s", mac)
command := fmt.Sprintf("ether-wake -b %s", mac)
cmd := exec.Command("sh", "-c", command)
output, err := cmd.CombinedOutput()

View File

@@ -39,7 +39,7 @@ func Connect(c *gin.Context) {
var iceServers []webrtc.ICEServer
if conf.Stun != "" {
if conf.Stun != "" && conf.Stun != "disable" {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: []string{"stun:" + conf.Stun},
})
@@ -53,11 +53,9 @@ func Connect(c *gin.Context) {
})
}
rtc_config := webrtc.Configuration{
peerConn, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: iceServers,
}
peerConn, err := webrtc.NewPeerConnection(rtc_config)
})
if err != nil {
log.Errorf("failed to create PeerConnection: %s", err)
return

View File

@@ -18,7 +18,7 @@ var imageVersionMap = map[string]string{
"2024-07-23-20-18-587710.img": "v1.1.0",
"2024-08-08-19-44-bef2ca.img": "v1.2.0",
"2024-11-13-09-59-9c961a.img": "v1.3.0",
"2025-02-17-16-59-3649fe.img": "v1.4.0",
"2025-02-17-19-08-3649fe.img": "v1.4.0",
}
func (s *Service) GetInfo(c *gin.Context) {
@@ -53,6 +53,10 @@ func getIp() string {
}
func getMdns() string {
if pid := getAvahiDaemonPid(); pid == "" {
return ""
}
content, err := os.ReadFile("/etc/hostname")
if err != nil {
return ""

91
server/service/vm/mdns.go Normal file
View File

@@ -0,0 +1,91 @@
package vm
import (
"NanoKVM-Server/proto"
"fmt"
"os"
"os/exec"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
AvahiDaemonPid = "/run/avahi-daemon/pid"
AvahiDaemonScript = "/etc/init.d/S50avahi-daemon"
AvahiDaemonBackupScript = "/kvmapp/system/init.d/S50avahi-daemon"
)
func (s *Service) GetMdnsState(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
rsp.OkRspWithData(c, &proto.GetMdnsStateRsp{
Enabled: pid != "",
})
}
func (s *Service) EnableMdns(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
if pid != "" {
rsp.OkRsp(c)
return
}
commands := []string{
fmt.Sprintf("cp -f %s %s", AvahiDaemonBackupScript, AvahiDaemonScript),
fmt.Sprintf("%s start", AvahiDaemonScript),
}
command := strings.Join(commands, " && ")
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to start avahi-daemon: %s", err)
rsp.ErrRsp(c, -1, "failed to enable mdns")
return
}
rsp.OkRsp(c)
log.Debugf("avahi-daemon started")
}
func (s *Service) DisableMdns(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
if pid == "" {
rsp.OkRsp(c)
return
}
command := fmt.Sprintf("kill -9 %s", pid)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to stop avahi-daemon: %s", err)
rsp.ErrRsp(c, -1, "failed to disable mdns")
return
}
_ = os.Remove(AvahiDaemonPid)
rsp.OkRsp(c)
log.Debugf("avahi-daemon stopped")
}
func getAvahiDaemonPid() string {
if _, err := os.Stat(AvahiDaemonPid); err != nil {
return ""
}
content, err := os.ReadFile(AvahiDaemonPid)
if err != nil {
log.Errorf("failed to read mdns pid: %s", err)
return ""
}
return strings.ReplaceAll(string(content), "\n", "")
}

View File

@@ -52,7 +52,7 @@ int get_ip_addr(ip_addr_t ip_type)
switch (ip_type){
case ETH_IP: // eth_addr
if(strcmp(ip_address()["eth0"].c_str(), (char*)kvm_sys_state.eth_addr) != 0){
if(*(ip_address()["eth0"].c_str()) == NULL){
if(*(ip_address()["eth0"].c_str()) == 0){
printf("can`t get ip addr\r\n");
kvm_sys_state.eth_addr[0] = 0;
return 0;
@@ -67,7 +67,7 @@ int get_ip_addr(ip_addr_t ip_type)
return 1;
case WiFi_IP: // wifi_addr
if(strcmp(ip_address()["wlan0"].c_str(), (char*)kvm_sys_state.wifi_addr) != 0){
if(*(ip_address()["wlan0"].c_str()) == NULL){
if(*(ip_address()["wlan0"].c_str()) == 0){
printf("can`t get ip addr\r\n");
kvm_sys_state.wifi_addr[0] = 0;
return 0;
@@ -81,7 +81,7 @@ int get_ip_addr(ip_addr_t ip_type)
}
return 1;
case Tailscale_IP: // tail_addr
if(*(ip_address()["tailscale0"].c_str()) == NULL){
if(*(ip_address()["tailscale0"].c_str()) == 0){
printf("can`t get ip addr\r\n");
kvm_sys_state.tail_addr[0] = 0;
return 0;
@@ -94,7 +94,7 @@ int get_ip_addr(ip_addr_t ip_type)
printf("\r\n");
return 1;
case RNDIS_IP: // rndis_addr
if(*(ip_address()["usb0"].c_str()) == NULL){
if(*(ip_address()["usb0"].c_str()) == 0){
printf("can`t get ip addr\r\n");
kvm_sys_state.rndis_addr[0] = 0;
return 0;
@@ -466,6 +466,8 @@ void kvm_update_tailscale_state(void)
uint8_t ion_free_space(void)
{
//cat /sys/kernel/debug/ion/cvi_carveout_heap_dump/summary | grep "usage rate:" | awk '{print $2}'
return 0;
}
uint8_t watchdog_sf_is_open()

View File

@@ -25,6 +25,7 @@
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.0",
"react-error-boundary": "^4.1.2",
"react-helmet-async": "^2.0.5",
"react-i18next": "^14.1.3",
@@ -58,7 +59,7 @@
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite": "^5.4.14",
"vite": "^6.2.1",
"vite-tsconfig-paths": "^4.3.2"
},
"msw": {
@@ -66,4 +67,4 @@
"public"
]
}
}
}

493
web/pnpm-lock.yaml generated
View File

@@ -50,6 +50,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-draggable:
specifier: ^4.4.0
version: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-error-boundary:
specifier: ^4.1.2
version: 4.1.2(react@18.3.1)
@@ -107,7 +110,7 @@ importers:
version: 8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.6.3)
'@vitejs/plugin-react':
specifier: ^4.3.3
version: 4.3.3(vite@5.4.14(@types/node@22.9.0))
version: 4.3.3(vite@6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0))
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47)
@@ -145,11 +148,11 @@ importers:
specifier: ^5.6.3
version: 5.6.3
vite:
specifier: ^5.4.14
version: 5.4.14(@types/node@22.9.0)
specifier: ^6.2.1
version: 6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0)
vite-tsconfig-paths:
specifier: ^4.3.2
version: 4.3.2(typescript@5.6.3)(vite@5.4.14(@types/node@22.9.0))
version: 4.3.2(typescript@5.6.3)(vite@6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0))
packages:
@@ -297,141 +300,153 @@ packages:
'@emotion/unitless@0.7.5':
resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
'@esbuild/aix-ppc64@0.25.0':
resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
'@esbuild/android-arm64@0.25.0':
resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
'@esbuild/android-arm@0.25.0':
resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
'@esbuild/android-x64@0.25.0':
resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
'@esbuild/darwin-arm64@0.25.0':
resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
'@esbuild/darwin-x64@0.25.0':
resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
'@esbuild/freebsd-arm64@0.25.0':
resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
'@esbuild/freebsd-x64@0.25.0':
resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
'@esbuild/linux-arm64@0.25.0':
resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
'@esbuild/linux-arm@0.25.0':
resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
'@esbuild/linux-ia32@0.25.0':
resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
'@esbuild/linux-loong64@0.25.0':
resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
'@esbuild/linux-mips64el@0.25.0':
resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
'@esbuild/linux-ppc64@0.25.0':
resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
'@esbuild/linux-riscv64@0.25.0':
resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
'@esbuild/linux-s390x@0.25.0':
resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
'@esbuild/linux-x64@0.25.0':
resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
'@esbuild/netbsd-arm64@0.25.0':
resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.0':
resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
'@esbuild/openbsd-arm64@0.25.0':
resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.0':
resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
'@esbuild/sunos-x64@0.25.0':
resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
'@esbuild/win32-arm64@0.25.0':
resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
'@esbuild/win32-ia32@0.25.0':
resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
'@esbuild/win32-x64@0.25.0':
resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -790,98 +805,98 @@ packages:
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
engines: {node: '>=14.0.0'}
'@rollup/rollup-android-arm-eabi@4.34.4':
resolution: {integrity: sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==}
'@rollup/rollup-android-arm-eabi@4.34.9':
resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.34.4':
resolution: {integrity: sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==}
'@rollup/rollup-android-arm64@4.34.9':
resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.34.4':
resolution: {integrity: sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==}
'@rollup/rollup-darwin-arm64@4.34.9':
resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.34.4':
resolution: {integrity: sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==}
'@rollup/rollup-darwin-x64@4.34.9':
resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.34.4':
resolution: {integrity: sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==}
'@rollup/rollup-freebsd-arm64@4.34.9':
resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.34.4':
resolution: {integrity: sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==}
'@rollup/rollup-freebsd-x64@4.34.9':
resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.34.4':
resolution: {integrity: sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==}
'@rollup/rollup-linux-arm-gnueabihf@4.34.9':
resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.34.4':
resolution: {integrity: sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==}
'@rollup/rollup-linux-arm-musleabihf@4.34.9':
resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.34.4':
resolution: {integrity: sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==}
'@rollup/rollup-linux-arm64-gnu@4.34.9':
resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.34.4':
resolution: {integrity: sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==}
'@rollup/rollup-linux-arm64-musl@4.34.9':
resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.34.4':
resolution: {integrity: sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==}
'@rollup/rollup-linux-loongarch64-gnu@4.34.9':
resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.34.4':
resolution: {integrity: sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==}
'@rollup/rollup-linux-powerpc64le-gnu@4.34.9':
resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.34.4':
resolution: {integrity: sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==}
'@rollup/rollup-linux-riscv64-gnu@4.34.9':
resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.34.4':
resolution: {integrity: sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==}
'@rollup/rollup-linux-s390x-gnu@4.34.9':
resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.34.4':
resolution: {integrity: sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==}
'@rollup/rollup-linux-x64-gnu@4.34.9':
resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.34.4':
resolution: {integrity: sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==}
'@rollup/rollup-linux-x64-musl@4.34.9':
resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.34.4':
resolution: {integrity: sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==}
'@rollup/rollup-win32-arm64-msvc@4.34.9':
resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.34.4':
resolution: {integrity: sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==}
'@rollup/rollup-win32-ia32-msvc@4.34.9':
resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.34.4':
resolution: {integrity: sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==}
'@rollup/rollup-win32-x64-msvc@4.34.9':
resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==}
cpu: [x64]
os: [win32]
@@ -1166,6 +1181,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1337,9 +1356,9 @@ packages:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
esbuild@0.25.0:
resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
@@ -2040,6 +2059,10 @@ packages:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2356,6 +2379,12 @@ packages:
peerDependencies:
react: ^18.3.1
react-draggable@4.4.6:
resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==}
peerDependencies:
react: '>= 16.3.0'
react-dom: '>= 16.3.0'
react-error-boundary@4.1.2:
resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==}
peerDependencies:
@@ -2495,8 +2524,8 @@ packages:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup@4.34.4:
resolution: {integrity: sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==}
rollup@4.34.9:
resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -2775,22 +2804,27 @@ packages:
vite:
optional: true
vite@5.4.14:
resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==}
engines: {node: ^18.0.0 || >=20.0.0}
vite@6.2.1:
resolution: {integrity: sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
@@ -2805,6 +2839,10 @@ packages:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
@@ -3073,73 +3111,79 @@ snapshots:
'@emotion/unitless@0.7.5': {}
'@esbuild/aix-ppc64@0.21.5':
'@esbuild/aix-ppc64@0.25.0':
optional: true
'@esbuild/android-arm64@0.21.5':
'@esbuild/android-arm64@0.25.0':
optional: true
'@esbuild/android-arm@0.21.5':
'@esbuild/android-arm@0.25.0':
optional: true
'@esbuild/android-x64@0.21.5':
'@esbuild/android-x64@0.25.0':
optional: true
'@esbuild/darwin-arm64@0.21.5':
'@esbuild/darwin-arm64@0.25.0':
optional: true
'@esbuild/darwin-x64@0.21.5':
'@esbuild/darwin-x64@0.25.0':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
'@esbuild/freebsd-arm64@0.25.0':
optional: true
'@esbuild/freebsd-x64@0.21.5':
'@esbuild/freebsd-x64@0.25.0':
optional: true
'@esbuild/linux-arm64@0.21.5':
'@esbuild/linux-arm64@0.25.0':
optional: true
'@esbuild/linux-arm@0.21.5':
'@esbuild/linux-arm@0.25.0':
optional: true
'@esbuild/linux-ia32@0.21.5':
'@esbuild/linux-ia32@0.25.0':
optional: true
'@esbuild/linux-loong64@0.21.5':
'@esbuild/linux-loong64@0.25.0':
optional: true
'@esbuild/linux-mips64el@0.21.5':
'@esbuild/linux-mips64el@0.25.0':
optional: true
'@esbuild/linux-ppc64@0.21.5':
'@esbuild/linux-ppc64@0.25.0':
optional: true
'@esbuild/linux-riscv64@0.21.5':
'@esbuild/linux-riscv64@0.25.0':
optional: true
'@esbuild/linux-s390x@0.21.5':
'@esbuild/linux-s390x@0.25.0':
optional: true
'@esbuild/linux-x64@0.21.5':
'@esbuild/linux-x64@0.25.0':
optional: true
'@esbuild/netbsd-x64@0.21.5':
'@esbuild/netbsd-arm64@0.25.0':
optional: true
'@esbuild/openbsd-x64@0.21.5':
'@esbuild/netbsd-x64@0.25.0':
optional: true
'@esbuild/sunos-x64@0.21.5':
'@esbuild/openbsd-arm64@0.25.0':
optional: true
'@esbuild/win32-arm64@0.21.5':
'@esbuild/openbsd-x64@0.25.0':
optional: true
'@esbuild/win32-ia32@0.21.5':
'@esbuild/sunos-x64@0.25.0':
optional: true
'@esbuild/win32-x64@0.21.5':
'@esbuild/win32-arm64@0.25.0':
optional: true
'@esbuild/win32-ia32@0.25.0':
optional: true
'@esbuild/win32-x64@0.25.0':
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(jiti@1.21.6))':
@@ -3499,61 +3543,61 @@ snapshots:
'@remix-run/router@1.20.0': {}
'@rollup/rollup-android-arm-eabi@4.34.4':
'@rollup/rollup-android-arm-eabi@4.34.9':
optional: true
'@rollup/rollup-android-arm64@4.34.4':
'@rollup/rollup-android-arm64@4.34.9':
optional: true
'@rollup/rollup-darwin-arm64@4.34.4':
'@rollup/rollup-darwin-arm64@4.34.9':
optional: true
'@rollup/rollup-darwin-x64@4.34.4':
'@rollup/rollup-darwin-x64@4.34.9':
optional: true
'@rollup/rollup-freebsd-arm64@4.34.4':
'@rollup/rollup-freebsd-arm64@4.34.9':
optional: true
'@rollup/rollup-freebsd-x64@4.34.4':
'@rollup/rollup-freebsd-x64@4.34.9':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.34.4':
'@rollup/rollup-linux-arm-gnueabihf@4.34.9':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.34.4':
'@rollup/rollup-linux-arm-musleabihf@4.34.9':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.34.4':
'@rollup/rollup-linux-arm64-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-arm64-musl@4.34.4':
'@rollup/rollup-linux-arm64-musl@4.34.9':
optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.34.4':
'@rollup/rollup-linux-loongarch64-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.34.4':
'@rollup/rollup-linux-powerpc64le-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.34.4':
'@rollup/rollup-linux-riscv64-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.34.4':
'@rollup/rollup-linux-s390x-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-x64-gnu@4.34.4':
'@rollup/rollup-linux-x64-gnu@4.34.9':
optional: true
'@rollup/rollup-linux-x64-musl@4.34.4':
'@rollup/rollup-linux-x64-musl@4.34.9':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.34.4':
'@rollup/rollup-win32-arm64-msvc@4.34.9':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.34.4':
'@rollup/rollup-win32-ia32-msvc@4.34.9':
optional: true
'@rollup/rollup-win32-x64-msvc@4.34.4':
'@rollup/rollup-win32-x64-msvc@4.34.9':
optional: true
'@types/babel__core@7.20.5':
@@ -3689,14 +3733,14 @@ snapshots:
'@typescript-eslint/types': 8.19.1
eslint-visitor-keys: 4.2.0
'@vitejs/plugin-react@4.3.3(vite@5.4.14(@types/node@22.9.0))':
'@vitejs/plugin-react@4.3.3(vite@6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0))':
dependencies:
'@babel/core': 7.26.0
'@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0)
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
vite: 5.4.14(@types/node@22.9.0)
vite: 6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0)
transitivePeerDependencies:
- supports-color
@@ -3961,6 +4005,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clsx@1.2.1: {}
clsx@2.1.1: {}
color-convert@2.0.1:
@@ -4179,31 +4225,33 @@ snapshots:
d: 1.0.2
ext: 1.7.0
esbuild@0.21.5:
esbuild@0.25.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
'@esbuild/android-arm': 0.21.5
'@esbuild/android-arm64': 0.21.5
'@esbuild/android-x64': 0.21.5
'@esbuild/darwin-arm64': 0.21.5
'@esbuild/darwin-x64': 0.21.5
'@esbuild/freebsd-arm64': 0.21.5
'@esbuild/freebsd-x64': 0.21.5
'@esbuild/linux-arm': 0.21.5
'@esbuild/linux-arm64': 0.21.5
'@esbuild/linux-ia32': 0.21.5
'@esbuild/linux-loong64': 0.21.5
'@esbuild/linux-mips64el': 0.21.5
'@esbuild/linux-ppc64': 0.21.5
'@esbuild/linux-riscv64': 0.21.5
'@esbuild/linux-s390x': 0.21.5
'@esbuild/linux-x64': 0.21.5
'@esbuild/netbsd-x64': 0.21.5
'@esbuild/openbsd-x64': 0.21.5
'@esbuild/sunos-x64': 0.21.5
'@esbuild/win32-arm64': 0.21.5
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
'@esbuild/aix-ppc64': 0.25.0
'@esbuild/android-arm': 0.25.0
'@esbuild/android-arm64': 0.25.0
'@esbuild/android-x64': 0.25.0
'@esbuild/darwin-arm64': 0.25.0
'@esbuild/darwin-x64': 0.25.0
'@esbuild/freebsd-arm64': 0.25.0
'@esbuild/freebsd-x64': 0.25.0
'@esbuild/linux-arm': 0.25.0
'@esbuild/linux-arm64': 0.25.0
'@esbuild/linux-ia32': 0.25.0
'@esbuild/linux-loong64': 0.25.0
'@esbuild/linux-mips64el': 0.25.0
'@esbuild/linux-ppc64': 0.25.0
'@esbuild/linux-riscv64': 0.25.0
'@esbuild/linux-s390x': 0.25.0
'@esbuild/linux-x64': 0.25.0
'@esbuild/netbsd-arm64': 0.25.0
'@esbuild/netbsd-x64': 0.25.0
'@esbuild/openbsd-arm64': 0.25.0
'@esbuild/openbsd-x64': 0.25.0
'@esbuild/sunos-x64': 0.25.0
'@esbuild/win32-arm64': 0.25.0
'@esbuild/win32-ia32': 0.25.0
'@esbuild/win32-x64': 0.25.0
escalade@3.2.0: {}
@@ -4891,6 +4939,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.3:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
prelude-ls@1.2.1: {}
prettier-plugin-tailwindcss@0.6.9(@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.4.2))(prettier@3.4.2):
@@ -5242,6 +5296,13 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-draggable@4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
clsx: 1.2.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-error-boundary@4.1.2(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
@@ -5377,29 +5438,29 @@ snapshots:
reusify@1.0.4: {}
rollup@4.34.4:
rollup@4.34.9:
dependencies:
'@types/estree': 1.0.6
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.34.4
'@rollup/rollup-android-arm64': 4.34.4
'@rollup/rollup-darwin-arm64': 4.34.4
'@rollup/rollup-darwin-x64': 4.34.4
'@rollup/rollup-freebsd-arm64': 4.34.4
'@rollup/rollup-freebsd-x64': 4.34.4
'@rollup/rollup-linux-arm-gnueabihf': 4.34.4
'@rollup/rollup-linux-arm-musleabihf': 4.34.4
'@rollup/rollup-linux-arm64-gnu': 4.34.4
'@rollup/rollup-linux-arm64-musl': 4.34.4
'@rollup/rollup-linux-loongarch64-gnu': 4.34.4
'@rollup/rollup-linux-powerpc64le-gnu': 4.34.4
'@rollup/rollup-linux-riscv64-gnu': 4.34.4
'@rollup/rollup-linux-s390x-gnu': 4.34.4
'@rollup/rollup-linux-x64-gnu': 4.34.4
'@rollup/rollup-linux-x64-musl': 4.34.4
'@rollup/rollup-win32-arm64-msvc': 4.34.4
'@rollup/rollup-win32-ia32-msvc': 4.34.4
'@rollup/rollup-win32-x64-msvc': 4.34.4
'@rollup/rollup-android-arm-eabi': 4.34.9
'@rollup/rollup-android-arm64': 4.34.9
'@rollup/rollup-darwin-arm64': 4.34.9
'@rollup/rollup-darwin-x64': 4.34.9
'@rollup/rollup-freebsd-arm64': 4.34.9
'@rollup/rollup-freebsd-x64': 4.34.9
'@rollup/rollup-linux-arm-gnueabihf': 4.34.9
'@rollup/rollup-linux-arm-musleabihf': 4.34.9
'@rollup/rollup-linux-arm64-gnu': 4.34.9
'@rollup/rollup-linux-arm64-musl': 4.34.9
'@rollup/rollup-linux-loongarch64-gnu': 4.34.9
'@rollup/rollup-linux-powerpc64le-gnu': 4.34.9
'@rollup/rollup-linux-riscv64-gnu': 4.34.9
'@rollup/rollup-linux-s390x-gnu': 4.34.9
'@rollup/rollup-linux-x64-gnu': 4.34.9
'@rollup/rollup-linux-x64-musl': 4.34.9
'@rollup/rollup-win32-arm64-msvc': 4.34.9
'@rollup/rollup-win32-ia32-msvc': 4.34.9
'@rollup/rollup-win32-x64-msvc': 4.34.9
fsevents: 2.3.3
run-parallel@1.2.0:
@@ -5719,25 +5780,27 @@ snapshots:
- '@types/react'
- '@types/react-dom'
vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.14(@types/node@22.9.0)):
vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0)):
dependencies:
debug: 4.3.7
globrex: 0.1.2
tsconfck: 3.1.4(typescript@5.6.3)
optionalDependencies:
vite: 5.4.14(@types/node@22.9.0)
vite: 6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@5.4.14(@types/node@22.9.0):
vite@6.2.1(@types/node@22.9.0)(jiti@1.21.6)(yaml@2.6.0):
dependencies:
esbuild: 0.21.5
postcss: 8.4.47
rollup: 4.34.4
esbuild: 0.25.0
postcss: 8.5.3
rollup: 4.34.9
optionalDependencies:
'@types/node': 22.9.0
fsevents: 2.3.3
jiti: 1.21.6
yaml: 2.6.0
void-elements@3.1.0: {}

View File

@@ -76,3 +76,18 @@ export function enableSSH() {
export function disableSSH() {
return http.post('/api/vm/ssh/disable');
}
// get mDNS state
export function getMdnsState() {
return http.get('/api/vm/mdns');
}
// enable mDNS
export function enableMdns() {
return http.post('/api/vm/mdns/enable');
}
// disable mDNS
export function disableMdns() {
return http.post('/api/vm/mdns/disable');
}

View File

@@ -1,15 +1,15 @@
import { Button } from 'antd';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
export const MainError = () => {
const { t } = useTranslation();
return (
<div
className="flex h-screen w-screen flex-col items-center justify-center space-y-5"
role="alert"
>
<h2 className="text-lg font-semibold text-red-500">
{t('error.title')}
</h2>
<h2 className="text-lg font-semibold text-red-500">{t('error.title')}</h2>
<Button type="primary" danger onClick={() => window.location.assign(window.location.origin)}>
{t('error.refresh')}
</Button>

View File

@@ -0,0 +1,72 @@
import { ReactNode, useState } from 'react';
import { Popover, Tooltip } from 'antd';
import { useMediaQuery } from 'react-responsive';
type MenuItemProps = {
title: string;
icon: ReactNode;
content: ReactNode;
className?: string;
fresh?: boolean;
onOpenChange?: (open: boolean) => void;
};
export const MenuItem = ({
title,
icon,
content,
className,
fresh,
onOpenChange
}: MenuItemProps) => {
const isBigScreen = useMediaQuery({ minWidth: 640 });
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
function togglePopover(open: boolean) {
setIsTooltipOpen(false);
setIsPopoverOpen(open);
if (onOpenChange) {
onOpenChange(open);
}
}
function toggleTooltip(open: boolean) {
if (isPopoverOpen) {
return;
}
setIsTooltipOpen(open);
}
return (
<Popover
content={content}
arrow={false}
trigger="click"
placement={isBigScreen ? 'bottomLeft' : 'bottom'}
open={isPopoverOpen}
onOpenChange={togglePopover}
fresh={!!fresh}
>
<Tooltip
title={title}
mouseEnterDelay={0.6}
placement="bottom"
open={isTooltipOpen}
onOpenChange={toggleTooltip}
>
<div
className={
className
? className
: 'flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/80 hover:text-white'
}
>
{icon}
</div>
</Tooltip>
</Popover>
);
};

View File

@@ -96,6 +96,7 @@ const en = {
mountFailed: 'Mount Failed',
mountDesc:
"In some systems, it's necessary to eject the virtual disk on the remote host before mounting the image.",
refresh: 'Refresh the image list',
tips: {
title: 'How to upload',
usb1: 'Connect the NanoKVM to your computer via USB.',
@@ -199,6 +200,10 @@ const en = {
description: 'Enable SSH remote access',
tip: 'Set a strong password before enabling (Account - Change Password)'
},
mdns: {
description: 'Enable mDNS discovery service',
tip: "Turning it off if it's not needed"
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
@@ -208,7 +213,7 @@ const en = {
title: 'Tailscale',
memory: {
title: 'Memory optimization',
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. It's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. It's recommended to set to 75MB if using Tailscale. A Tailscale restart is required for the change to take effect.",
disable: 'Disable'
},
restart: 'Restart Tailscale?',
@@ -260,15 +265,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',
toggle: 'Toggle Fullscreen'
},
menu: {
collapse: 'Collapse Menu',
expand: 'Expand Menu',
expand: 'Expand Menu'
}
}
};

View File

@@ -14,13 +14,11 @@ const fr = {
placeholderPassword2: 'Veuillez entrer votre mot de passe à nouveau',
noEmptyUsername: "Le nom d'utilisateur ne peut pas être vide",
noEmptyPassword: 'Le mot de passe ne peut pas être vide',
noAccount:
"Impossible de récupérer les informations de l'utilisateur, veuillez rafraîchir la page ou réinitialiser le mot de passe",
noAccount: "Impossible de récupérer les informations de l'utilisateur, veuillez rafraîchir la page ou réinitialiser le mot de passe",
invalidUser: "Nom d'utilisateur ou mot de passe invalide",
error: 'Erreur inattendue',
changePassword: 'Changer le mot de passe',
changePasswordDesc:
'Pour la sécurité de votre appareil, veuillez modifier le mot de passe de connexion Web.',
changePasswordDesc: 'Pour la sécurité de votre appareil, veuillez modifier le mot de passe de connexion Web.',
differentPassword: 'Les mots de passe ne correspondent pas',
illegalUsername: "Le nom d'utilisateur contient des caractères illégaux",
illegalPassword: 'Le mot de passe contient des caractères illégaux',
@@ -140,6 +138,12 @@ const fr = {
input: "Veuillez entrer l'adresse MAC",
ok: 'Ok'
},
download: {
title: 'Télécharger limage',
input: 'Veuillez entrer lURL dune image distante',
ok: 'Ok',
disabled: 'La partition /data est en lecture seule, impossible de télécharger limage'
},
power: {
title: 'Power',
reset: 'Réinitialiser',
@@ -244,6 +248,10 @@ const fr = {
password: 'Mot de passe',
updateBtn: 'Mettre à jour',
logoutBtn: 'Déconnexion'
},
error: {
title: "Une erreur est survenue",
refresh: 'Rafraîchir'
}
}
}

View File

@@ -14,8 +14,7 @@ const ko = {
placeholderPassword2: '비밀번호를 다시 입력하세요.',
noEmptyUsername: '사용자 이름은 비어있을 수 없습니다.',
noEmptyPassword: '비밀번호는 비어있을 수 없습니다.',
noAccount:
'사용자 정보를 불러오는 데 실패했습니다. 페이지를 새로고침하거나 비밀번호를 초기화하세요.',
noAccount: '사용자 정보를 불러오는 데 실패했습니다. 페이지를 새로고침하거나 비밀번호를 초기화하세요.',
invalidUser: '사용자 이름이나 비밀번호가 틀렸습니다.',
error: '알 수 없는 오류',
changePassword: '비밀번호 변경',
@@ -28,7 +27,8 @@ const ko = {
cancel: '취소',
loginButtonText: '로그인',
tips: {
reset1: '비밀번호를 재설정하려면 NanoKVM의 BOOT 버튼을 10초 동안 누르고 계세요.',
reset1:
'비밀번호를 재설정하려면 NanoKVM의 BOOT 버튼을 10초 동안 누르고 계세요.',
reset2: '자세한 절차는 이 문서를 참조하세요:',
reset3: '웹 기본 계정:',
reset4: 'SSH 기본 계정:',
@@ -89,9 +89,11 @@ const ko = {
title: '이미지',
loading: '불러오는 중...',
empty: '아무것도 없습니다.',
cdrom: 'CD-ROM 모드로 이미지 마운트',
mountFailed: '이미지 마운트 실패',
mountDesc:
'일부 시스템에서는 이미지를 마운트하기 전에 원격 호스트에서 가상 디스크를 제거해야 합니다.',
refresh: '이미지 목록 새로고침',
tips: {
title: '업로드 방법',
usb1: 'USB를 통해 NanoKVM을 컴퓨터에 연결하세요.',
@@ -99,7 +101,7 @@ const ko = {
usb3: '컴퓨터에서 가상 디스크를 열고 이미지 파일을 가상 디스크의 루트 디렉토리로 복사하세요.',
scp1: 'NanoKVM과 컴퓨터가 동일한 로컬 네트워크에 있는지 확인하세요.',
scp2: '컴퓨터에서 터미널을 열고 SCP 명령을 사용하여 이미지 파일을 NanoKVM의 /data 디렉터리에 업로드하세요.',
scp3: '예: scp [이미지 파일 경로] root@[NanoKVM IP 주소]:/data',
scp3: '예: scp [이미지 파일 경로] root@[NanoKVM IP 주소]:/data',
tfCard: 'TF 카드',
tf1: '이 방법은 Linux 시스템에서 지원됩니다',
tf2: 'NanoKVM에서 TF 카드를 가져옵니다(전체 버전의 경우 먼저 케이스를 분해하세요).',
@@ -116,7 +118,7 @@ const ko = {
runFailed: '실행 실패',
attention: '주의',
delDesc: '이 파일을 정말로 삭제합니까?',
confirm: '',
confirm: '',
cancel: '아니오',
delete: '삭제',
close: '닫기'
@@ -222,7 +224,8 @@ const ko = {
upTailscale: 'tailscale을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/bin/',
upTailscaled: 'tailscaled을 NanoKVM 의 다음 경로에 업로드 했습니다. : /usr/sbin/',
refresh: '현재 페이지 새로고침',
notLogin: '이 기기는 현재 연동 되지 않았습니다. 로그인해서 계정에 이 장치를 연동하세요.',
notLogin:
'이 기기는 현재 연동 되지 않았습니다. 로그인해서 계정에 이 장치를 연동하세요.',
urlPeriod: '이 주소는 10분간 유효합니다.',
login: '로그인',
loginSuccess: '로그인 성공',
@@ -233,7 +236,7 @@ const ko = {
logout: '로그아웃',
logout2: '정말로 로그아웃 합니까?',
uninstall: 'Tailscale 제거',
okBtn: '',
okBtn: '',
cancelBtn: '아니오'
},
update: {
@@ -253,6 +256,10 @@ const ko = {
updateBtn: '업데이트',
logoutBtn: '로그아웃'
}
},
error: {
title: '문제가 발생했습니다.',
refresh: '새로고침'
}
}
};

View File

@@ -90,6 +90,7 @@ const ru = {
title: 'Образы',
loading: 'Загрузка...',
empty: 'Пусто',
cdrom: 'Смонтировать образ как CD-ROM диск',
mountFailed: 'Монтирование образа не удалось',
mountDesc:
'В некоторых системах необходимо отсоединить виртуальный диск на удаленном хосте перед монтированием образа.',
@@ -254,6 +255,10 @@ const ru = {
updateBtn: 'Обновить',
logoutBtn: 'Выйти'
}
},
error: {
title: "У нас возникла проблема",
refresh: 'Обновить страницу'
}
}
};

View File

@@ -193,6 +193,10 @@ const zh = {
description: '启用 SSH 远程访问',
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
},
mdns: {
description: '启用 mDNS 发现服务',
tip: '如果您未使用此功能,建议将其关闭'
},
disk: '虚拟U盘',
diskDesc: '在远程主机中挂载虚拟U盘',
network: '虚拟网卡',

View File

@@ -46,12 +46,12 @@ const zh_tw = {
finishBtn: '完成'
},
screen: {
video: '影片模式',
video: '編碼格式',
resolution: '解析度',
auto: '自動',
autoTips:
'在特定解析度下可能會出現畫面撕裂或滑鼠偏移的情況。考慮調整遠端主機的解析度或停用自動模式。',
fps: '影格速率',
fps: '更新頻率',
customizeFps: '自定義',
quality: '品質',
qualityLossless: '無損',
@@ -87,6 +87,7 @@ const zh_tw = {
title: '映像',
loading: '載入中...',
empty: '未找到任何內容',
cdrom: '以CD-ROM模式掛載',
mountFailed: '掛載失敗',
mountDesc: '在某些系統中,在掛載映像之前需要中斷遠端主機上的虛擬磁碟。',
tips: {
@@ -250,6 +251,17 @@ const zh_tw = {
updateBtn: '修改',
logoutBtn: '登出'
}
},
error: {
title: '我們遇到了一些問題',
refresh: '重新整理',
},
fullscreen: {
toggle: '進入全螢幕模式',
},
menu: {
collapse: '收起選單',
expand: '展開選單',
}
}
};

View File

@@ -1,5 +1,5 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { Button, Divider, Input, Popover, Tooltip } from 'antd';
import { Button, Divider, Input } from 'antd';
import type { InputRef } from 'antd';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
@@ -8,22 +8,18 @@ import { useTranslation } from 'react-i18next';
import { downloadImage, imageEnabled, statusImage } from '@/api/download.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { MenuItem } from '@/components/menu-item.tsx';
export const DownloadImage = () => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [input, setInput] = useState('');
const [status, setStatus] = useState('');
const [log, setLog] = useState('');
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);
@@ -41,6 +37,7 @@ export const DownloadImage = () => {
setDiskEnabled(false);
});
}
function handleOpenChange(open: boolean) {
if (open) {
clearInterval(intervalId.current);
@@ -60,8 +57,6 @@ export const DownloadImage = () => {
clearInterval(intervalId.current);
intervalId.current = undefined;
}
setIsPopoverOpen(open);
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
@@ -116,7 +111,7 @@ export const DownloadImage = () => {
}
const content = (
<div className="min-w-[300px]">
<div key={popoverKey} className="min-w-[300px]">
<div className="flex items-center justify-between px-1">
<span className="text-base font-bold text-neutral-300">{t('download.title')}</span>
</div>
@@ -161,23 +156,11 @@ export const DownloadImage = () => {
);
return (
<Popover
key={popoverKey}
<MenuItem
title={t('download.title')}
icon={<DownloadIcon size={18} />}
content={content}
placement="bottomLeft"
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
handleOpenChange(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
onOpenChange={handleOpenChange}
/>
);
};

View File

@@ -1,12 +1,11 @@
import { useEffect, useState } from 'react';
import { Tooltip } from 'antd';
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() {
@@ -24,16 +23,16 @@ export const Fullscreen = () => {
function handleFullscreen() {
if (!document.fullscreenElement) {
const element = document.documentElement;
element.requestFullscreen().then();
element.requestFullscreen();
} else {
document.exitFullscreen().then();
document.exitFullscreen();
}
}
return (
<Tooltip title={tooltipValue} placement="bottom">
<Tooltip title={t('fullscreen.toggle')} placement="bottom" mouseEnterDelay={0.6}>
<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"
className="hidden h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/80 hover:text-white sm:flex"
onClick={handleFullscreen}
>
{isFullscreen ? <MinimizeIcon size={18} /> : <MaximizeIcon size={18} />}

View File

@@ -14,11 +14,12 @@ import * as api from '@/api/storage.ts';
import { client } from '@/lib/websocket.ts';
type ImagesProps = {
isOpen: boolean;
cdrom: boolean;
setIsMounted: (isMounted: boolean) => void;
};
export const Images = ({ cdrom, setIsMounted }: ImagesProps) => {
export const Images = ({ isOpen, cdrom, setIsMounted }: ImagesProps) => {
const { t } = useTranslation();
const [notify, contextHolder] = notification.useNotification();
@@ -28,8 +29,10 @@ export const Images = ({ cdrom, setIsMounted }: ImagesProps) => {
const [mountedImage, setMountedImage] = useState('');
useEffect(() => {
getImages();
}, []);
if (isOpen) {
getImages();
}
}, [isOpen]);
function getImages() {
if (isLoading) return;

View File

@@ -1,35 +1,33 @@
import { useEffect, useState } from 'react';
import { Divider, Popover, Switch, Tooltip } from 'antd';
import { Divider, Switch, Tooltip } from 'antd';
import clsx from 'clsx';
import { DiscIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { getCdRom, getMountedImage } from '@/api/storage.ts';
import { MenuItem } from '@/components/menu-item.tsx';
import { Images } from './images.tsx';
import { Tips } from './tips.tsx';
export const Image = () => {
const { t } = useTranslation();
const isBigScreen = useMediaQuery({ minWidth: 640 });
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
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;
setIsMounted(!!rsp.data?.file);
if (rsp.code === 0) {
setIsMounted(!!rsp.data?.file);
}
});
getCdRom().then((rsp) => {
if (rsp.code !== 0) return;
setCdrom(rsp.data?.cdrom === 1);
if (rsp.code === 0) {
setCdrom(rsp.data?.cdrom === 1);
}
});
}, []);
@@ -41,43 +39,38 @@ export const Image = () => {
<Tips />
</div>
<Tooltip title={t('image.cdrom')} placement="bottom">
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-400">CD-ROM</span>
<div className="flex items-center space-x-2">
<Tooltip title={t('image.cdrom')} placement="bottom">
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-400">CD-ROM</span>
<Switch size="small" checked={cdrom} onChange={(checked) => setCdrom(checked)}></Switch>
</div>
</Tooltip>
<Switch
size="small"
checked={cdrom}
onChange={(checked) => setCdrom(checked)}
></Switch>
</div>
</Tooltip>
</div>
</div>
<Divider style={{ margin: '10px 0 15px 0' }} />
{isPopoverOpen && <Images cdrom={cdrom} setIsMounted={setIsMounted} />}
<Images isOpen={isPopoverOpen} cdrom={cdrom} setIsMounted={setIsMounted} />
</div>
);
return (
<Popover
<MenuItem
title={t('image.title')}
icon={<DiscIcon size={17} />}
content={content}
placement={isBigScreen ? 'bottomLeft' : 'bottom'}
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
setIsPopoverOpen(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
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'
)}
fresh={true}
onOpenChange={setIsPopoverOpen}
/>
);
};

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Divider, Tooltip } from 'antd';
import clsx from 'clsx';
import { useAtom } from 'jotai';
import { MenuIcon, XIcon } from 'lucide-react';
import Draggable from 'react-draggable';
import { useTranslation } from 'react-i18next';
import { getMenuDisabledItems } from '@/lib/localstorage.ts';
import { menuDisabledItemsAtom } from '@/jotai/settings.ts';
@@ -18,77 +20,83 @@ import { Script } from './script';
import { Settings } from './settings';
import { Terminal } from './terminal';
import { Wol } from './wol';
import { t } from 'i18next';
export const Menu = () => {
const { t } = useTranslation();
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
const [isMenuOpen, setIsMenuOpen] = useState(true);
const nodeRef = useRef<any>(null);
useEffect(() => {
const items = getMenuDisabledItems();
setMenuDisabledItems(items);
}, []);
return (
<div className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2">
<div className="sticky top-[10px]">
<div
className={clsx(
'h-[36px] items-center rounded bg-neutral-800/80',
isMenuOpen ? 'flex' : 'hidden'
)}
>
<div className="hidden h-[30px] select-none items-center px-3 sm:flex">
<img src="/sipeed.ico" width={18} height={18} alt="sipeed" />
<Draggable nodeRef={nodeRef} handle="strong" positionOffset={{ x: '-50%', y: '0%' }}>
<div ref={nodeRef} className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2">
<div className="sticky top-[10px] flex w-full justify-center">
<div
className={clsx(
'h-[36px] items-center rounded bg-neutral-800/80',
isMenuOpen ? 'flex' : 'hidden'
)}
>
<strong>
<div className="hidden h-[30px] cursor-move select-none items-center px-3 sm:flex">
<img src="/sipeed.ico" width={18} height={18} draggable={false} alt="sipeed" />
</div>
</strong>
<Screen />
<Keyboard />
<Mouse />
<Divider type="vertical" />
{!menuDisabledItems.includes('image') && <Image />}
{!menuDisabledItems.includes('download') && <DownloadImage />}
{!menuDisabledItems.includes('script') && <Script />}
{!menuDisabledItems.includes('terminal') && <Terminal />}
{!menuDisabledItems.includes('wol') && <Wol />}
{['image', 'script', 'terminal', 'wol', 'download'].some(
(key) => !menuDisabledItems.includes(key)
) && <Divider type="vertical" />}
{!menuDisabledItems.includes('power') && (
<>
<Power />
<Divider type="vertical" />
</>
)}
<Settings />
<Fullscreen />
<Tooltip title={t('menu.collapse')} placement="bottom" mouseEnterDelay={0.6}>
<div
className="mr-1 flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/80 hover:text-white"
onClick={() => setIsMenuOpen((o) => !o)}
>
<XIcon size={20} />
</div>
</Tooltip>
</div>
<Screen />
<Keyboard />
<Mouse />
<Divider type="vertical" />
{!menuDisabledItems.includes('image') && <Image />}
{!menuDisabledItems.includes('download') && <DownloadImage />}
{!menuDisabledItems.includes('script') && <Script />}
{!menuDisabledItems.includes('terminal') && <Terminal />}
{!menuDisabledItems.includes('wol') && <Wol />}
{['image', 'script', 'terminal', 'wol', 'download'].some(
(key) => !menuDisabledItems.includes(key)
) && <Divider type="vertical" />}
{!menuDisabledItems.includes('power') && (
<>
<Power />
<Divider type="vertical" />
</>
)}
<Settings />
<Fullscreen />
<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 && (
<Tooltip title={t('menu.expand')} placement="bottom">
<Tooltip title={t('menu.expand')} placement="bottom" mouseEnterDelay={0.6}>
<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"
className="flex h-[30px] w-[32px] cursor-pointer items-center justify-center rounded bg-neutral-800/50 text-white/50 hover:bg-neutral-700 hover:text-white"
onClick={() => setIsMenuOpen((o) => !o)}
>
<MenuIcon />
<MenuIcon size={20} />
</div>
</Tooltip>
)}
</div>
</div>
</Draggable>
);
};

View File

@@ -1,44 +1,26 @@
import { useState } from 'react';
import { Popover, Tooltip } from 'antd';
import { KeyboardIcon } from 'lucide-react';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
import { MenuItem } from '@/components/menu-item.tsx';
import { CtrlAltDel } from './ctrl-alt-del.tsx';
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} />
<VirtualKeyboard />
<CtrlAltDel />
</>
);
const { t } = useTranslation();
return (
<Popover
content={content}
placement="bottomLeft"
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
setIsPopoverOpen(visible);
setTooltipValue(visible ? "" : tooltip);
}}
>
<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>
<MenuItem
title={t('keyboard.title')}
icon={<KeyboardIcon size={18} />}
content={
<>
<Paste />
<VirtualKeyboard />
<CtrlAltDel />
</>
}
/>
);
};

View File

@@ -10,11 +10,7 @@ import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
const { TextArea } = Input;
type PasteProps = {
setIsPopoverOpen: (open: boolean) => void;
};
export const Paste = ({ setIsPopoverOpen }: PasteProps) => {
export const Paste = () => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
@@ -26,12 +22,6 @@ export const Paste = ({ setIsPopoverOpen }: PasteProps) => {
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');
@@ -82,7 +72,7 @@ export const Paste = ({ setIsPopoverOpen }: PasteProps) => {
className={clsx(
'flex cursor-pointer select-none items-center space-x-2 rounded py-1 pl-2 pr-5 hover:bg-neutral-700/70'
)}
onClick={openModal}
onClick={() => setIsModalOpen(true)}
>
<ClipboardIcon size={18} />
<span>{t('keyboard.paste')}</span>

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Divider, Popover, Tooltip } from 'antd';
import { Divider, Popover } from 'antd';
import clsx from 'clsx';
import { useAtom } from 'jotai';
import {
@@ -19,17 +19,14 @@ import * as api from '@/api/hid';
import * as ls from '@/lib/localstorage';
import { client } from '@/lib/websocket';
import { mouseModeAtom, mouseStyleAtom } from '@/jotai/mouse';
import { MenuItem } from '@/components/menu-item.tsx';
export const Mouse = () => {
const { t } = useTranslation();
const [mouseStyle, setMouseStyle] = useAtom(mouseStyleAtom);
const [mouseMode, setMouseMode] = useAtom(mouseModeAtom);
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' },
@@ -81,7 +78,6 @@ export const Mouse = () => {
api.reset().finally(() => {
client.connect();
setIsResetting(false);
setIsPopoverOpen(false);
});
}
@@ -150,23 +146,5 @@ export const Mouse = () => {
</>
);
return (
<Popover
content={content}
placement="bottomLeft"
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
setIsPopoverOpen(visible);
setTooltipValue(visible ? "" : tooltip);
}}
>
<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>
);
return <MenuItem title={t('mouse.title')} icon={<MouseIcon size={18} />} content={content} />;
};

View File

@@ -1,21 +1,18 @@
import { useEffect, useState } from 'react';
import { Popover, Slider, Tooltip } from 'antd';
import { Slider } from 'antd';
import clsx from 'clsx';
import { CirclePowerIcon, LoaderCircleIcon, PowerIcon, RotateCcwIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as api from '@/api/vm';
import { MenuItem } from '@/components/menu-item.tsx';
export const Power = () => {
const { t } = useTranslation();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isPowerOn, setIsPowerOn] = useState(false);
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);
@@ -40,8 +37,6 @@ export const Power = () => {
if (isLoading) return;
setIsLoading(true);
setIsPopoverOpen(false);
const millisecond = Math.floor((duration ? duration : powerDuration) * 1000);
api.setGpio(button, millisecond).finally(() => {
@@ -53,6 +48,16 @@ export const Power = () => {
setPowerDuration(value);
}
const icon = (
<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>
);
const content = (
<div className="flex flex-col space-y-1">
<div
@@ -91,31 +96,5 @@ export const Power = () => {
</div>
);
return (
<Popover
content={content}
placement="bottomLeft"
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
setIsPopoverOpen(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
</Tooltip>
</Popover>
);
return <MenuItem title={t('power.title')} icon={icon} content={content} />;
};

View File

@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { Popover, Tooltip } from 'antd';
import { useAtomValue } from 'jotai';
import { MonitorIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { updateScreen } from '@/api/vm';
import * as ls from '@/lib/localstorage';
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
import { MenuItem } from '@/components/menu-item.tsx';
import { BitRateMap, QualityMap } from './constants.ts';
import { Fps } from './fps';
@@ -14,17 +15,16 @@ 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 { t } = useTranslation();
const videoMode = useAtomValue(videoModeAtom);
const resolution = useAtomValue(resolutionAtom);
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);
@@ -57,28 +57,16 @@ export const Screen = () => {
});
}
return (
<Popover
content={
<div className="flex flex-col space-y-1">
<VideoMode />
<Resolution />
<Quality quality={quality} setQuality={setQuality} />
<Fps fps={fps} setFps={setFps} />
{videoMode === 'mjpeg' && <FrameDetect />}
<Reset />
</div>
}
placement="bottomLeft"
trigger="click"
arrow={false}
onOpenChange={(visible) => setTooltipValue(visible ? '' : tooltip)}
>
<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>
const content = (
<div className="flex flex-col space-y-1">
<VideoMode />
<Resolution />
<Quality quality={quality} setQuality={setQuality} />
<Fps fps={fps} setFps={setFps} />
{videoMode === 'mjpeg' && <FrameDetect />}
<Reset />
</div>
);
return <MenuItem title={t('screen.title')} icon={<MonitorIcon size={18} />} content={content} />;
};

View File

@@ -1,20 +1,18 @@
import { ChangeEvent, useRef, useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, Divider, Popconfirm, Popover, Tooltip } from 'antd';
import { Button, Divider, Popconfirm } from 'antd';
import clsx from 'clsx';
import { ChevronRightIcon, FileJsonIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import * as api from '@/api/script.ts';
import { MenuItem } from '@/components/menu-item.tsx';
import { Run } from './run';
export const Script = () => {
const { t } = useTranslation();
const isBigScreen = useMediaQuery({ minWidth: 640 });
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [scripts, setScripts] = useState<string[]>([]);
const [currentScript, setCurrentScript] = useState('');
const [isRunning, setIsRunning] = useState(false);
@@ -22,17 +20,12 @@ 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();
} else {
setCurrentScript('');
}
setIsPopoverOpen(open);
}
function selectFile() {
@@ -83,8 +76,6 @@ export const Script = () => {
}
});
}
setIsPopoverOpen(false);
}
function getScripts() {
@@ -191,23 +182,12 @@ export const Script = () => {
return (
<>
<Popover
<MenuItem
title={t('script.title')}
icon={<FileJsonIcon size={18} />}
content={content}
placement={isBigScreen ? 'bottomLeft' : 'bottom'}
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
handleOpenChange(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
onOpenChange={handleOpenChange}
/>
{isRunning && <Run script={currentScript} setIsRunning={setIsRunning} />}
</>

View File

@@ -39,10 +39,12 @@ export const Information = () => {
<span>{information ? information.ip : '-'}</span>
</div>
<div className="flex w-full items-center justify-between">
<span>{t('settings.about.mdns')}</span>
<span>{information ? information.mdns : '-'}</span>
</div>
{!!information?.mdns && (
<div className="flex w-full items-center justify-between">
<span>{t('settings.about.mdns')}</span>
<span>{information.mdns}</span>
</div>
)}
<div className="flex w-full items-center justify-between">
<div className="flex items-center space-x-2">

View File

@@ -1,6 +1,7 @@
import { Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { Mdns } from './mdns.tsx';
import { Oled } from './oled.tsx';
import { Ssh } from './ssh.tsx';
import { VirtualDevices } from './virtual-devices.tsx';
@@ -18,6 +19,7 @@ export const Device = () => {
<Oled />
<Wifi />
<Ssh />
<Mdns />
<VirtualDevices />
</div>
</>

View File

@@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';
import { Switch, Tooltip } from 'antd';
import { CircleAlertIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as api from '@/api/vm.ts';
export const Mdns = () => {
const { t } = useTranslation();
const [isEnabled, setIsEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
api
.getMdnsState()
.then((rsp) => {
if (rsp.data?.enabled) {
setIsEnabled(true);
}
})
.finally(() => {
setIsLoading(false);
});
}, []);
async function update() {
if (isLoading) return;
setIsLoading(true);
const rsp = isEnabled ? await api.disableMdns() : await api.enableMdns();
setIsLoading(false);
if (rsp.code !== 0) {
console.log(rsp.msg);
return;
}
setIsEnabled(!isEnabled);
}
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<span>mDNS</span>
<div className="flex items-center space-x-1">
<span className="text-xs text-neutral-500">{t('settings.device.mdns.description')}</span>
<Tooltip
title={t('settings.device.mdns.tip')}
className="cursor-pointer text-neutral-500"
placement="bottom"
>
<CircleAlertIcon size={16} />
</Tooltip>
</div>
</div>
<Switch checked={isEnabled} loading={isLoading} onChange={update} />
</div>
);
};

View File

@@ -32,9 +32,6 @@ 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 /> },
@@ -95,9 +92,9 @@ export const Settings = () => {
return (
<>
<Tooltip title={tooltipValue} placement="bottom">
<Tooltip title={t('settings.title')} placement="bottom" mouseEnterDelay={0.6}>
<div
className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700"
className="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/80"
onClick={() => setIsModalOpen(true)}
>
<Badge dot={isUpdateAvailable} color="blue" offset={[0, 2]}>
@@ -115,7 +112,6 @@ 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">

View File

@@ -115,16 +115,18 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
{status === 'failed' && <Result subTitle={errMsg} />}
<div className="flex justify-center">
<Button
type="link"
size="small"
href="https://github.com/sipeed/NanoKVM/blob/main/CHANGELOG.md"
target="_blank"
>
CHANGELOG
</Button>
</div>
{status !== 'loading' && (
<div className="flex justify-center">
<Button
type="link"
size="small"
href="https://github.com/sipeed/NanoKVM/blob/main/CHANGELOG.md"
target="_blank"
>
CHANGELOG
</Button>
</div>
)}
</>
);
};

View File

@@ -1,17 +1,14 @@
import { useState } from 'react';
import { Divider, Popover, Tooltip } from 'antd';
import { Divider } from 'antd';
import { SquareTerminalIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { MenuItem } from '@/components/menu-item.tsx';
import { Nanokvm } from './nanokvm';
import { SerialPort } from './serial-port';
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]">
@@ -21,28 +18,16 @@ export const Terminal = () => {
<Divider style={{ margin: '10px 0 15px 0' }} />
<Nanokvm setIsPopoverOpen={setIsPopoverOpen} />
<SerialPort setIsPopoverOpen={setIsPopoverOpen} />
<Nanokvm />
<SerialPort />
</div>
);
return (
<Popover
<MenuItem
title={t('terminal.title')}
icon={<SquareTerminalIcon size={18} />}
content={content}
placement="bottomLeft"
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
setIsPopoverOpen(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
/>
);
};

View File

@@ -1,15 +1,10 @@
import { SquareTerminalIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type NanokvmProps = {
setIsPopoverOpen: (open: boolean) => void;
};
export const Nanokvm = ({ setIsPopoverOpen }: NanokvmProps) => {
export const Nanokvm = () => {
const { t } = useTranslation();
function openTerminal() {
setIsPopoverOpen(false);
window.open('/#terminal', '_blank');
}

View File

@@ -6,11 +6,7 @@ import { useTranslation } from 'react-i18next';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
type SerialPortProps = {
setIsPopoverOpen: (open: boolean) => void;
};
export const SerialPort = ({ setIsPopoverOpen }: SerialPortProps) => {
export const SerialPort = () => {
const { t } = useTranslation();
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
@@ -22,7 +18,6 @@ export const SerialPort = ({ setIsPopoverOpen }: SerialPortProps) => {
setIsKeyboardEnable(false);
setIsModalOpen(true);
setIsPopoverOpen(false);
}
function closeModal() {

View File

@@ -1,30 +1,24 @@
import { ChangeEvent, useRef, useState } from 'react';
import { Button, Divider, Input, List, Popover, Tooltip } from 'antd';
import { Button, Divider, Input, List } from 'antd';
import type { InputRef } from 'antd';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { NetworkIcon, SendIcon, Trash2Icon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { deleteWolMac, getWolMacs, wol } from '@/api/network.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { MenuItem } from '@/components/menu-item.tsx';
export const Wol = () => {
const { t } = useTranslation();
const isBigScreen = useMediaQuery({ minWidth: 640 });
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [input, setInput] = useState('');
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);
@@ -41,8 +35,6 @@ export const Wol = () => {
setIsKeyboardEnable(true);
}
setIsPopoverOpen(open);
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
@@ -150,22 +142,11 @@ export const Wol = () => {
);
return (
<Popover
<MenuItem
title={t('wol.title')}
icon={<NetworkIcon size={16} />}
content={content}
placement={isBigScreen ? 'bottomLeft' : 'bottom'}
trigger="click"
arrow={false}
open={isPopoverOpen}
onOpenChange={(visible) => {
handleOpenChange(visible);
setTooltipValue(visible ? '' : tooltip);
}}
>
<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>
onOpenChange={handleOpenChange}
/>
);
};

View File

@@ -18,10 +18,13 @@ export const H264 = () => {
let heartbeatTimer: any;
const videoElement = document.getElementById('screen') as HTMLVideoElement;
const url = `${getBaseUrl('ws')}/api/stream/h264`;
const ws = new W3cWebSocket(url);
const pc = new RTCPeerConnection({
iceServers: [
{
urls: ['stun:stun.l.google.com:19302', 'stun:turn.cloudflare.com:3478']
urls: ['stun:stun.l.google.com:19302']
}
]
});
@@ -34,9 +37,6 @@ export const H264 = () => {
videoElement.srcObject = event.streams[0];
};
const url = `${getBaseUrl('ws')}/api/stream/h264`;
const ws = new W3cWebSocket(url);
ws.onopen = () => {
pc.onicecandidate = (event) => {
if (event.candidate) {