From 9a4e604c614c9de6c08e5d7e47a0bab509e9c134 Mon Sep 17 00:00:00 2001 From: luckfox-eng29 Date: Thu, 5 Feb 2026 11:28:14 +0800 Subject: [PATCH] Update App version to 0.1.1 Signed-off-by: luckfox-eng29 --- .github/dependabot.yml | 17 - .github/workflows/build.yml | 51 - .github/workflows/golangci-lint.yml | 37 - .github/workflows/smoketest.yml | 174 -- .github/workflows/ui-lint.yml | 34 - .golangci.yml | 43 - Makefile | 4 +- README.md | 2 +- README_CN.md | 2 +- config.go | 60 +- internal/usbgadget/udc.go | 10 + io.go | 53 + jsonrpc.go | 458 +-- main.go | 54 +- native.go | 8 + ota.go | 951 ++++++- ratelimit.go | 95 + serial.go | 80 + stream_broadcaster.go | 59 + terminal.go | 79 + ui/.gitignore | 1 + ui/eslint.config.cjs | 20 +- ui/index.html | 26 +- ui/package-lock.json | 2524 ++++++++++++++++- ui/package.json | 14 +- ui/public/sse.html | 0 ui/src/assets/second/ConnectStatsPressed.svg | 16 + ui/src/assets/second/ConnectStatsSelected.svg | 16 + ui/src/assets/second/DisabledPressed.svg | 17 + ui/src/assets/second/IMG.svg | 9 + ui/src/assets/second/KeyboardPressed.svg | 80 + ui/src/assets/second/KeyboardSelected.svg | 80 + ui/src/assets/second/MTPPressed.svg | 17 + ui/src/assets/second/MingLing.svg | 8 + ui/src/assets/second/MousePressed.svg | 16 + ui/src/assets/second/MouseSelected.svg | 16 + ui/src/assets/second/SharedFoldersPressed.svg | 18 + ui/src/assets/second/UAC.svg | 8 + ui/src/assets/second/UACPressed.svg | 17 + ui/src/assets/second/UACSelected.svg | 17 + ui/src/assets/second/VideoPressed.svg | 17 + ui/src/assets/second/VideoSelected.svg | 17 + .../assets/second/VirtualStoragePressed.svg | 18 + .../assets/second/VirtualStorageSelected.svg | 18 + ui/src/assets/second/access.svg | 6 + ui/src/assets/second/advanced.svg | 4 + ui/src/assets/second/copy.svg | 9 + ui/src/assets/second/delete.svg | 5 + ui/src/assets/second/down.svg | 5 + ui/src/assets/second/dwon.svg | 5 + ui/src/assets/second/dwon2.svg | 16 + ui/src/assets/second/float_button1.svg | 4 + ui/src/assets/second/float_button2.svg | 5 + ui/src/assets/second/general.svg | 5 + ui/src/assets/second/gobottom.svg | 5 + ui/src/assets/second/hardware.svg | 29 + ui/src/assets/second/hdmi-cord.svg | 13 + ui/src/assets/second/hdml.svg | 14 + ui/src/assets/second/hdml2.svg | 14 + ui/src/assets/second/keyboard.svg | 27 + ui/src/assets/second/keyboard2.svg | 27 + ui/src/assets/second/left.svg | 4 + ui/src/assets/second/media.svg | 9 + ui/src/assets/second/minglingdarkac.svg | 8 + ui/src/assets/second/mouse.svg | 7 + ui/src/assets/second/network.svg | 8 + ui/src/assets/second/noSD.svg | 7 + ui/src/assets/second/open.svg | 5 + ui/src/assets/second/refresh.svg | 6 + ui/src/assets/second/rname.py | 161 ++ ui/src/assets/second/set1.svg | 7 + ui/src/assets/second/set2.svg | 7 + ui/src/assets/second/shuaxin.svg | 6 + ui/src/assets/second/state.svg | 7 + ui/src/assets/second/swich_dir2.svg | 9 + ui/src/assets/second/swich_dri1.svg | 9 + ui/src/assets/second/tiaozhuan.svg | 5 + ui/src/assets/second/to_down.svg | 5 + ui/src/assets/second/to_up.svg | 5 + ui/src/assets/second/up.svg | 5 + ui/src/assets/second/upload.svg | 8 + ui/src/assets/second/usb.svg | 12 + ui/src/assets/second/usb2.svg | 12 + ui/src/assets/second/vedio.svg | 8 + ui/src/assets/second/vedio2.svg | 8 + ui/src/assets/second/version.svg | 7 + ui/src/assets/second/xinhao.svg | 9 + ui/src/assets/second/zhongduan.svg | 7 + ui/src/assets/second/zhongduan2.svg | 7 + ui/src/components/ActionBar.tsx | 331 --- ui/src/components/AdaptiveContainer.tsx | 79 + ui/src/components/AuthLayout.tsx | 66 - ui/src/components/Button.tsx | 59 +- ui/src/components/Card.tsx | 13 +- ui/src/components/CardHeader.tsx | 19 - ui/src/components/Checkbox.tsx | 80 - ui/src/components/ConfirmDialog.tsx | 76 +- ...onnectionStats.tsx => ConnectionStats.tsx} | 24 +- ui/src/components/Container.tsx | 3 +- ui/src/components/DhcpLeaseCard.tsx | 214 -- ui/src/components/Drawer.tsx | 299 ++ ui/src/components/EmptyCard.tsx | 8 +- ui/src/components/ExtLink.tsx | 58 +- ui/src/components/FeatureFlag.tsx | 28 - ui/src/components/FieldLabel.tsx | 4 +- .../components/FileManager/FileUploader.tsx | 469 +++ .../FileManager/Mount.tsx} | 84 +- .../{ => FileManager}/UploadDialog.tsx | 12 +- ui/src/components/GridBackground.tsx | 41 - ui/src/components/{ => Header}/Header.tsx | 44 +- .../{ => Header}/PeerConnectionStatusCard.tsx | 2 +- .../components/{ => Header}/StatusCards.tsx | 0 .../{ => Header}/USBStateStatus.tsx | 2 +- .../{ => Header}/VpnConnectionStatusCard.tsx | 27 +- ui/src/components/Icons.tsx | 328 --- ui/src/components/InfoBar.tsx | 184 -- ui/src/components/InputField.tsx | 4 +- ui/src/components/KvmCard.tsx | 2 +- ui/src/components/LoadingSpinner.tsx | 1 - ui/src/components/LogDialog.tsx | 109 +- ui/src/components/{ => Macro}/Combobox.tsx | 6 +- ui/src/components/{ => Macro}/MacroBar.tsx | 0 ui/src/components/{ => Macro}/MacroForm.tsx | 52 +- .../components/{ => Macro}/MacroStepCard.tsx | 46 +- ui/src/components/MousePanel.tsx | 211 ++ ui/src/components/Network/DhcpLeaseCard.tsx | 389 +++ .../{ => Network}/Ipv6NetworkCard.tsx | 16 +- ui/src/components/NotFoundPage.tsx | 2 +- ui/src/components/PinchZoom.tsx | 201 ++ ui/src/components/PopoverButton.tsx | 63 + ui/src/components/PreUploadedImageItem.tsx | 109 + ui/src/components/ScrollThrottlingSelect.tsx | 289 ++ ui/src/components/SelectMenuBasic.tsx | 5 +- .../{ => Settings}/SettingsPageheader.tsx | 0 .../{ => Settings}/SettingsSectionHeader.tsx | 0 ui/src/components/Settings/SettingsView.tsx | 73 + ui/src/components/Sidebar/SideTabs.tsx | 110 + ui/src/components/Sidebar/SidebarDrawer.tsx | 54 + .../{ => Sidebar}/SidebarHeader.tsx | 3 +- ui/src/components/Sidebar/SlideAnimation.tsx | 104 + ui/src/components/Sidebar/StatsTopbar.tsx | 46 + ui/src/components/SimpleNavbar.tsx | 2 +- ui/src/components/SmartButton.tsx | 68 + ui/src/components/StatChart.tsx | 169 +- ui/src/components/StatsSidebarHeader.tsx | 68 + ui/src/components/StepCounter.tsx | 4 +- ui/src/components/Tabs.tsx | 46 - ui/src/components/Terminal.tsx | 233 -- ui/src/components/TextArea.tsx | 10 +- .../components/UpdateInProgressStatusCard.tsx | 42 - ui/src/components/UsbDeviceSetting.tsx | 240 -- ui/src/components/UsbEpModeSetting.tsx | 16 +- ui/src/components/UsbInfoSetting.tsx | 303 -- ui/src/components/Video/VideoContainer.tsx | 25 + ui/src/components/Video/VideoElement.tsx | 49 + ui/src/components/VideoOverlay.tsx | 306 +- ui/src/components/VirtualKeyboard.tsx | 746 ++++- ui/src/components/VolumeControl.tsx | 97 +- .../WakeOnLan/AddDeviceForm.tsx | 36 +- ui/src/components/WakeOnLan/DeviceList.tsx | 87 + .../components/WakeOnLan/EmptyStateCard.tsx | 29 + .../{popovers => }/WakeOnLan/Index.tsx | 6 +- ui/src/components/WebRTCVideo.tsx | 1010 ------- ui/src/components/extensions/IOControl.tsx | 124 - ui/src/components/extensions/PowerControl.tsx | 173 -- .../components/extensions/SerialConsole.tsx | 133 - .../components/popovers/ExtensionPopover.tsx | 141 - ui/src/components/popovers/MountPopover.tsx | 445 --- ui/src/components/popovers/PasteModal.tsx | 382 --- .../popovers/WakeOnLan/DeviceList.tsx | 86 - .../popovers/WakeOnLan/EmptyStateCard.tsx | 58 - ui/src/hooks/stores.ts | 96 +- ui/src/hooks/useJsonRpc.ts | 103 +- ui/src/hooks/useKeyboard.ts | 10 +- ui/src/index.css | 62 +- ui/src/keyboardLayouts.ts | 4 +- ui/src/keyboardMappings.ts | 40 +- .../keyboard/KeyboardLayoutModal.tsx | 121 + .../keyboard/KeyboardPanel.tsx | 254 ++ .../terminal/TerminalKVM.tsx | 158 ++ .../terminal/TerminalSerial.tsx | 212 ++ .../terminal/TerminalSerialSide.tsx | 263 ++ .../components_bottom/terminal/common.ts | 38 + .../terminal/index.mobile.tsx | 67 + .../components_bottom/terminal/index.pc.tsx | 257 ++ .../components_bottom/terminal/useTerminal.ts | 122 + .../usbepmode/UsbEpModeSelect.mobile.tsx | 115 + .../usbepmode/UsbEpModeSelect.pc.tsx | 127 + .../usbepmode/UsbEpModeSelect.tsx | 142 + .../usbepmode/usbModeOptions.ts | 41 + .../SettingsModalMobile.tsx | 230 ++ .../components_setting/SettingsModalPC.tsx | 118 + .../access/AccessContent.tsx} | 407 ++- .../components_setting/access/auth.tsx} | 230 +- .../advanced/AdvancedContent.tsx} | 198 +- .../general/GeneralContent.tsx | 119 + .../hardware/HardwareContent.tsx} | 62 +- ui/src/layout/components_setting/index.tsx | 31 + .../network/NetworkContent.tsx} | 315 +- .../version/VersionContent.tsx | 754 +++++ .../components_side/Clipboard/Clipboard.tsx | 328 +++ .../components_side/Macros/MacroTopBar.tsx | 187 ++ .../Macros/SettingsMacrosAdd.tsx} | 137 +- .../Macros/SettingsMacrosEdit.tsx} | 285 +- .../Macros/SettingsMacrosList.tsx} | 728 ++--- .../layout/components_side/Macros/index.tsx | 44 + .../components_side/Power/PowerControlUp.tsx | 180 ++ .../components_side/Power/WakeOnLan.tsx | 130 + ui/src/layout/components_side/Power/index.tsx | 17 + .../SharedFolders/DeviceFilePage.tsx | 14 + .../SharedFolders/FileManager.tsx | 506 ++++ .../SharedFolders/SDFilePage.tsx | 50 + .../components_side/SharedFolders/index.tsx | 21 + .../components_side/Stats/StatsSidebar.tsx | 114 + .../Video/SettingsVideoSide.tsx | 423 +++ .../VirtualMediaSource/DevicePage.tsx | 16 + .../VirtualMediaSource/ImageManager.tsx | 506 ++++ .../VirtualMediaSource/SDPage.tsx | 23 + .../VirtualMediaSource/StorageSpaceBar.tsx | 36 + .../VirtualMediaSource/UnMount.tsx | 278 ++ .../VirtualMediaSource/ViewHeader.tsx | 14 + .../VirtualMediaSource/index.tsx | 32 + .../contexts/DeviceAwareComponentProps.tsx | 20 + ui/src/layout/contexts/ThemeContext.tsx | 167 ++ .../core/bar_bottom/BottomBarMobile.tsx | 333 +++ ui/src/layout/core/bar_bottom/BottomBarPC.tsx | 385 +++ ui/src/layout/core/bar_bottom/index.tsx | 12 + ui/src/layout/core/bar_side.tsx | 73 + ui/src/layout/core/bar_top/TopBarMobile.tsx | 172 ++ ui/src/layout/core/bar_top/TopBarPC.tsx | 183 ++ ui/src/layout/core/bar_top/index.tsx | 20 + ui/src/layout/core/desktop/DesktopMobile.tsx | 418 +++ ui/src/layout/core/desktop/DesktopPC.tsx | 159 ++ .../core/desktop/hooks/useFullscreen.ts | 73 + .../core/desktop/hooks/useKeyboardEvents.ts | 107 + .../core/desktop/hooks/useMouseEvents.ts | 259 ++ .../core/desktop/hooks/usePasteHandler.ts | 215 ++ .../core/desktop/hooks/usePointerLock.ts | 52 + .../layout/core/desktop/hooks/useTouchZoom.ts | 119 + .../core/desktop/hooks/useVideoEffects.ts | 21 + .../core/desktop/hooks/useVideoOverlays.ts | 62 + .../core/desktop/hooks/useVideoStream.ts | 284 ++ ui/src/layout/core/desktop/index.tsx | 13 + .../core/other-session.tsx} | 25 +- ui/src/layout/index.mobile.tsx | 884 ++++++ .../devices.$id.tsx => layout/index.pc.tsx} | 388 ++- ui/src/layout/index.tsx | 37 + ui/src/layout/theme_color.ts | 28 + ui/src/locales/en.json | 488 ++-- ui/src/locales/zh.json | 494 ++-- ui/src/main.tsx | 196 +- ui/src/notifications.tsx | 5 +- ui/src/routes/adopt.tsx | 43 - ui/src/routes/devices.$id.deregister.tsx | 97 - ui/src/routes/devices.$id.mtp.tsx | 1629 ----------- ui/src/routes/devices.$id.rename.tsx | 103 - ui/src/routes/devices.$id.settings._index.tsx | 7 - .../devices.$id.settings.appearance.tsx | 57 - .../devices.$id.settings.ctrlaltdel.tsx | 28 - .../devices.$id.settings.general._index.tsx | 101 - .../devices.$id.settings.general.update.tsx | 567 ---- .../routes/devices.$id.settings.keyboard.tsx | 126 - ui/src/routes/devices.$id.settings.mouse.tsx | 181 -- ui/src/routes/devices.$id.settings.tsx | 309 -- ui/src/routes/devices.$id.settings.video.tsx | 303 -- ui/src/routes/devices.$id.setup.tsx | 79 - ui/src/routes/devices.already-adopted.tsx | 43 - ui/src/routes/devices.tsx | 84 - ui/src/routes/login-local.tsx | 277 +- ui/src/routes/login.tsx | 32 - ui/src/routes/login_page/AuthMethodCard.tsx | 70 + ui/src/routes/login_page/SetupHeader.tsx | 127 + ui/src/routes/login_page/index.tsx | 168 ++ ui/src/routes/login_page/useLocalAuth.ts | 126 + ui/src/routes/password.tsx | 202 ++ ui/src/routes/signup.tsx | 31 - ui/src/routes/welcome-local.mode.tsx | 183 -- ui/src/routes/welcome-local.password.tsx | 186 -- ui/src/routes/welcome-local.tsx | 91 - ui/tailwind.config.js | 5 + ui/tsconfig.json | 2 +- ui/vite-env.d.ts | 7 + ui/vite.config.ts | 30 +- usb.go | 32 + video.go | 5 + vpn.go | 125 + web.go | 234 +- web_tls.go | 2 + webrtc.go | 7 +- 289 files changed, 23077 insertions(+), 12474 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/golangci-lint.yml delete mode 100644 .github/workflows/smoketest.yml delete mode 100644 .github/workflows/ui-lint.yml delete mode 100644 .golangci.yml create mode 100644 ratelimit.go create mode 100644 stream_broadcaster.go mode change 120000 => 100644 ui/public/sse.html create mode 100644 ui/src/assets/second/ConnectStatsPressed.svg create mode 100644 ui/src/assets/second/ConnectStatsSelected.svg create mode 100644 ui/src/assets/second/DisabledPressed.svg create mode 100644 ui/src/assets/second/IMG.svg create mode 100644 ui/src/assets/second/KeyboardPressed.svg create mode 100644 ui/src/assets/second/KeyboardSelected.svg create mode 100644 ui/src/assets/second/MTPPressed.svg create mode 100644 ui/src/assets/second/MingLing.svg create mode 100644 ui/src/assets/second/MousePressed.svg create mode 100644 ui/src/assets/second/MouseSelected.svg create mode 100644 ui/src/assets/second/SharedFoldersPressed.svg create mode 100644 ui/src/assets/second/UAC.svg create mode 100644 ui/src/assets/second/UACPressed.svg create mode 100644 ui/src/assets/second/UACSelected.svg create mode 100644 ui/src/assets/second/VideoPressed.svg create mode 100644 ui/src/assets/second/VideoSelected.svg create mode 100644 ui/src/assets/second/VirtualStoragePressed.svg create mode 100644 ui/src/assets/second/VirtualStorageSelected.svg create mode 100644 ui/src/assets/second/access.svg create mode 100644 ui/src/assets/second/advanced.svg create mode 100644 ui/src/assets/second/copy.svg create mode 100644 ui/src/assets/second/delete.svg create mode 100644 ui/src/assets/second/down.svg create mode 100644 ui/src/assets/second/dwon.svg create mode 100644 ui/src/assets/second/dwon2.svg create mode 100644 ui/src/assets/second/float_button1.svg create mode 100644 ui/src/assets/second/float_button2.svg create mode 100644 ui/src/assets/second/general.svg create mode 100644 ui/src/assets/second/gobottom.svg create mode 100644 ui/src/assets/second/hardware.svg create mode 100644 ui/src/assets/second/hdmi-cord.svg create mode 100644 ui/src/assets/second/hdml.svg create mode 100644 ui/src/assets/second/hdml2.svg create mode 100644 ui/src/assets/second/keyboard.svg create mode 100644 ui/src/assets/second/keyboard2.svg create mode 100644 ui/src/assets/second/left.svg create mode 100644 ui/src/assets/second/media.svg create mode 100644 ui/src/assets/second/minglingdarkac.svg create mode 100644 ui/src/assets/second/mouse.svg create mode 100644 ui/src/assets/second/network.svg create mode 100644 ui/src/assets/second/noSD.svg create mode 100644 ui/src/assets/second/open.svg create mode 100644 ui/src/assets/second/refresh.svg create mode 100644 ui/src/assets/second/rname.py create mode 100644 ui/src/assets/second/set1.svg create mode 100644 ui/src/assets/second/set2.svg create mode 100644 ui/src/assets/second/shuaxin.svg create mode 100644 ui/src/assets/second/state.svg create mode 100644 ui/src/assets/second/swich_dir2.svg create mode 100644 ui/src/assets/second/swich_dri1.svg create mode 100644 ui/src/assets/second/tiaozhuan.svg create mode 100644 ui/src/assets/second/to_down.svg create mode 100644 ui/src/assets/second/to_up.svg create mode 100644 ui/src/assets/second/up.svg create mode 100644 ui/src/assets/second/upload.svg create mode 100644 ui/src/assets/second/usb.svg create mode 100644 ui/src/assets/second/usb2.svg create mode 100644 ui/src/assets/second/vedio.svg create mode 100644 ui/src/assets/second/vedio2.svg create mode 100644 ui/src/assets/second/version.svg create mode 100644 ui/src/assets/second/xinhao.svg create mode 100644 ui/src/assets/second/zhongduan.svg create mode 100644 ui/src/assets/second/zhongduan2.svg delete mode 100644 ui/src/components/ActionBar.tsx create mode 100644 ui/src/components/AdaptiveContainer.tsx delete mode 100644 ui/src/components/AuthLayout.tsx delete mode 100644 ui/src/components/CardHeader.tsx delete mode 100644 ui/src/components/Checkbox.tsx rename ui/src/components/{sidebar/connectionStats.tsx => ConnectionStats.tsx} (94%) delete mode 100644 ui/src/components/DhcpLeaseCard.tsx create mode 100644 ui/src/components/Drawer.tsx delete mode 100644 ui/src/components/FeatureFlag.tsx create mode 100644 ui/src/components/FileManager/FileUploader.tsx rename ui/src/{routes/devices.$id.mount.tsx => components/FileManager/Mount.tsx} (97%) rename ui/src/components/{ => FileManager}/UploadDialog.tsx (84%) delete mode 100644 ui/src/components/GridBackground.tsx rename ui/src/components/{ => Header}/Header.tsx (91%) rename ui/src/components/{ => Header}/PeerConnectionStatusCard.tsx (96%) rename ui/src/components/{ => Header}/StatusCards.tsx (100%) rename ui/src/components/{ => Header}/USBStateStatus.tsx (98%) rename ui/src/components/{ => Header}/VpnConnectionStatusCard.tsx (64%) delete mode 100644 ui/src/components/Icons.tsx delete mode 100644 ui/src/components/InfoBar.tsx rename ui/src/components/{ => Macro}/Combobox.tsx (96%) rename ui/src/components/{ => Macro}/MacroBar.tsx (100%) rename ui/src/components/{ => Macro}/MacroForm.tsx (87%) rename ui/src/components/{ => Macro}/MacroStepCard.tsx (88%) create mode 100644 ui/src/components/MousePanel.tsx create mode 100644 ui/src/components/Network/DhcpLeaseCard.tsx rename ui/src/components/{ => Network}/Ipv6NetworkCard.tsx (93%) create mode 100644 ui/src/components/PinchZoom.tsx create mode 100644 ui/src/components/PopoverButton.tsx create mode 100644 ui/src/components/PreUploadedImageItem.tsx create mode 100644 ui/src/components/ScrollThrottlingSelect.tsx rename ui/src/components/{ => Settings}/SettingsPageheader.tsx (100%) rename ui/src/components/{ => Settings}/SettingsSectionHeader.tsx (100%) create mode 100644 ui/src/components/Settings/SettingsView.tsx create mode 100644 ui/src/components/Sidebar/SideTabs.tsx create mode 100644 ui/src/components/Sidebar/SidebarDrawer.tsx rename ui/src/components/{ => Sidebar}/SidebarHeader.tsx (99%) create mode 100644 ui/src/components/Sidebar/SlideAnimation.tsx create mode 100644 ui/src/components/Sidebar/StatsTopbar.tsx create mode 100644 ui/src/components/SmartButton.tsx create mode 100644 ui/src/components/StatsSidebarHeader.tsx delete mode 100644 ui/src/components/Tabs.tsx delete mode 100644 ui/src/components/Terminal.tsx delete mode 100644 ui/src/components/UpdateInProgressStatusCard.tsx delete mode 100644 ui/src/components/UsbDeviceSetting.tsx delete mode 100644 ui/src/components/UsbInfoSetting.tsx create mode 100644 ui/src/components/Video/VideoContainer.tsx create mode 100644 ui/src/components/Video/VideoElement.tsx rename ui/src/components/{popovers => }/WakeOnLan/AddDeviceForm.tsx (82%) create mode 100644 ui/src/components/WakeOnLan/DeviceList.tsx create mode 100644 ui/src/components/WakeOnLan/EmptyStateCard.tsx rename ui/src/components/{popovers => }/WakeOnLan/Index.tsx (97%) delete mode 100644 ui/src/components/WebRTCVideo.tsx delete mode 100644 ui/src/components/extensions/IOControl.tsx delete mode 100644 ui/src/components/extensions/PowerControl.tsx delete mode 100644 ui/src/components/extensions/SerialConsole.tsx delete mode 100644 ui/src/components/popovers/ExtensionPopover.tsx delete mode 100644 ui/src/components/popovers/MountPopover.tsx delete mode 100644 ui/src/components/popovers/PasteModal.tsx delete mode 100644 ui/src/components/popovers/WakeOnLan/DeviceList.tsx delete mode 100644 ui/src/components/popovers/WakeOnLan/EmptyStateCard.tsx create mode 100644 ui/src/layout/components_bottom/keyboard/KeyboardLayoutModal.tsx create mode 100644 ui/src/layout/components_bottom/keyboard/KeyboardPanel.tsx create mode 100644 ui/src/layout/components_bottom/terminal/TerminalKVM.tsx create mode 100644 ui/src/layout/components_bottom/terminal/TerminalSerial.tsx create mode 100644 ui/src/layout/components_bottom/terminal/TerminalSerialSide.tsx create mode 100644 ui/src/layout/components_bottom/terminal/common.ts create mode 100644 ui/src/layout/components_bottom/terminal/index.mobile.tsx create mode 100644 ui/src/layout/components_bottom/terminal/index.pc.tsx create mode 100644 ui/src/layout/components_bottom/terminal/useTerminal.ts create mode 100644 ui/src/layout/components_bottom/usbepmode/UsbEpModeSelect.mobile.tsx create mode 100644 ui/src/layout/components_bottom/usbepmode/UsbEpModeSelect.pc.tsx create mode 100644 ui/src/layout/components_bottom/usbepmode/UsbEpModeSelect.tsx create mode 100644 ui/src/layout/components_bottom/usbepmode/usbModeOptions.ts create mode 100644 ui/src/layout/components_setting/SettingsModalMobile.tsx create mode 100644 ui/src/layout/components_setting/SettingsModalPC.tsx rename ui/src/{routes/devices.$id.settings.access._index.tsx => layout/components_setting/access/AccessContent.tsx} (84%) rename ui/src/{routes/devices.$id.settings.access.local-auth.tsx => layout/components_setting/access/auth.tsx} (61%) rename ui/src/{routes/devices.$id.settings.advanced.tsx => layout/components_setting/advanced/AdvancedContent.tsx} (66%) create mode 100644 ui/src/layout/components_setting/general/GeneralContent.tsx rename ui/src/{routes/devices.$id.settings.hardware.tsx => layout/components_setting/hardware/HardwareContent.tsx} (89%) create mode 100644 ui/src/layout/components_setting/index.tsx rename ui/src/{routes/devices.$id.settings.network.tsx => layout/components_setting/network/NetworkContent.tsx} (73%) create mode 100644 ui/src/layout/components_setting/version/VersionContent.tsx create mode 100644 ui/src/layout/components_side/Clipboard/Clipboard.tsx create mode 100644 ui/src/layout/components_side/Macros/MacroTopBar.tsx rename ui/src/{routes/devices.$id.settings.macros.add.tsx => layout/components_side/Macros/SettingsMacrosAdd.tsx} (73%) rename ui/src/{routes/devices.$id.settings.macros.edit.tsx => layout/components_side/Macros/SettingsMacrosEdit.tsx} (57%) rename ui/src/{routes/devices.$id.settings.macros.tsx => layout/components_side/Macros/SettingsMacrosList.tsx} (75%) create mode 100644 ui/src/layout/components_side/Macros/index.tsx create mode 100644 ui/src/layout/components_side/Power/PowerControlUp.tsx create mode 100644 ui/src/layout/components_side/Power/WakeOnLan.tsx create mode 100644 ui/src/layout/components_side/Power/index.tsx create mode 100644 ui/src/layout/components_side/SharedFolders/DeviceFilePage.tsx create mode 100644 ui/src/layout/components_side/SharedFolders/FileManager.tsx create mode 100644 ui/src/layout/components_side/SharedFolders/SDFilePage.tsx create mode 100644 ui/src/layout/components_side/SharedFolders/index.tsx create mode 100644 ui/src/layout/components_side/Stats/StatsSidebar.tsx create mode 100644 ui/src/layout/components_side/Video/SettingsVideoSide.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/DevicePage.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/ImageManager.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/SDPage.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/StorageSpaceBar.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/UnMount.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/ViewHeader.tsx create mode 100644 ui/src/layout/components_side/VirtualMediaSource/index.tsx create mode 100644 ui/src/layout/contexts/DeviceAwareComponentProps.tsx create mode 100644 ui/src/layout/contexts/ThemeContext.tsx create mode 100644 ui/src/layout/core/bar_bottom/BottomBarMobile.tsx create mode 100644 ui/src/layout/core/bar_bottom/BottomBarPC.tsx create mode 100644 ui/src/layout/core/bar_bottom/index.tsx create mode 100644 ui/src/layout/core/bar_side.tsx create mode 100644 ui/src/layout/core/bar_top/TopBarMobile.tsx create mode 100644 ui/src/layout/core/bar_top/TopBarPC.tsx create mode 100644 ui/src/layout/core/bar_top/index.tsx create mode 100644 ui/src/layout/core/desktop/DesktopMobile.tsx create mode 100644 ui/src/layout/core/desktop/DesktopPC.tsx create mode 100644 ui/src/layout/core/desktop/hooks/useFullscreen.ts create mode 100644 ui/src/layout/core/desktop/hooks/useKeyboardEvents.ts create mode 100644 ui/src/layout/core/desktop/hooks/useMouseEvents.ts create mode 100644 ui/src/layout/core/desktop/hooks/usePasteHandler.ts create mode 100644 ui/src/layout/core/desktop/hooks/usePointerLock.ts create mode 100644 ui/src/layout/core/desktop/hooks/useTouchZoom.ts create mode 100644 ui/src/layout/core/desktop/hooks/useVideoEffects.ts create mode 100644 ui/src/layout/core/desktop/hooks/useVideoOverlays.ts create mode 100644 ui/src/layout/core/desktop/hooks/useVideoStream.ts create mode 100644 ui/src/layout/core/desktop/index.tsx rename ui/src/{routes/devices.$id.other-session.tsx => layout/core/other-session.tsx} (70%) create mode 100644 ui/src/layout/index.mobile.tsx rename ui/src/{routes/devices.$id.tsx => layout/index.pc.tsx} (76%) create mode 100644 ui/src/layout/index.tsx create mode 100644 ui/src/layout/theme_color.ts delete mode 100644 ui/src/routes/adopt.tsx delete mode 100644 ui/src/routes/devices.$id.deregister.tsx delete mode 100644 ui/src/routes/devices.$id.mtp.tsx delete mode 100644 ui/src/routes/devices.$id.rename.tsx delete mode 100644 ui/src/routes/devices.$id.settings._index.tsx delete mode 100644 ui/src/routes/devices.$id.settings.appearance.tsx delete mode 100644 ui/src/routes/devices.$id.settings.ctrlaltdel.tsx delete mode 100644 ui/src/routes/devices.$id.settings.general._index.tsx delete mode 100644 ui/src/routes/devices.$id.settings.general.update.tsx delete mode 100644 ui/src/routes/devices.$id.settings.keyboard.tsx delete mode 100644 ui/src/routes/devices.$id.settings.mouse.tsx delete mode 100644 ui/src/routes/devices.$id.settings.tsx delete mode 100644 ui/src/routes/devices.$id.settings.video.tsx delete mode 100644 ui/src/routes/devices.$id.setup.tsx delete mode 100644 ui/src/routes/devices.already-adopted.tsx delete mode 100644 ui/src/routes/devices.tsx delete mode 100644 ui/src/routes/login.tsx create mode 100644 ui/src/routes/login_page/AuthMethodCard.tsx create mode 100644 ui/src/routes/login_page/SetupHeader.tsx create mode 100644 ui/src/routes/login_page/index.tsx create mode 100644 ui/src/routes/login_page/useLocalAuth.ts create mode 100644 ui/src/routes/password.tsx delete mode 100644 ui/src/routes/signup.tsx delete mode 100644 ui/src/routes/welcome-local.mode.tsx delete mode 100644 ui/src/routes/welcome-local.password.tsx delete mode 100644 ui/src/routes/welcome-local.tsx diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index cc36cf7..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: gomod - directory: / - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: github-actions - directory: / - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: npm - directory: /ui - open-pull-requests-limit: 10 - schedule: - interval: monthly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index fdeb5dd..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: build image -on: - push: - branches: - - dev - - main - workflow_dispatch: - pull_request_review: - types: [submitted] - -jobs: - build: - runs-on: buildjet-4vcpu-ubuntu-2204 - name: Build - if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'" - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - cache-dependency-path: "**/package-lock.json" - - name: Set up Golang - uses: actions/setup-go@v5 - with: - go-version: "1.24.4" - - name: Build frontend - run: | - make frontend - - name: Build application - run: | - make build_dev - - name: Run tests - run: | - go test ./... -json > testreport.json - - name: Make test cases - run: | - make build_dev_test - - name: Golang Test Report - uses: becheran/go-testreport@v0.3.2 - with: - input: "testreport.json" - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: kvm-app - path: | - bin/kvm_app - device-tests.tar.gz \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml deleted file mode 100644 index 3ce3ef3..0000000 --- a/.github/workflows/golangci-lint.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: golangci-lint -on: - push: - paths: - - "go.sum" - - "go.mod" - - "**.go" - - ".github/workflows/golangci-lint.yml" - - ".golangci.yml" - pull_request: - -permissions: # added using https://github.com/step-security/secure-repo - contents: read - -jobs: - golangci: - permissions: - contents: read # for actions/checkout to fetch code - pull-requests: read # for golangci/golangci-lint-action to fetch pull requests - name: lint - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install Go - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1 - with: - go-version: 1.24.4 - - name: Create empty resource directory - run: | - mkdir -p static && touch static/.gitkeep - - name: Lint - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 - with: - args: --verbose - version: v2.0.2 diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml deleted file mode 100644 index 5e61e5a..0000000 --- a/.github/workflows/smoketest.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: smoketest -on: - repository_dispatch: - types: [smoketest] - -jobs: - ghbot_payload: - name: Ghbot payload - runs-on: ubuntu-latest - steps: - - name: "GH_CHECK_RUN_ID=${{ github.event.client_payload.check_run_id }}" - run: | - echo "== START GHBOT_PAYLOAD ==" - cat <<'GHPAYLOAD_EOF' | base64 - ${{ toJson(github.event.client_payload) }} - GHPAYLOAD_EOF - echo "== END GHBOT_PAYLOAD ==" - deploy_and_test: - runs-on: buildjet-4vcpu-ubuntu-2204 - name: Smoke test - concurrency: - group: smoketest-jk - steps: - - name: Download artifact - run: | - wget -O /tmp/jk.zip "${{ github.event.client_payload.artifact_download_url }}" - unzip /tmp/jk.zip - - name: Configure WireGuard and check connectivity - run: | - WG_KEY_FILE=$(mktemp) - echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ - sudo apt-get update && sudo apt-get install -y wireguard-tools && \ - sudo ip link add dev wg-ci type wireguard && \ - sudo ip addr add $CI_WG_IPS dev wg-ci && \ - sudo wg set wg-ci listen-port 51820 \ - private-key $WG_KEY_FILE \ - peer $CI_WG_PUBLIC \ - allowed-ips $CI_WG_ALLOWED_IPS \ - endpoint $CI_WG_ENDPOINT \ - persistent-keepalive 15 && \ - sudo ip link set up dev wg-ci && \ - sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci - ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) - env: - CI_HOST: ${{ vars.KVM_CI_HOST }} - CI_WG_IPS: ${{ vars.KVM_CI_WG_IPS }} - CI_WG_GATEWAY: ${{ vars.KVM_CI_GATEWAY }} - CI_WG_ALLOWED_IPS: ${{ vars.KVM_CI_WG_ALLOWED_IPS }} - CI_WG_PUBLIC: ${{ secrets.KVM_CI_WG_PUBLIC }} - CI_WG_PRIVATE: ${{ secrets.KVM_CI_WG_PRIVATE }} - CI_WG_ENDPOINT: ${{ secrets.KVM_CI_WG_ENDPOINT }} - - name: Configure SSH - run: | - # Write SSH private key to a file - SSH_PRIVATE_KEY=$(mktemp) - echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY - chmod 0600 $SSH_PRIVATE_KEY - # Configure SSH - mkdir -p ~/.ssh - cat <> ~/.ssh/config - Host jkci - HostName $CI_HOST - User $CI_USER - StrictHostKeyChecking no - UserKnownHostsFile /dev/null - IdentityFile $SSH_PRIVATE_KEY - EOF - env: - CI_USER: ${{ vars.KVM_CI_USER }} - CI_HOST: ${{ vars.KVM_CI_HOST }} - CI_SSH_PRIVATE: ${{ secrets.KVM_CI_SSH_PRIVATE }} - - name: Run tests - run: | - set -e - echo "+ Copying device-tests.tar.gz to remote host" - ssh jkci "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz - echo "+ Running go tests" - ssh jkci ash << 'EOF' - set -e - TMP_DIR=$(mktemp -d) - cd ${TMP_DIR} - tar zxf /tmp/device-tests.tar.gz - ./gotestsum --format=testdox \ - --jsonfile=/tmp/device-tests.json \ - --post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \ - --raw-command -- ./run_all_tests -json - - GOTESTSUM_EXIT_CODE=$? - if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then - echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)" - rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz - exit 1 - fi - - TESTS_FAILED=$(cat /tmp/device-tests.failed) - if [ "$TESTS_FAILED" -ne 0 ]; then - echo "❌ Tests failed $TESTS_FAILED tests failed" - rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz - exit 1 - fi - - echo "✅ Tests passed" - rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz - EOF - ssh jkci "cat /tmp/device-tests.json" > device-tests.json - - name: Set up Golang - uses: actions/setup-go@v5 - with: - go-version: "1.24.4" - - name: Golang Test Report - uses: becheran/go-testreport@v0.3.2 - with: - input: "device-tests.json" - - name: Deploy application - run: | - set -e - # Copy the binary to the remote host - echo "+ Copying the application to the remote host" - cat bin/kvm_app | gzip | ssh jkci "cat > /userdata/picokvm/kvm_app.update.gz" - # Deploy and run the application on the remote host - echo "+ Deploying the application on the remote host" - ssh jkci ash < /proc/sys/vm/drop_caches - # Reboot the application - reboot -d 5 -f & - EOF - sleep 10 - echo "Deployment complete, waiting for KVM to come back online " - function check_online() { - for i in {1..60}; do - if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then - echo "KVM is back online" - return 0 - fi - echo -n "." - sleep 1 - done - echo "KVM did not come back online within 60 seconds" - return 1 - } - check_online - env: - CI_HOST: ${{ vars.KVM_CI_HOST }} - - name: Run smoke tests - run: | - echo "+ Checking the status of the device" - curl -v http://$CI_HOST/device/status && echo - echo "+ Waiting for 15 seconds to allow all services to start" - sleep 15 - echo "+ Collecting logs" - local_log_tar=$(mktemp) - ssh jkci ash > $local_log_tar <<'EOF' - log_path=$(mktemp -d) - dmesg > $log_path/dmesg.log - cp /userdata/picokvm/last.log $log_path/last.log - tar -czf - -C $log_path . - EOF - tar -xf $local_log_tar - cat dmesg.log last.log - env: - CI_HOST: ${{ vars.KVM_CI_HOST }} - - name: Upload logs - uses: actions/upload-artifact@v4 - with: - name: device-logs - path: | - last.log - dmesg.log - device-tests.json diff --git a/.github/workflows/ui-lint.yml b/.github/workflows/ui-lint.yml deleted file mode 100644 index 492a5fe..0000000 --- a/.github/workflows/ui-lint.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: ui-lint -on: - push: - paths: - - "ui/**" - - "package.json" - - "package-lock.json" - - ".github/workflows/ui-lint.yml" - -permissions: - contents: read - -jobs: - ui-lint: - name: UI Lint - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: v21.1.0 - cache: "npm" - cache-dependency-path: "ui/package-lock.json" - - name: Install dependencies - run: | - cd ui - npm ci - - name: Lint UI - run: | - cd ui - npm run lint diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 2433386..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: "2" -linters: - enable: - - forbidigo - - misspell - - whitespace - - gochecknoinits - disable: - - unused - settings: - forbidigo: - forbid: - - pattern: ^fmt\.Print.*$ - msg: Do not commit print statements. Use logger package. - - pattern: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ - msg: Do not commit log statements. Use logger package. - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - rules: - - linters: - - errcheck - path: _test.go - - linters: - - gochecknoinits - path: internal/logging/sse.go - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - enable: - - goimports - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ diff --git a/Makefile b/Makefile index 5121226..7bceb6b 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV ?= 0.0.4-dev -VERSION ?= 0.0.4 +VERSION_DEV ?= 0.1.1-dev +VERSION ?= 0.1.1 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := kvm diff --git a/README.md b/README.md index cb815c4..92ecad7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![luckfox](https://github.com/LuckfoxTECH/luckfox-pico/assets/144299491/cec5c4a5-22b9-4a9a-abb1-704b11651e88) [中文](./README_CN.md) # Luckfox PicoKVM -Luckfox PicoKVM is a lightweight IP KVM operation and maintenance tool. It allows remote access to a target device’s display and simulates HID input over the network, enabling contactless operation and management of development boards, PCs, and servers. The product delivers stable, low-latency video capture and remote control, making it well-suited for scenarios such as remote computer management and server maintenance. The software is based on a secondary development of JetKVM. +Luckfox PicoKVM is a lightweight IP KVM operation and maintenance tool. It allows remote access to a target device’s display and simulates HID input over the network, enabling contactless operation and management of development boards, PCs, and servers. The product delivers stable, low-latency video capture and remote control, making it well-suited for scenarios such as remote computer management and server maintenance. The software is based on a secondary development of [JetKVM](https://jetkvm.com/). ## Features * **Micro SD card support**: Used for software boot settings or storage expansion diff --git a/README_CN.md b/README_CN.md index 7469ef2..8dea104 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,7 +1,7 @@ ![luckfox](https://github.com/LuckfoxTECH/luckfox-pico/assets/144299491/cec5c4a5-22b9-4a9a-abb1-704b11651e88) [English](./README.md) # Luckfox PicoKVM -Luckfox PicoKVM 是一款轻量级 IP KVM 运维工具,支持通过网络远程获取目标设备画面并模拟 HID 输入,实现对开发板、电脑及服务器等系统的无接触运维管理。该产品具备稳定、低延迟的视频采集和远程控制能力,广泛适用于远程电脑控制和服务器维护等场景。软件基于 JetKVM 二次开发。 +Luckfox PicoKVM 是一款轻量级 IP KVM 运维工具,支持通过网络远程获取目标设备画面并模拟 HID 输入,实现对开发板、电脑及服务器等系统的无接触运维管理。该产品具备稳定、低延迟的视频采集和远程控制能力,广泛适用于远程电脑控制和服务器维护等场景。软件基于 [JetKVM](https://jetkvm.com/) 二次开发。 ## 特性 * **Micro SD 卡支持**:可用于软件启动设置或存储拓展 diff --git a/config.go b/config.go index 4795962..3b43a16 100644 --- a/config.go +++ b/config.go @@ -100,10 +100,10 @@ type Config struct { DisplayOffAfterSec int `json:"display_off_after_sec"` TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" UsbConfig *usbgadget.Config `json:"usb_config"` - UsbDevices *usbgadget.Devices `json:"usb_devices"` - NetworkConfig *network.NetworkConfig `json:"network_config"` - AppliedNetworkConfig *network.NetworkConfig `json:"applied_network_config,omitempty"` - DefaultLogLevel string `json:"default_log_level"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` + NetworkConfig *network.NetworkConfig `json:"network_config"` + AppliedNetworkConfig *network.NetworkConfig `json:"applied_network_config,omitempty"` + DefaultLogLevel string `json:"default_log_level"` TailScaleAutoStart bool `json:"tailscale_autostart"` TailScaleXEdge bool `json:"tailscale_xedge"` ZeroTierNetworkID string `json:"zerotier_network_id"` @@ -123,17 +123,25 @@ type Config struct { EasytierConfig EasytierConfig `json:"easytier_config"` VntAutoStart bool `json:"vnt_autostart"` VntConfig VntConfig `json:"vnt_config"` + WireguardAutoStart bool `json:"wireguard_autostart"` + WireguardConfig WireguardConfig `json:"wireguard_config"` + NpuAppEnabled bool `json:"npu_app_enabled"` } type VntConfig struct { - Token string `json:"token"` - DeviceId string `json:"device_id"` - Name string `json:"name"` - ServerAddr string `json:"server_addr"` - ConfigMode string `json:"config_mode"` // "params" or "file" - ConfigFile string `json:"config_file"` - Model string `json:"model"` - Password string `json:"password"` + Token string `json:"token"` + DeviceId string `json:"device_id"` + Name string `json:"name"` + ServerAddr string `json:"server_addr"` + ConfigMode string `json:"config_mode"` // "params" or "file" + ConfigFile string `json:"config_file"` + Model string `json:"model"` + Password string `json:"password"` +} + +type WireguardConfig struct { + NetworkName string `json:"network_name"` + ConfigFile string `json:"config_file"` } const configPath = "/userdata/kvm_config.json" @@ -167,20 +175,22 @@ var defaultConfig = &Config{ Audio: false, //At any given time, only one of Audio and Mtp can be set to true Mtp: false, }, - NetworkConfig: &network.NetworkConfig{}, - AppliedNetworkConfig: nil, - DefaultLogLevel: "INFO", - ZeroTierAutoStart: false, - TailScaleAutoStart: false, - TailScaleXEdge: false, - FrpcAutoStart: false, + NetworkConfig: &network.NetworkConfig{}, + AppliedNetworkConfig: nil, + DefaultLogLevel: "INFO", + ZeroTierAutoStart: false, + TailScaleAutoStart: false, + TailScaleXEdge: false, + FrpcAutoStart: false, CloudflaredAutoStart: false, - IO0Status: true, - IO1Status: true, - AudioMode: "disabled", - LEDGreenMode: "network-rx", - LEDYellowMode: "kernel-activity", - AutoMountSystemInfo: true, + IO0Status: false, + IO1Status: false, + AudioMode: "disabled", + LEDGreenMode: "network-rx", + LEDYellowMode: "kernel-activity", + AutoMountSystemInfo: true, + WireguardAutoStart: false, + NpuAppEnabled: false, } var ( diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go index e77e2c1..26b5ee4 100644 --- a/internal/usbgadget/udc.go +++ b/internal/usbgadget/udc.go @@ -52,6 +52,16 @@ func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error { // GetUsbState returns the current state of the USB gadget func (u *UsbGadget) GetUsbState() (state string) { + // Check the auxiliary disc node first + discFile := "/sys/devices/platform/ff3e0000.usb2-phy/disc" + discBytes, err := os.ReadFile(discFile) + if err == nil { + discState := strings.TrimSpace(string(discBytes)) + if discState == "DISCONNECTED" { + return "not attached" + } + } + stateFile := path.Join("/sys/class/udc", u.udc, "state") stateBytes, err := os.ReadFile(stateFile) if err != nil { diff --git a/io.go b/io.go index 1da8b4c..e2c18fe 100644 --- a/io.go +++ b/io.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "strconv" + "strings" + "time" ) const ( @@ -109,11 +111,62 @@ func setLedMode(ledConfigPath string, mode string) error { return nil } +func pulseGPIO(pin int, duration time.Duration) error { + // First pull up + if err := setGPIOValue(pin, true); err != nil { + return err + } + + // Wait for duration + time.Sleep(duration) + + // Then pull down + if err := setGPIOValue(pin, false); err != nil { + return err + } + + return nil +} + +func getGPIOValue(pin int) (bool, error) { + if !isGPIOExported(pin) { + if err := exportGPIO(pin); err != nil { + return false, fmt.Errorf("failed to export GPIO: %v", err) + } + if err := setGPIODirection(pin, "in"); err != nil { + return false, fmt.Errorf("failed to set GPIO direction: %v", err) + } + } + valueFile := fmt.Sprintf("%s/gpio%d/value", gpioBasePath, pin) + data, err := os.ReadFile(valueFile) + if err != nil { + return false, fmt.Errorf("failed to read GPIO value: %v", err) + } + value := strings.TrimSpace(string(data)) + return value == "1", nil +} + +func resetIOInput() error { + // Reset IO2 (GPIO0) and IO3 (GPIO1) to input mode + if err := setGPIODirection(0, "in"); err != nil { + return fmt.Errorf("failed to reset IO2: %v", err) + } + if err := setGPIODirection(1, "in"); err != nil { + return fmt.Errorf("failed to reset IO3: %v", err) + } + return nil +} + func initGPIO() { LoadConfig() // IO0: GPIO58 IO1: GPIO59 _ = setGPIOValue(58, config.IO0Status) _ = setGPIOValue(59, config.IO1Status) + + // IO2: GPIO0 IO3: GPIO1 - Input + _ = setGPIODirection(0, "in") + _ = setGPIODirection(1, "in") + _ = setLedMode(ledYellowPath, config.LEDYellowMode) _ = setLedMode(ledGreenPath, config.LEDGreenMode) } diff --git a/jsonrpc.go b/jsonrpc.go index a6849f8..d004e74 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -91,6 +91,39 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) { } } +func DispatchRPCRequest(request JSONRPCRequest) (JSONRPCResponse, error) { + handler, ok := rpcHandlers[request.Method] + if !ok { + return JSONRPCResponse{ + JSONRPC: "2.0", + Error: map[string]interface{}{ + "code": -32601, + "message": "Method not found", + }, + ID: request.ID, + }, nil + } + + result, err := callRPCHandler(handler, request.Params) + if err != nil { + return JSONRPCResponse{ + JSONRPC: "2.0", + Error: map[string]interface{}{ + "code": -32603, + "message": "Internal error", + "data": err.Error(), + }, + ID: request.ID, + }, nil + } + + return JSONRPCResponse{ + JSONRPC: "2.0", + Result: result, + ID: request.ID, + }, nil +} + func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { var request JSONRPCRequest err := json.Unmarshal(message.Data, &request) @@ -119,44 +152,10 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { scopedLogger.Trace().Msg("Received RPC request") - handler, ok := rpcHandlers[request.Method] - if !ok { - errorResponse := JSONRPCResponse{ - JSONRPC: "2.0", - Error: map[string]interface{}{ - "code": -32601, - "message": "Method not found", - }, - ID: request.ID, - } - writeJSONRPCResponse(errorResponse, session) - return - } + response, _ := DispatchRPCRequest(request) - scopedLogger.Trace().Msg("Calling RPC handler") - result, err := callRPCHandler(handler, request.Params) - if err != nil { - scopedLogger.Error().Err(err).Msg("Error calling RPC handler") - errorResponse := JSONRPCResponse{ - JSONRPC: "2.0", - Error: map[string]interface{}{ - "code": -32603, - "message": "Internal error", - "data": err.Error(), - }, - ID: request.ID, - } - writeJSONRPCResponse(errorResponse, session) - return - } + scopedLogger.Trace().Interface("result", response.Result).Msg("RPC handler returned") - scopedLogger.Trace().Interface("result", result).Msg("RPC handler returned") - - response := JSONRPCResponse{ - JSONRPC: "2.0", - Result: result, - ID: request.ID, - } writeJSONRPCResponse(response, session) } @@ -209,6 +208,41 @@ func rpcSetStreamQualityFactor(factor float64) error { return nil } +var streamEncodecType = "avc" + +func rpcGetStreamEncodecType() (string, error) { + return streamEncodecType, nil +} + +func rpcSetStreamEncodecType(encodecType string) error { + logger.Info().Str("encodecType", encodecType).Msg("Setting stream encodec type") + var _, err = CallCtrlAction("set_video_encodec_type", map[string]interface{}{"encodec_type": encodecType}) + if err != nil { + return err + } + + streamEncodecType = encodecType + return nil +} + +func rpcSetNpuAppStatus(enable bool) error { + logger.Info().Bool("enable", enable).Msg("Setting NPU app status") + var _, err = CallCtrlAction("set_yolo_enable", map[string]interface{}{"enable": enable}) + if err != nil { + return err + } + + config.NpuAppEnabled = enable + if SaveConfig() != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcGetNpuAppStatus() (bool, error) { + return config.NpuAppEnabled, nil +} + func rpcGetAutoUpdateState() (bool, error) { return config.AutoUpdateEnabled, nil } @@ -345,6 +379,15 @@ func rpcTryUpdate() error { return nil } +func rpcGetCustomUpdateBaseURL() (string, error) { + return customUpdateBaseURL, nil +} + +func rpcSetCustomUpdateBaseURL(baseURL string) error { + customUpdateBaseURL = baseURL + return nil +} + func rpcSetDisplayRotation(params DisplayRotationSettings) error { var err error _, err = lvDispSetRotation(params.Rotation) @@ -1023,6 +1066,69 @@ func rpcSetIOSettings(settings IOSettings) error { return nil } +func rpcSetIOStatus(ioName string, status bool) error { + var pin int + if ioName == "power" { + pin = 58 + } else if ioName == "reset" { + pin = 59 + } else { + return fmt.Errorf("unknown IO name: %s", ioName) + } + + if err := setGPIOValue(pin, status); err != nil { + return fmt.Errorf("failed to set GPIO value: %v", err) + } + return nil +} + +func rpcTriggerPower() error { + go func() { + if err := pulseGPIO(58, 2*time.Second); err != nil { + logger.Error().Err(err).Msg("Failed to trigger power pulse") + } + }() + return nil +} + +func rpcTriggerReset() error { + go func() { + if err := pulseGPIO(59, 2*time.Second); err != nil { + logger.Error().Err(err).Msg("Failed to trigger reset pulse") + } + }() + return nil +} + +func rpcResetIOInput() error { + if err := resetIOInput(); err != nil { + return err + } + return nil +} + +func rpcGetIOInputStatus() (map[string]bool, error) { + // IO2: GPIO0 - Power LED + // IO3: GPIO1 - HDD LED + powerLed, err := getGPIOValue(0) + if err != nil { + logger.Error().Err(err).Msg("Failed to read Power LED status") + // Don't return error, just default to false + } + + hddLed, err := getGPIOValue(1) + if err != nil { + logger.Error().Err(err).Msg("Failed to read HDD LED status") + // Don't return error, just default to false + } + + // Active Low: Low level means LED is ON (Radio active) + return map[string]bool{ + "powerLed": !powerLed, + "hddLed": !hddLed, + }, nil +} + func rpcGetAudioMode() (string, error) { return config.AudioMode, nil } @@ -1088,133 +1194,155 @@ func rpcSetAutoMountSystemInfo(enabled bool) error { return nil } -var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "reboot": {Func: rpcReboot, Params: []string{"force"}}, - "getDeviceID": {Func: rpcGetDeviceID}, - "getNetworkState": {Func: rpcGetNetworkState}, - "getNetworkSettings": {Func: rpcGetNetworkSettings}, - "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, - "renewDHCPLease": {Func: rpcRenewDHCPLease}, - "requestDHCPAddress": {Func: rpcRequestDHCPAddress, Params: []string{"ip"}}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, - "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "reinitializeUsbGadget": {Func: rpcReinitializeUsbGadget}, - "reinitializeUsbGadgetSoft": {Func: rpcReinitializeUsbGadgetSoft}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "sendUsbWakeupSignal": {Func: rpcSendUsbWakeupSignal}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, - "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, - "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, - "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, - "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, - "getEDID": {Func: rpcGetEDID}, - "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, - "setForceHpd": {Func: rpcSetForceHpd, Params: []string{"forceHpd"}}, - "getForceHpd": {Func: rpcGetForceHpd}, - "getDevChannelState": {Func: rpcGetDevChannelState}, - "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, - "getLocalUpdateStatus": {Func: rpcGetLocalUpdateStatus}, - "getUpdateStatus": {Func: rpcGetUpdateStatus}, - "tryUpdate": {Func: rpcTryUpdate}, - "getDevModeState": {Func: rpcGetDevModeState}, - "getSSHKeyState": {Func: rpcGetSSHKeyState}, - "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, - "getTLSState": {Func: rpcGetTLSState}, - "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "getUsbConfig": {Func: rpcGetUsbConfig}, - "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "getSDStorageSpace": {Func: rpcGetSDStorageSpace}, - "resetSDStorage": {Func: rpcResetSDStorage}, - "mountSDStorage": {Func: rpcMountSDStorage}, - "unmountSDStorage": {Func: rpcUnmountSDStorage}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "mountWithSDStorage": {Func: rpcMountWithSDStorage, Params: []string{"filename", "mode"}}, - "setAutoMountSystemInfo": {Func: rpcSetAutoMountSystemInfo, Params: []string{"enabled"}}, - "getAutoMountSystemInfo": {Func: rpcGetAutoMountSystemInfo}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, - "listSDStorageFiles": {Func: rpcListSDStorageFiles}, - "deleteSDStorageFile": {Func: rpcDeleteSDStorageFile, Params: []string{"filename"}}, - "startSDStorageFileUpload": {Func: rpcStartSDStorageFileUpload, Params: []string{"filename", "size"}}, - "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, - "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, - "resetConfig": {Func: rpcResetConfig}, - "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, - "getDisplayRotation": {Func: rpcGetDisplayRotation}, - "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, - "getBacklightSettings": {Func: rpcGetBacklightSettings}, - "setTimeZone": {Func: rpcSetTimeZone, Params: []string{"timeZone"}}, - "getTimeZone": {Func: rpcGetTimeZone}, - "setLedGreenMode": {Func: rpcSetLedGreenMode, Params: []string{"mode"}}, - "setLedYellowMode": {Func: rpcSetLedYellowMode, Params: []string{"mode"}}, - "getLedGreenMode": {Func: rpcGetLedGreenMode}, - "getLedYellowMode": {Func: rpcGetLedYellowMode}, - "getActiveExtension": {Func: rpcGetActiveExtension}, - "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, - "getSerialSettings": {Func: rpcGetSerialSettings}, - "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, - "getUsbDevices": {Func: rpcGetUsbDevices}, - "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, - "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, - "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, - "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, - "getKeyboardMacros": {Func: getKeyboardMacros}, - "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, - "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, - "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, - "getIOSettings": {Func: rpcGetIOSettings}, - "setIOSettings": {Func: rpcSetIOSettings, Params: []string{"settings"}}, - "getSDMountStatus": {Func: rpcGetSDMountStatus}, - "loginTailScale": {Func: rpcLoginTailScale, Params: []string{"xEdge"}}, - "logoutTailScale": {Func: rpcLogoutTailScale}, - "cancelTailScale": {Func: rpcCancelTailScale}, - "getTailScaleSettings": {Func: rpcGetTailScaleSettings}, - "loginZeroTier": {Func: rpcLoginZeroTier, Params: []string{"networkID"}}, - "logoutZeroTier": {Func: rpcLogoutZeroTier, Params: []string{"networkID"}}, - "getZeroTierSettings": {Func: rpcGetZeroTierSettings}, - "setUpdateSource": {Func: rpcSetUpdateSource, Params: []string{"source"}}, - "getAudioMode": {Func: rpcGetAudioMode}, - "setAudioMode": {Func: rpcSetAudioMode, Params: []string{"mode"}}, - "startFrpc": {Func: rpcStartFrpc, Params: []string{"frpcToml"}}, - "stopFrpc": {Func: rpcStopFrpc}, - "getFrpcStatus": {Func: rpcGetFrpcStatus}, - "getFrpcToml": {Func: rpcGetFrpcToml}, - "getFrpcLog": {Func: rpcGetFrpcLog}, - "startEasyTier": {Func: rpcStartEasyTier, Params: []string{"name", "secret", "node"}}, - "stopEasyTier": {Func: rpcStopEasyTier}, - "getEasyTierStatus": {Func: rpcGetEasyTierStatus}, - "getEasyTierConfig": {Func: rpcGetEasyTierConfig}, - "getEasyTierLog": {Func: rpcGetEasyTierLog}, - "startVnt": {Func: rpcStartVnt, Params: []string{"config_mode", "token", "device_id", "name", "server_addr", "config_file", "model", "password"}}, - "stopVnt": {Func: rpcStopVnt}, - "getVntStatus": {Func: rpcGetVntStatus}, - "getVntConfig": {Func: rpcGetVntConfig}, - "getVntConfigFile": {Func: rpcGetVntConfigFile}, - "getVntLog": {Func: rpcGetVntLog}, - "getVntInfo": {Func: rpcGetVntInfo}, - "getEasyTierNodeInfo": {Func: rpcGetEasyTierNodeInfo}, - "startCloudflared": {Func: rpcStartCloudflared, Params: []string{"token"}}, - "stopCloudflared": {Func: rpcStopCloudflared}, - "getCloudflaredStatus": {Func: rpcGetCloudflaredStatus}, - "getCloudflaredLog": {Func: rpcGetCloudflaredLog}, +func rpcConfirmOtherSession() (bool, error) { + return true, nil +} + +var rpcHandlers = map[string]RPCHandler{ + "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, + "getDeviceID": {Func: rpcGetDeviceID}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, + "requestDHCPAddress": {Func: rpcRequestDHCPAddress, Params: []string{"ip"}}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, + "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "getVideoState": {Func: rpcGetVideoState}, + "getUSBState": {Func: rpcGetUSBState}, + "reinitializeUsbGadget": {Func: rpcReinitializeUsbGadget}, + "reinitializeUsbGadgetSoft": {Func: rpcReinitializeUsbGadgetSoft}, + "unmountImage": {Func: rpcUnmountImage}, + "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: rpcGetJigglerState}, + "sendUsbWakeupSignal": {Func: rpcSendUsbWakeupSignal}, + "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, + "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, + "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, + "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, + "getEDID": {Func: rpcGetEDID}, + "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, + "setForceHpd": {Func: rpcSetForceHpd, Params: []string{"forceHpd"}}, + "getForceHpd": {Func: rpcGetForceHpd}, + "getDevChannelState": {Func: rpcGetDevChannelState}, + "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getLocalUpdateStatus": {Func: rpcGetLocalUpdateStatus}, + "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "tryUpdate": {Func: rpcTryUpdate}, + "getCustomUpdateBaseURL": {Func: rpcGetCustomUpdateBaseURL}, + "setCustomUpdateBaseURL": {Func: rpcSetCustomUpdateBaseURL, Params: []string{"baseURL"}}, + "getDevModeState": {Func: rpcGetDevModeState}, + "getSSHKeyState": {Func: rpcGetSSHKeyState}, + "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, + "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, + "getStorageSpace": {Func: rpcGetStorageSpace}, + "getSDStorageSpace": {Func: rpcGetSDStorageSpace}, + "resetSDStorage": {Func: rpcResetSDStorage}, + "mountSDStorage": {Func: rpcMountSDStorage}, + "unmountSDStorage": {Func: rpcUnmountSDStorage}, + "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, + "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, + "mountWithSDStorage": {Func: rpcMountWithSDStorage, Params: []string{"filename", "mode"}}, + "setAutoMountSystemInfo": {Func: rpcSetAutoMountSystemInfo, Params: []string{"enabled"}}, + "getAutoMountSystemInfo": {Func: rpcGetAutoMountSystemInfo}, + "confirmOtherSession": {Func: rpcConfirmOtherSession}, + "listStorageFiles": {Func: rpcListStorageFiles}, + "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "listSDStorageFiles": {Func: rpcListSDStorageFiles}, + "deleteSDStorageFile": {Func: rpcDeleteSDStorageFile, Params: []string{"filename"}}, + "startSDStorageFileUpload": {Func: rpcStartSDStorageFileUpload, Params: []string{"filename", "size"}}, + "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, + "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, + "resetConfig": {Func: rpcResetConfig}, + "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, + "getDisplayRotation": {Func: rpcGetDisplayRotation}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, + "setTimeZone": {Func: rpcSetTimeZone, Params: []string{"timeZone"}}, + "getTimeZone": {Func: rpcGetTimeZone}, + "setLedGreenMode": {Func: rpcSetLedGreenMode, Params: []string{"mode"}}, + "setLedYellowMode": {Func: rpcSetLedYellowMode, Params: []string{"mode"}}, + "getLedGreenMode": {Func: rpcGetLedGreenMode}, + "getLedYellowMode": {Func: rpcGetLedYellowMode}, + "getActiveExtension": {Func: rpcGetActiveExtension}, + "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, + "getSerialSettings": {Func: rpcGetSerialSettings}, + "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, + "getUsbDevices": {Func: rpcGetUsbDevices}, + "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, + "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, + "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, + "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "getIOSettings": {Func: rpcGetIOSettings}, + "setIOSettings": {Func: rpcSetIOSettings, Params: []string{"settings"}}, + "triggerPower": {Func: rpcTriggerPower}, + "triggerReset": {Func: rpcTriggerReset}, + "setIOStatus": {Func: rpcSetIOStatus, Params: []string{"ioName", "status"}}, + "getIOInputStatus": {Func: rpcGetIOInputStatus}, + "resetIOInput": {Func: rpcResetIOInput}, + "getSDMountStatus": {Func: rpcGetSDMountStatus}, + "loginTailScale": {Func: rpcLoginTailScale, Params: []string{"xEdge"}}, + "logoutTailScale": {Func: rpcLogoutTailScale}, + "cancelTailScale": {Func: rpcCancelTailScale}, + "getTailScaleSettings": {Func: rpcGetTailScaleSettings}, + "loginZeroTier": {Func: rpcLoginZeroTier, Params: []string{"networkID"}}, + "logoutZeroTier": {Func: rpcLogoutZeroTier, Params: []string{"networkID"}}, + "getZeroTierSettings": {Func: rpcGetZeroTierSettings}, + "setUpdateSource": {Func: rpcSetUpdateSource, Params: []string{"source"}}, + "getAudioMode": {Func: rpcGetAudioMode}, + "setAudioMode": {Func: rpcSetAudioMode, Params: []string{"mode"}}, + "startFrpc": {Func: rpcStartFrpc, Params: []string{"frpcToml"}}, + "stopFrpc": {Func: rpcStopFrpc}, + "getFrpcStatus": {Func: rpcGetFrpcStatus}, + "getFrpcToml": {Func: rpcGetFrpcToml}, + "getFrpcLog": {Func: rpcGetFrpcLog}, + "startEasyTier": {Func: rpcStartEasyTier, Params: []string{"name", "secret", "node"}}, + "stopEasyTier": {Func: rpcStopEasyTier}, + "getEasyTierStatus": {Func: rpcGetEasyTierStatus}, + "getEasyTierConfig": {Func: rpcGetEasyTierConfig}, + "getEasyTierLog": {Func: rpcGetEasyTierLog}, + "startVnt": {Func: rpcStartVnt, Params: []string{"config_mode", "token", "device_id", "name", "server_addr", "config_file", "model", "password"}}, + "stopVnt": {Func: rpcStopVnt}, + "getVntStatus": {Func: rpcGetVntStatus}, + "getVntConfig": {Func: rpcGetVntConfig}, + "getVntConfigFile": {Func: rpcGetVntConfigFile}, + "getVntLog": {Func: rpcGetVntLog}, + "getVntInfo": {Func: rpcGetVntInfo}, + "getEasyTierNodeInfo": {Func: rpcGetEasyTierNodeInfo}, + "startCloudflared": {Func: rpcStartCloudflared, Params: []string{"token"}}, + "stopCloudflared": {Func: rpcStopCloudflared}, + "getCloudflaredStatus": {Func: rpcGetCloudflaredStatus}, + "getCloudflaredLog": {Func: rpcGetCloudflaredLog}, + "getStreamEncodecType": {Func: rpcGetStreamEncodecType}, + "setStreamEncodecType": {Func: rpcSetStreamEncodecType, Params: []string{"encodecType"}}, + "setNpuAppStatus": {Func: rpcSetNpuAppStatus, Params: []string{"enable"}}, + "getNpuAppStatus": {Func: rpcGetNpuAppStatus}, + "startWireguard": {Func: rpcStartWireguard, Params: []string{"configFile"}}, + "stopWireguard": {Func: rpcStopWireguard}, + "getWireguardStatus": {Func: rpcGetWireguardStatus}, + "getWireguardConfig": {Func: rpcGetWireguardConfig}, + "getWireguardLog": {Func: rpcGetWireguardLog}, + "getWireguardInfo": {Func: rpcGetWireguardInfo}, } diff --git a/main.go b/main.go index 42ac58f..21f14ef 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "syscall" "time" + "github.com/Masterminds/semver/v3" "github.com/gwatts/rootcerts" ) @@ -26,14 +27,19 @@ func Main() { logger.Warn().Err(err).Msg("failed to get local version") } + minRequiredSystemVersion := semver.MustParse("0.1.4") + isNewEnoughSystem := systemVersionLocal != nil && !systemVersionLocal.LessThan(minRequiredSystemVersion) + logger.Info(). Interface("system_version", systemVersionLocal). Interface("app_version", appVersionLocal). Msg("starting KVM") - //go runWatchdog() + go runWatchdog() go confirmCurrentSystem() //A/B system - go setForceHpd() + if isNewEnoughSystem { + go setForceHpd() + } http.DefaultClient.Timeout = 1 * time.Minute @@ -74,6 +80,22 @@ func Main() { // Initialize native video socket server StartVideoDataSocketServer() + // Set up callbacks for HTTP video stream subscribers + // When first HTTP subscriber connects and there's no WebRTC session, start video + videoBroadcaster.onFirstSubscribe = func() { + if actionSessions == 0 { + logger.Info().Msg("First HTTP video subscriber connected, starting video stream") + _ = writeCtrlAction("start_video") + } + } + // When last HTTP subscriber disconnects and there's no WebRTC session, stop video + videoBroadcaster.onLastUnsubscribe = func() { + if actionSessions == 0 { + logger.Info().Msg("Last HTTP video subscriber disconnected, stopping video stream") + _ = writeCtrlAction("stop_video") + } + } + // Initialize native audio socket server StartAudioCtrlSocketServer() @@ -109,29 +131,31 @@ func Main() { } }() - // initialize usb gadget - initUsbGadget() + if isNewEnoughSystem { + // initialize usb gadget + initUsbGadget() + + if err := setInitialVirtualMediaState(); err != nil { + logger.Warn().Err(err).Msg("failed to set initial virtual media state") + } + + if err := initImagesFolder(); err != nil { + logger.Warn().Err(err).Msg("failed to init images folder") + } + initJiggler() + + initSystemInfo() + } // initialize GPIO initGPIO() - if err := setInitialVirtualMediaState(); err != nil { - logger.Warn().Err(err).Msg("failed to set initial virtual media state") - } - - if err := initImagesFolder(); err != nil { - logger.Warn().Err(err).Msg("failed to init images folder") - } - initJiggler() - // initialize display initDisplay() // Initialize VPN initVPN() - initSystemInfo() - //Auto update //go func() { // time.Sleep(15 * time.Minute) diff --git a/native.go b/native.go index f87af3a..d7d485e 100644 --- a/native.go +++ b/native.go @@ -132,12 +132,14 @@ func StartVideoSocketServer(socketPath string, handleClient func(net.Conn), isCt go func() { for { + scopedLogger.Debug().Msg("waiting for client connection") conn, err := listener.Accept() if err != nil { scopedLogger.Warn().Err(err).Msg("failed to accept socket") continue } + scopedLogger.Info().Str("remote_addr", conn.RemoteAddr().String()).Msg("new client connection accepted") if isCtrl { // check if the channel is closed select { @@ -238,6 +240,12 @@ func handleVideoClient(conn net.Conn) { now := time.Now() sinceLastFrame := now.Sub(lastFrame) lastFrame = now + + // Broadcast to HTTP clients + dataCopy := make([]byte, n) + copy(dataCopy, inboundPacket[:n]) + videoBroadcaster.Broadcast(dataCopy) + if currentSession != nil { err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { diff --git a/ota.go b/ota.go index be2f9f1..d638f1f 100644 --- a/ota.go +++ b/ota.go @@ -1,6 +1,7 @@ package kvm import ( + "archive/zip" "bytes" "context" "crypto/sha256" @@ -10,8 +11,11 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/exec" + "path/filepath" + "regexp" "strings" "time" @@ -39,6 +43,7 @@ type RemoteMetadata struct { AppUrl string `json:"appUrl"` AppHash string `json:"appHash"` SystemUrl string `json:"systemUrl"` + SystemHash string `json:"systemHash,omitempty"` SystemVersion string `json:"systemVersion"` } @@ -53,17 +58,55 @@ type UpdateStatus struct { Error string `json:"error,omitempty"` } -var UpdateMetadataUrls = []string{ - "https://api.github.com/repos/LuckfoxTECH/PicoKVM/releases/latest", +var UpdateGithubAppReleaseUrls = []string{ "https://api.github.com/repos/LuckfoxTECH/kvm/releases/latest", + "https://api.github.com/repos/LuckfoxTECH/kvm_app/releases/latest", "https://api.github.com/repos/luckfox-eng29/kvm/releases/latest", + "https://api.github.com/repos/luckfox-eng29/kvm_app/releases/latest", } -var builtAppVersion = "0.0.4+dev" +var UpdateGiteeAppReleaseUrls = []string{ + "https://gitee.com/api/v5/repos/LuckfoxTECH/kvm/releases/latest", + "https://gitee.com/api/v5/repos/LuckfoxTECH/kvm_app/releases/latest", + "https://gitee.com/api/v5/repos/luckfox-eng29/kvm/releases/latest", + "https://gitee.com/api/v5/repos/luckfox-eng29/kvm_app/releases/latest", +} + +var UpdateGithubSystemReleaseUrls = []string{ + "https://api.github.com/repos/LuckfoxTECH/kvm_system/releases/latest", + "https://api.github.com/repos/luckfox-eng29/kvm_system/releases/latest", +} + +var UpdateGiteeSystemReleaseUrls = []string{ + "https://gitee.com/api/v5/repos/LuckfoxTECH/kvm_system/releases/latest", + "https://gitee.com/api/v5/repos/luckfox-eng29/kvm_system/releases/latest", +} + +var UpdateGiteeSystemZipUrls = []string{ + "https://gitee.com/LuckfoxTECH/kvm_system/archive/refs/tags/", + "https://gitee.com/luckfox-eng29/kvm_system/archive/refs/tags/", +} + +const cdnUpdateBaseURL = "https://cdn.picokvm.top/luckfox_picokvm_firmware/lastest/" + +var builtAppVersion = "0.1.1+dev" var updateSource = "github" +var customUpdateBaseURL string + +const ( + updateSourceGithub = "github" + updateSourceGitee = "gitee" + updateSourceCDN = "cdn" + updateSourceCustom = "custom" +) func rpcSetUpdateSource(source string) error { + switch source { + case updateSourceGithub, updateSourceGitee, updateSourceCDN, updateSourceCustom: + default: + return fmt.Errorf("invalid update source: %s", source) + } updateSource = source return nil } @@ -88,68 +131,545 @@ func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Versio } func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*RemoteMetadata, error) { - //cmd := exec.Command("curl", "-s", UpdateMetadataUrl) - //output, err := cmd.Output() - //if err != nil { - // return nil, fmt.Errorf("failed to fetch GitHub releases: %w", err) - //} - //_ = cmd.Process.Release() + if updateSource == updateSourceCDN || updateSource == updateSourceCustom { + baseURL := cdnUpdateBaseURL + if updateSource == updateSourceCustom { + if strings.TrimSpace(customUpdateBaseURL) == "" { + return nil, fmt.Errorf("custom update base URL is not set") + } + baseURL = customUpdateBaseURL + } + return fetchUpdateMetadataFromBaseURL(ctx, baseURL) + } + + _, _ = deviceId, includePreRelease + + appVersionRemote, appURL, appSha256, err := fetchKvmAppLatestRelease(ctx) + if err != nil { + return nil, err + } + + systemVersionRemote, systemZipURL, err := fetchKvmSystemLatestRelease(ctx) + if err != nil { + return nil, err + } + + return &RemoteMetadata{ + AppUrl: appURL, + AppVersion: appVersionRemote, + AppHash: appSha256, + SystemUrl: systemZipURL, + SystemVersion: systemVersionRemote, + }, nil +} + +func fetchKvmAppLatestRelease(ctx context.Context) (tag string, downloadURL string, sha256 string, err error) { + apiURLs := UpdateGithubAppReleaseUrls + fallbackToGithub := false + if updateSource == updateSourceGitee { + apiURLs = UpdateGiteeAppReleaseUrls + fallbackToGithub = true + } + + tryFetch := func(urls []string) (string, string, string, error) { + var lastErr error + for _, apiURL := range urls { + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + lastErr = fmt.Errorf("failed to create release request for %s: %w", apiURL, err) + continue + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to fetch release from %s: %w", apiURL, err) + continue + } + + output, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + lastErr = fmt.Errorf("failed to read release response from %s: %w", apiURL, readErr) + continue + } + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf( + "failed to fetch release from %s: status %d: %s", + apiURL, + resp.StatusCode, + strings.TrimSpace(string(output)), + ) + continue + } + + var release struct { + TagName string `json:"tag_name"` + Assets []releaseAsset `json:"assets"` + } + if err := json.Unmarshal(output, &release); err != nil { + lastErr = fmt.Errorf("failed to parse releases JSON from %s: %w", apiURL, err) + continue + } + + tag := strings.TrimSpace(release.TagName) + if tag == "" { + lastErr = fmt.Errorf("empty tag_name from %s", apiURL) + continue + } + + var downloadURL string + var sha256 string + if len(release.Assets) > 0 { + downloadURL = release.Assets[0].BrowserDownloadURL + sha256 = release.Assets[0].Digest + } + sha256 = strings.TrimPrefix(strings.TrimSpace(sha256), "sha256:") + + if strings.TrimSpace(downloadURL) == "" { + lastErr = fmt.Errorf("empty app download url from %s", apiURL) + continue + } + + return tag, downloadURL, sha256, nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no app release API URLs configured") + } + return "", "", "", lastErr + } + var lastErr error + tag, downloadURL, sha256, err = tryFetch(apiURLs) + if err == nil { + return tag, downloadURL, sha256, nil + } - for _, url := range UpdateMetadataUrls { - resp, err := http.Get(url) - if err != nil { - lastErr = fmt.Errorf("failed to fetch GitHub releases from %s: %w", url, err) + lastErr = err + if updateSource == updateSourceGitee && fallbackToGithub { + tag, downloadURL, sha256, err = tryFetch(UpdateGithubAppReleaseUrls) + if err == nil { + downloadURL = strings.Replace(downloadURL, "github.com", "gitee.com", 1) + return tag, downloadURL, sha256, nil + } + lastErr = fmt.Errorf("gitee app release fetch failed (%v); github fallback failed (%w)", lastErr, err) + } + return "", "", "", lastErr +} + +type releaseAsset struct { + BrowserDownloadURL string `json:"browser_download_url"` + Name string `json:"name"` + Digest string `json:"digest"` +} + +func pickZipAssetURL(assets []releaseAsset) string { + for _, a := range assets { + u := strings.TrimSpace(a.BrowserDownloadURL) + if u == "" { continue } - defer resp.Body.Close() + name := strings.ToLower(strings.TrimSpace(a.Name)) + if strings.HasSuffix(name, ".zip") || strings.HasSuffix(strings.ToLower(u), ".zip") { + return u + } + } + if len(assets) == 1 { + return strings.TrimSpace(assets[0].BrowserDownloadURL) + } + return "" +} - output, err := io.ReadAll(resp.Body) +func fetchKvmSystemLatestRelease(ctx context.Context) (tag string, zipURL string, err error) { + apiURLs := UpdateGithubSystemReleaseUrls + fallbackToGithub := false + if updateSource == updateSourceGitee { + apiURLs = UpdateGiteeSystemReleaseUrls + fallbackToGithub = true + } + + var lastErr error + for _, apiURL := range apiURLs { + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { - lastErr = fmt.Errorf("failed to read GitHub releases from %s: %w", url, err) + lastErr = fmt.Errorf("error creating system release request: %w", err) continue } - if strings.Contains(string(output), "404") { - lastErr = fmt.Errorf("failed to find release from %s: %w", url, err) + resp, err := http.DefaultClient.Do(req) + if err != nil { + lastErr = fmt.Errorf("error fetching system release: %w", err) + continue + } + + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + lastErr = fmt.Errorf("error reading system release response: %w", readErr) + continue + } + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf( + "unexpected status code fetching system release from %s: %d, %s", + apiURL, + resp.StatusCode, + strings.TrimSpace(string(body)), + ) continue } var release struct { - TagName string `json:"tag_name"` - Assets []struct { - BrowserDownloadURL string `json:"browser_download_url"` - Digest string `json:"digest"` - } `json:"assets"` + TagName string `json:"tag_name"` + ZipballURL string `json:"zipball_url"` + Assets []releaseAsset `json:"assets"` } - if err := json.Unmarshal(output, &release); err != nil { - lastErr = fmt.Errorf("failed to parse GitHub releases JSON from %s: %w", url, err) + if err := json.Unmarshal(body, &release); err != nil { + lastErr = fmt.Errorf("error parsing system release JSON from %s: %w", apiURL, err) continue } - appVersionRemote := release.TagName - - var updateUrl string - var appSha256 string - if len(release.Assets) > 0 { - updateUrl = release.Assets[0].BrowserDownloadURL - appSha256 = release.Assets[0].Digest + tag := strings.TrimSpace(release.TagName) + if tag == "" { + lastErr = fmt.Errorf("empty system tag_name from %s", apiURL) + continue } - appSha256 = strings.TrimPrefix(appSha256, "sha256:") - - remoteMetadata := &RemoteMetadata{ - AppUrl: updateUrl, - AppVersion: appVersionRemote, - AppHash: appSha256, - SystemUrl: "", - SystemVersion: "0.1.0", + if u := pickZipAssetURL(release.Assets); strings.TrimSpace(u) != "" { + return tag, strings.TrimSpace(u), nil + } + if strings.TrimSpace(release.ZipballURL) != "" { + return tag, strings.TrimSpace(release.ZipballURL), nil } - return remoteMetadata, nil + lastErr = fmt.Errorf("no usable system archive url in release response from %s", apiURL) + continue } - return nil, lastErr + if lastErr == nil { + lastErr = fmt.Errorf("no system release API URLs configured") + } + if updateSource == updateSourceGitee && fallbackToGithub { + var githubErr error + var githubTag string + var githubZipURL string + for i, apiURL := range UpdateGithubSystemReleaseUrls { + githubTag, githubZipURL, githubErr = func(apiURL string) (string, string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return "", "", fmt.Errorf("error creating system release request: %w", err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", "", fmt.Errorf("error fetching system release: %w", err) + } + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + return "", "", fmt.Errorf("error reading system release response: %w", readErr) + } + if resp.StatusCode != http.StatusOK { + return "", "", fmt.Errorf( + "unexpected status code fetching system release from %s: %d, %s", + apiURL, + resp.StatusCode, + strings.TrimSpace(string(body)), + ) + } + var release struct { + TagName string `json:"tag_name"` + ZipballURL string `json:"zipball_url"` + Assets []releaseAsset `json:"assets"` + } + if err := json.Unmarshal(body, &release); err != nil { + return "", "", fmt.Errorf("error parsing system release JSON from %s: %w", apiURL, err) + } + tag := strings.TrimSpace(release.TagName) + if tag == "" { + return "", "", fmt.Errorf("empty system tag_name from %s", apiURL) + } + if u := pickZipAssetURL(release.Assets); strings.TrimSpace(u) != "" { + return tag, strings.TrimSpace(u), nil + } + if strings.TrimSpace(release.ZipballURL) != "" { + return tag, strings.TrimSpace(release.ZipballURL), nil + } + return "", "", fmt.Errorf("no usable system archive url in release response from %s", apiURL) + }(apiURL) + if githubErr == nil && strings.TrimSpace(githubTag) != "" { + _ = githubZipURL + selectedZipURL := "" + if i < len(UpdateGiteeSystemZipUrls) { + selectedZipURL = UpdateGiteeSystemZipUrls[i] + } else if len(UpdateGiteeSystemZipUrls) > 0 { + selectedZipURL = UpdateGiteeSystemZipUrls[0] + } + if strings.TrimSpace(selectedZipURL) != "" { + zipTag := strings.TrimSpace(githubTag) + if v, parseErr := semver.NewVersion(zipTag); parseErr == nil && v != nil { + zipTag = v.String() + } else { + zipTag = strings.TrimPrefix(zipTag, "v") + zipTag = strings.TrimPrefix(zipTag, "V") + } + zipURL := strings.TrimRight(selectedZipURL, "/") + "/" + zipTag + ".zip" + return githubTag, zipURL, nil + } + githubErr = fmt.Errorf("no gitee system zip urls configured") + break + } + } + return "", "", fmt.Errorf("gitee system release fetch failed (%v); github fallback failed (%w)", lastErr, githubErr) + } + return "", "", lastErr +} + +func fetchUpdateMetadataFromBaseURL(ctx context.Context, baseURL string) (*RemoteMetadata, error) { + baseURL = normalizeBaseURL(baseURL) + versionURL, err := resolveURL(baseURL, "version.txt") + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "GET", versionURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + client := http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: rootcerts.ServerCertPool(), + }, + }, + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error fetching version.txt: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code fetching version.txt: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading version.txt: %w", err) + } + + appVersion, systemVersion, err := parseVersionTxt(string(body)) + if err != nil { + return nil, err + } + + appURL, err := resolveURL(baseURL, "kvm_app") + if err != nil { + return nil, err + } + + appHash, err := fetchFirstSHA256FromBaseURL(ctx, baseURL, []string{"kvm_app.sha2565", "kvm_app.sha256"}) + if err != nil { + return nil, err + } + + systemURL, err := resolveURL(baseURL, "update_system.zip") + if err != nil { + return nil, err + } + systemHash, err := fetchFirstSHA256FromBaseURL(ctx, baseURL, []string{"update_system.zip.sha2565", "update_system.zip.sha256"}) + if err != nil { + var urlErr error + systemURL, urlErr = resolveURL(baseURL, "update_system.tar") + if urlErr != nil { + return nil, err + } + var hashErr error + systemHash, hashErr = fetchFirstSHA256FromBaseURL(ctx, baseURL, []string{"update_system.tar.sha256"}) + if hashErr != nil { + return nil, err + } + } + + return &RemoteMetadata{ + AppVersion: appVersion, + AppUrl: appURL, + AppHash: appHash, + SystemVersion: systemVersion, + SystemUrl: systemURL, + SystemHash: systemHash, + }, nil +} + +func extractUpdateSystemTarFromZip(zipPath string, tarPath string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("failed to open update_system.zip: %w", err) + } + defer r.Close() + + var tarFile *zip.File + for _, f := range r.File { + if strings.TrimSpace(f.Name) == "" { + continue + } + if filepath.Base(f.Name) == "update_system.tar" { + tarFile = f + break + } + } + if tarFile == nil { + return fmt.Errorf("update_system.tar not found in %s", zipPath) + } + + rc, err := tarFile.Open() + if err != nil { + return fmt.Errorf("failed to open update_system.tar in zip: %w", err) + } + defer rc.Close() + + tmpPath := tarPath + ".tmp" + _ = os.Remove(tmpPath) + out, err := os.Create(tmpPath) + if err != nil { + return fmt.Errorf("failed to create %s: %w", tmpPath, err) + } + _, copyErr := io.Copy(out, rc) + closeErr := out.Close() + if copyErr != nil { + _ = os.Remove(tmpPath) + return fmt.Errorf("failed to extract update_system.tar: %w", copyErr) + } + if closeErr != nil { + _ = os.Remove(tmpPath) + return fmt.Errorf("failed to close %s: %w", tmpPath, closeErr) + } + + _ = os.Remove(tarPath) + if err := os.Rename(tmpPath, tarPath); err != nil { + _ = os.Remove(tmpPath) + return fmt.Errorf("failed to move extracted tar: %w", err) + } + return nil +} + +func fetchFirstSHA256FromBaseURL(ctx context.Context, baseURL string, candidates []string) (string, error) { + var lastErr error + for _, name := range candidates { + u, err := resolveURL(baseURL, name) + if err != nil { + lastErr = err + continue + } + hash, err := fetchSHA256FromURL(ctx, u) + if err == nil { + return hash, nil + } + lastErr = err + } + if lastErr == nil { + lastErr = fmt.Errorf("no sha256 candidates provided") + } + return "", lastErr +} + +func fetchSHA256FromURL(ctx context.Context, shaURL string) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", shaURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %w", err) + } + + client := http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: rootcerts.ServerCertPool(), + }, + }, + } + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error fetching sha256 file: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code fetching sha256 file: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading sha256 file: %w", err) + } + + hash, err := parseSHA256Text(string(body)) + if err != nil { + return "", fmt.Errorf("invalid sha256 file content: %w", err) + } + + return hash, nil +} + +func parseSHA256Text(s string) (string, error) { + re := regexp.MustCompile(`(?i)\b([a-f0-9]{64})\b`) + match := re.FindStringSubmatch(s) + if len(match) < 2 { + return "", fmt.Errorf("no sha256 hash found") + } + hash := strings.ToLower(strings.TrimSpace(match[1])) + hash = strings.TrimPrefix(hash, "sha256:") + return hash, nil +} + +func normalizeBaseURL(baseURL string) string { + s := strings.TrimSpace(baseURL) + if s == "" { + return s + } + if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") { + s = "https://" + s + } + if !strings.HasSuffix(s, "/") { + s += "/" + } + return s +} + +func resolveURL(baseURL string, path string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("invalid base URL: %w", err) + } + ref, err := url.Parse(path) + if err != nil { + return "", fmt.Errorf("invalid URL path: %w", err) + } + return u.ResolveReference(ref).String(), nil +} + +func parseVersionTxt(s string) (appVersion string, systemVersion string, err error) { + reApp := regexp.MustCompile(`(?i)\bAppVersion\s*:\s*([0-9A-Za-z.\-+v]+)\b`) + reSys := regexp.MustCompile(`(?i)\bSystemVersion\s*:\s*([0-9A-Za-z.\-+v]+)\b`) + + appMatch := reApp.FindStringSubmatch(s) + sysMatch := reSys.FindStringSubmatch(s) + + if len(appMatch) < 2 || len(sysMatch) < 2 { + return "", "", fmt.Errorf("invalid version.txt format") + } + + appVersion = strings.TrimSpace(appMatch[1]) + systemVersion = strings.TrimSpace(sysMatch[1]) + + return appVersion, systemVersion, nil } func downloadFile(ctx context.Context, path string, url string, downloadProgress *float32) error { @@ -158,6 +678,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress // return fmt.Errorf("error removing existing file: %w", err) // } //} + otaLogger.Info().Str("path", path).Str("url", url).Msg("downloading file") unverifiedPath := path + ".unverified" if _, err := os.Stat(unverifiedPath); err == nil { @@ -172,10 +693,6 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress } defer file.Close() - if updateSource == "gitee" { - url = strings.Replace(url, "github", "gitee", 1) - } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("error creating request: %w", err) @@ -184,6 +701,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress client := http.Client{ Timeout: 10 * time.Minute, Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSHandshakeTimeout: 30 * time.Second, TLSClientConfig: &tls.Config{ RootCAs: rootcerts.ServerCertPool(), @@ -202,11 +720,12 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress } totalSize := resp.ContentLength - if totalSize <= 0 { - return fmt.Errorf("invalid content length") - } + hasKnownSize := totalSize > 0 var written int64 + var lastProgressBytes int64 + lastProgressAt := time.Now() + lastReportedProgress := float32(0) buf := make([]byte, 32*1024) for { nr, er := resp.Body.Read(buf) @@ -219,10 +738,31 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress if ew != nil { return fmt.Errorf("error writing to file: %w", ew) } - progress := float32(written) / float32(totalSize) - if progress-*downloadProgress >= 0.01 { - *downloadProgress = progress - triggerOTAStateUpdate() + if hasKnownSize && downloadProgress != nil { + progress := float32(written) / float32(totalSize) + if progress-lastReportedProgress >= 0.001 || time.Since(lastProgressAt) >= 1*time.Second { + lastReportedProgress = progress + *downloadProgress = lastReportedProgress + triggerOTAStateUpdate() + lastProgressAt = time.Now() + } + } + if !hasKnownSize && downloadProgress != nil { + if *downloadProgress <= 0 { + *downloadProgress = 0.01 + triggerOTAStateUpdate() + lastProgressBytes = written + } else if written-lastProgressBytes >= 1024*1024 { + next := *downloadProgress + 0.01 + if next > 0.99 { + next = 0.99 + } + if next-*downloadProgress >= 0.01 { + *downloadProgress = next + triggerOTAStateUpdate() + lastProgressBytes = written + } + } } } if er != nil { @@ -233,18 +773,27 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress } } + if hasKnownSize && written != totalSize { + return fmt.Errorf("incomplete download: wrote %d bytes, expected %d bytes", written, totalSize) + } + + if downloadProgress != nil && !hasKnownSize { + *downloadProgress = 1 + triggerOTAStateUpdate() + } + file.Close() // Flush filesystem buffers to ensure all data is written to disk err = exec.Command("sync").Run() if err != nil { - return fmt.Errorf("error flushing filesystem buffers: %w", err) + otaLogger.Warn().Err(err).Msg("Failed to flush filesystem buffers") } // Clear the filesystem caches to force a read from disk err = os.WriteFile("/proc/sys/vm/drop_caches", []byte("1"), 0644) if err != nil { - return fmt.Errorf("error clearing filesystem caches: %w", err) + otaLogger.Warn().Err(err).Msg("Failed to clear filesystem caches") } // without check @@ -259,12 +808,216 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return nil } +func prepareSystemUpdateTarFromKvmSystemZip( + ctx context.Context, + zipURL string, + outputTarPath string, + downloadProgress *float32, + verificationProgress *float32, + scopedLogger *zerolog.Logger, +) error { + if scopedLogger == nil { + scopedLogger = otaLogger + } + + baseDir := "/userdata/picokvm" + workDir := filepath.Join(baseDir, "kvm_system_work") + extractDir := filepath.Join(workDir, "extract") + zipPath := filepath.Join(workDir, "master.zip") + + if err := os.MkdirAll(workDir, 0755); err != nil { + return fmt.Errorf("error creating work dir: %w", err) + } + + if err := os.RemoveAll(extractDir); err != nil { + return fmt.Errorf("error cleaning extract dir: %w", err) + } + if err := os.MkdirAll(extractDir, 0755); err != nil { + return fmt.Errorf("error creating extract dir: %w", err) + } + + if verificationProgress != nil { + *verificationProgress = 0 + triggerOTAStateUpdate() + } + + maxAttempts := 3 + var lastErr error + for attempt := 1; attempt <= maxAttempts; attempt++ { + if downloadProgress != nil { + *downloadProgress = 0 + triggerOTAStateUpdate() + } + + if err := downloadFile(ctx, zipPath, zipURL, downloadProgress); err != nil { + lastErr = err + } else { + zipUnverifiedPath := zipPath + ".unverified" + if _, err := os.Stat(zipUnverifiedPath); err != nil { + lastErr = fmt.Errorf("downloaded zip not found: %s: %w", zipUnverifiedPath, err) + } else { + if err := unzipArchive(zipUnverifiedPath, extractDir); err != nil { + lastErr = err + } else { + lastErr = nil + break + } + } + } + + _ = os.Remove(zipPath + ".unverified") + _ = os.RemoveAll(extractDir) + _ = os.MkdirAll(extractDir, 0755) + if attempt < maxAttempts { + time.Sleep(time.Duration(attempt*2) * time.Second) + } + } + if lastErr != nil { + return lastErr + } + + extractedRoot := filepath.Join(extractDir, "kvm_system-master") + if _, err := os.Stat(extractedRoot); err != nil { + entries, readErr := os.ReadDir(extractDir) + if readErr != nil { + return fmt.Errorf("error reading extracted dir: %w", readErr) + } + found := "" + for _, entry := range entries { + if entry.IsDir() { + found = filepath.Join(extractDir, entry.Name()) + break + } + } + if found == "" { + return fmt.Errorf("unable to find extracted root dir in %s", extractDir) + } + extractedRoot = found + } + + scriptPath := filepath.Join(extractedRoot, "split_and_check_md5.sh") + if _, err := os.Stat(scriptPath); err != nil { + return fmt.Errorf("split_and_check_md5.sh not found: %w", err) + } + if err := os.Chmod(scriptPath, 0755); err != nil { + return fmt.Errorf("error chmod split_and_check_md5.sh: %w", err) + } + + var out bytes.Buffer + cmd := exec.Command(scriptPath, "merge", "update_system.tar") + cmd.Dir = extractedRoot + cmd.Stdout = &out + cmd.Stderr = &out + if err := cmd.Run(); err != nil { + out.Reset() + cmd2 := exec.Command("/bin/sh", scriptPath, "merge", "update_system.tar") + cmd2.Dir = extractedRoot + cmd2.Stdout = &out + cmd2.Stderr = &out + if err2 := cmd2.Run(); err2 != nil { + return fmt.Errorf("error merging split system tar: %w / %w\nOutput: %s", err, err2, out.String()) + } + } + + tarSourcePath := filepath.Join(extractedRoot, "update_system.tar") + if _, err := os.Stat(tarSourcePath); err != nil { + return fmt.Errorf("merged tar not found: %s: %w\nOutput: %s", tarSourcePath, err, out.String()) + } + + if err := os.RemoveAll(outputTarPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error removing existing system tar: %w", err) + } + if err := os.Rename(tarSourcePath, outputTarPath); err != nil { + return fmt.Errorf("error moving merged tar into place: %w", err) + } + + if verificationProgress != nil { + *verificationProgress = 1 + triggerOTAStateUpdate() + } + + if err := os.RemoveAll(extractDir); err != nil { + scopedLogger.Warn().Err(err).Str("path", extractDir).Msg("Failed to cleanup extracted system zip") + } + zipUnverifiedPath := zipPath + ".unverified" + if err := os.Remove(zipUnverifiedPath); err != nil { + scopedLogger.Warn().Err(err).Str("path", zipUnverifiedPath).Msg("Failed to cleanup system zip") + } + + return nil +} + +func unzipArchive(zipPath string, destDir string) error { + reader, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("error opening zip: %w", err) + } + defer reader.Close() + + destClean := filepath.Clean(destDir) + string(os.PathSeparator) + + for _, file := range reader.File { + targetPath := filepath.Join(destDir, file.Name) + cleanTargetPath := filepath.Clean(targetPath) + if !strings.HasPrefix(cleanTargetPath, destClean) { + return fmt.Errorf("invalid zip path: %s", file.Name) + } + + if file.FileInfo().IsDir() { + if err := os.MkdirAll(cleanTargetPath, 0755); err != nil { + return fmt.Errorf("error creating dir: %w", err) + } + continue + } + + if err := os.MkdirAll(filepath.Dir(cleanTargetPath), 0755); err != nil { + return fmt.Errorf("error creating dir: %w", err) + } + + rc, err := file.Open() + if err != nil { + return fmt.Errorf("error opening zipped file: %w", err) + } + + outFile, err := os.OpenFile(cleanTargetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + rc.Close() + return fmt.Errorf("error creating file: %w", err) + } + + _, copyErr := io.Copy(outFile, rc) + closeErr := outFile.Close() + rcErr := rc.Close() + if copyErr != nil { + return fmt.Errorf("error extracting file: %w", copyErr) + } + if closeErr != nil { + return fmt.Errorf("error closing extracted file: %w", closeErr) + } + if rcErr != nil { + return fmt.Errorf("error closing zip entry: %w", rcErr) + } + } + + return nil +} + func verifyFile(path string, expectedHash string, verifyProgress *float32, scopedLogger *zerolog.Logger) error { if scopedLogger == nil { scopedLogger = otaLogger } unverifiedPath := path + ".unverified" + if strings.TrimSpace(expectedHash) == "" { + if err := os.Rename(unverifiedPath, path); err != nil { + return fmt.Errorf("error renaming file: %w", err) + } + if err := os.Chmod(path, 0755); err != nil { + return fmt.Errorf("error making file executable: %w", err) + } + return nil + } + fileToHash, err := os.Open(unverifiedPath) if err != nil { return fmt.Errorf("error opening file for hashing: %w", err) @@ -356,6 +1109,25 @@ func triggerOTAStateUpdate() { }() } +func cleanupUpdateTempFiles(logger *zerolog.Logger) { + paths := []string{ + "/userdata/picokvm/bin/kvm_app.unverified", + "/userdata/picokvm/update_system.tar.unverified", + "/userdata/picokvm/update_system.tar", + "/userdata/picokvm/kvm_system_work", + } + + for _, p := range paths { + if err := os.RemoveAll(p); err != nil && !os.IsNotExist(err) { + if logger != nil { + logger.Warn().Err(err).Str("path", p).Msg("failed to cleanup temp update file") + } else { + otaLogger.Warn().Err(err).Str("path", p).Msg("failed to cleanup temp update file") + } + } + } +} + func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { scopedLogger := otaLogger.With(). Str("deviceId", deviceId). @@ -367,6 +1139,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err return fmt.Errorf("update already in progress") } + cleanupUpdateTempFiles(&scopedLogger) + otaState = OTAState{ Updating: true, } @@ -446,12 +1220,46 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err Str("remote", remote.SystemVersion). Msg("System update available") - err := downloadFile(ctx, "/userdata/picokvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) - if err != nil { - otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) - scopedLogger.Error().Err(err).Msg("Error downloading system update") - triggerOTAStateUpdate() - return err + systemTarPath := "/userdata/picokvm/update_system.tar" + if updateSource == updateSourceGithub || updateSource == updateSourceGitee { + err := prepareSystemUpdateTarFromKvmSystemZip( + ctx, + remote.SystemUrl, + systemTarPath, + &otaState.SystemDownloadProgress, + &otaState.SystemVerificationProgress, + &scopedLogger, + ) + if err != nil { + otaState.Error = fmt.Sprintf("Error preparing system update: %v", err) + scopedLogger.Error().Err(err).Msg("Error preparing system update") + triggerOTAStateUpdate() + return err + } + } else { + systemZipPath := "/userdata/picokvm/update_system.zip" + err := downloadFile(ctx, systemZipPath, remote.SystemUrl, &otaState.SystemDownloadProgress) + if err != nil { + otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) + scopedLogger.Error().Err(err).Msg("Error downloading system update") + triggerOTAStateUpdate() + return err + } + + err = verifyFile(systemZipPath, remote.SystemHash, &otaState.SystemVerificationProgress, &scopedLogger) + if err != nil { + otaState.Error = fmt.Sprintf("Error preparing system update archive: %v", err) + scopedLogger.Error().Err(err).Msg("Error preparing system update archive") + triggerOTAStateUpdate() + return err + } + + if err := extractUpdateSystemTarFromZip(systemZipPath, systemTarPath); err != nil { + otaState.Error = fmt.Sprintf("Error extracting system update tar: %v", err) + scopedLogger.Error().Err(err).Msg("Error extracting system update tar") + triggerOTAStateUpdate() + return err + } } downloadFinished := time.Now() otaState.SystemDownloadFinishedAt = &downloadFinished @@ -465,7 +1273,14 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err triggerOTAStateUpdate() scopedLogger.Info().Msg("Starting rk_ota command") - cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/picokvm/update_system.tar", "--save_dir=/userdata/picokvm/ota_save", "--partition=all") + if _, statErr := os.Stat(systemTarPath); statErr != nil { + otaState.Error = fmt.Sprintf("System update archive not found: %s (%v)", systemTarPath, statErr) + scopedLogger.Error().Err(statErr).Str("path", systemTarPath).Msg("System update archive missing") + triggerOTAStateUpdate() + return fmt.Errorf("system update archive not found: %s: %w", systemTarPath, statErr) + } + + cmd := exec.Command("rk_ota", "--misc=update", "--tar_path="+systemTarPath, "--save_dir=/userdata/picokvm/ota_save", "--partition=all") var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b @@ -473,6 +1288,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err) scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") + triggerOTAStateUpdate() return fmt.Errorf("error starting rk_ota command: %w", err) } ctx, cancel := context.WithCancel(context.Background()) @@ -509,11 +1325,13 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err Str("output", output). Int("exitCode", cmd.ProcessState.ExitCode()). Msg("Error executing rk_ota command") + triggerOTAStateUpdate() return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) } scopedLogger.Info().Str("output", output).Msg("rk_ota success") + updatedAt := time.Now() otaState.SystemUpdateProgress = 1 - otaState.SystemUpdatedAt = &verifyFinished + otaState.SystemUpdatedAt = &updatedAt triggerOTAStateUpdate() rebootNeeded = true } else { @@ -521,6 +1339,13 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err } if rebootNeeded { + configPath := "/userdata/kvm_config.json" + if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { + scopedLogger.Warn().Err(err).Str("path", configPath).Msg("failed to delete config before reboot") + } else { + scopedLogger.Info().Str("path", configPath).Msg("deleted config before reboot") + } + scopedLogger.Info().Msg("System Rebooting in 10s") time.Sleep(10 * time.Second) cmd := exec.Command("reboot") diff --git a/ratelimit.go b/ratelimit.go new file mode 100644 index 0000000..69ba8e0 --- /dev/null +++ b/ratelimit.go @@ -0,0 +1,95 @@ +package kvm + +import ( + "sync" + "time" +) + +type RateLimitInfo struct { + Failures int + BlockUntil time.Time + PenaltySeconds int + LastSeen time.Time +} + +var ( + ipRateLimits = make(map[string]*RateLimitInfo) + ipRateLimitsMu sync.Mutex +) + +const ( + MaxFailures = 5 + BasePenalty = 10 * 60 // 10 minutes in seconds + CleanupInterval = 1 * time.Hour + RecordExpiration = 24 * time.Hour +) + +func init() { + go func() { + for { + time.Sleep(CleanupInterval) + cleanupRateLimits() + } + }() +} + +func cleanupRateLimits() { + ipRateLimitsMu.Lock() + defer ipRateLimitsMu.Unlock() + + now := time.Now() + for ip, info := range ipRateLimits { + if now.Sub(info.LastSeen) > RecordExpiration { + delete(ipRateLimits, ip) + } + } +} + +// CheckRateLimit checks if the IP is allowed to attempt login. +// Returns allowed (bool) and waitDuration (time.Duration) if blocked. +func CheckRateLimit(ip string) (bool, time.Duration) { + ipRateLimitsMu.Lock() + defer ipRateLimitsMu.Unlock() + + info, exists := ipRateLimits[ip] + if !exists { + return true, 0 + } + + if time.Now().Before(info.BlockUntil) { + return false, info.BlockUntil.Sub(time.Now()) + } + + return true, 0 +} + +// RecordFailure records a failed login attempt for the IP. +func RecordFailure(ip string) { + ipRateLimitsMu.Lock() + defer ipRateLimitsMu.Unlock() + + info, exists := ipRateLimits[ip] + if !exists { + info = &RateLimitInfo{ + PenaltySeconds: BasePenalty, + } + ipRateLimits[ip] = info + } + + info.LastSeen = time.Now() + info.Failures++ + + if info.Failures >= MaxFailures { + info.BlockUntil = time.Now().Add(time.Duration(info.PenaltySeconds) * time.Second) + + // Extend penalty for next time. Doubling the penalty. + info.PenaltySeconds *= 2 + } +} + +// RecordSuccess resets the rate limit for the IP. +func RecordSuccess(ip string) { + ipRateLimitsMu.Lock() + defer ipRateLimitsMu.Unlock() + delete(ipRateLimits, ip) +} diff --git a/serial.go b/serial.go index e6930fa..1cb38ff 100644 --- a/serial.go +++ b/serial.go @@ -1,8 +1,12 @@ package kvm import ( + "bytes" + "encoding/json" "io" + "github.com/coder/websocket" + "github.com/gin-gonic/gin" "github.com/pion/webrtc/v4" "go.bug.st/serial" ) @@ -95,3 +99,79 @@ func handleSerialChannel(d *webrtc.DataChannel) { scopedLogger.Info().Msg("Serial channel closed") }) } + +func handleSerialWS(c *gin.Context) { + source := c.ClientIP() + scopedLogger := serialLogger.With(). + Str("transport", "websocket"). + Str("source", source). + Logger() + + wsCon, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{ + InsecureSkipVerify: true, + }) + if err != nil { + c.Status(500) + return + } + defer wsCon.Close(websocket.StatusNormalClosure, "") + + if err := reopenSerialPort(); err != nil { + scopedLogger.Error().Err(err).Msg("Failed to open serial port") + wsCon.Close(websocket.StatusInternalError, "") + return + } + + ctx := c.Request.Context() + + done := make(chan struct{}) + go func() { + defer close(done) + buf := make([]byte, 1024) + for { + if port == nil { + return + } + n, readErr := port.Read(buf) + if readErr != nil { + if readErr != io.EOF { + scopedLogger.Warn().Err(readErr).Msg("Failed to read from serial port") + } + return + } + if writeErr := wsCon.Write(ctx, websocket.MessageBinary, buf[:n]); writeErr != nil { + return + } + } + }() + + for { + msgType, data, readErr := wsCon.Read(ctx) + if readErr != nil { + break + } + + if msgType == websocket.MessageText { + maybeJson := bytes.TrimSpace(data) + if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' { + var size TerminalSize + if err := json.Unmarshal(maybeJson, &size); err == nil { + continue + } + } + } + + if port == nil { + continue + } + if _, err := port.Write(data); err != nil { + break + } + } + + if port != nil { + port.Close() + port = nil + } + <-done +} diff --git a/stream_broadcaster.go b/stream_broadcaster.go new file mode 100644 index 0000000..32b52d5 --- /dev/null +++ b/stream_broadcaster.go @@ -0,0 +1,59 @@ +package kvm + +import ( + "sync" + + "github.com/google/uuid" +) + +type VideoBroadcaster struct { + subscribers map[string]chan []byte + lock sync.RWMutex + onFirstSubscribe func() + onLastUnsubscribe func() +} + +var videoBroadcaster = &VideoBroadcaster{ + subscribers: make(map[string]chan []byte), +} + +func (b *VideoBroadcaster) Subscribe() (string, chan []byte) { + b.lock.Lock() + defer b.lock.Unlock() + id := uuid.New().String() + // Buffer a bit to avoid dropping frames too easily, + // but not too much to avoid latency build-up + ch := make(chan []byte, 200) + wasEmpty := len(b.subscribers) == 0 + b.subscribers[id] = ch + if wasEmpty && b.onFirstSubscribe != nil { + b.onFirstSubscribe() + } + return id, ch +} + +func (b *VideoBroadcaster) Unsubscribe(id string) { + b.lock.Lock() + defer b.lock.Unlock() + if ch, ok := b.subscribers[id]; ok { + close(ch) + delete(b.subscribers, id) + if len(b.subscribers) == 0 && b.onLastUnsubscribe != nil { + b.onLastUnsubscribe() + } + } +} + +func (b *VideoBroadcaster) Broadcast(data []byte) { + b.lock.RLock() + defer b.lock.RUnlock() + for _, ch := range b.subscribers { + // Non-blocking send + select { + case ch <- data: + default: + // Drop frame if channel is full to avoid blocking other subscribers + // Ideally we should have a ring buffer or similar, but this is simple + } + } +} diff --git a/terminal.go b/terminal.go index e06e5cd..46684ed 100644 --- a/terminal.go +++ b/terminal.go @@ -7,7 +7,9 @@ import ( "os" "os/exec" + "github.com/coder/websocket" "github.com/creack/pty" + "github.com/gin-gonic/gin" "github.com/pion/webrtc/v4" ) @@ -94,3 +96,80 @@ func handleTerminalChannel(d *webrtc.DataChannel) { scopedLogger.Warn().Err(err).Msg("Terminal channel error") }) } + +func handleTerminalWS(c *gin.Context) { + source := c.ClientIP() + scopedLogger := terminalLogger.With(). + Str("transport", "websocket"). + Str("source", source). + Logger() + + wsCon, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{ + InsecureSkipVerify: true, + }) + if err != nil { + c.Status(500) + return + } + defer wsCon.Close(websocket.StatusNormalClosure, "") + + cmd := exec.Command("/bin/sh") + ptmx, err := pty.Start(cmd) + if err != nil { + scopedLogger.Warn().Err(err).Msg("Failed to start pty") + wsCon.Close(websocket.StatusInternalError, "") + return + } + + ctx := c.Request.Context() + + done := make(chan struct{}) + go func() { + defer close(done) + buf := make([]byte, 1024) + for { + n, readErr := ptmx.Read(buf) + if readErr != nil { + if readErr != io.EOF { + scopedLogger.Warn().Err(readErr).Msg("Failed to read from pty") + } + return + } + if writeErr := wsCon.Write(ctx, websocket.MessageBinary, buf[:n]); writeErr != nil { + return + } + } + }() + + for { + msgType, data, readErr := wsCon.Read(ctx) + if readErr != nil { + break + } + + if msgType == websocket.MessageText { + maybeJson := bytes.TrimSpace(data) + if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' { + var size TerminalSize + if err := json.Unmarshal(maybeJson, &size); err == nil { + if err := pty.Setsize(ptmx, &pty.Winsize{ + Rows: uint16(size.Rows), + Cols: uint16(size.Cols), + }); err == nil { + continue + } + } + } + } + + if _, err := ptmx.Write(data); err != nil { + break + } + } + + _ = ptmx.Close() + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + <-done +} diff --git a/ui/.gitignore b/ui/.gitignore index a547bf3..7e445ac 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +.vite dist dist-ssr *.local diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs index a6c0c1f..e275002 100644 --- a/ui/eslint.config.cjs +++ b/ui/eslint.config.cjs @@ -58,14 +58,22 @@ module.exports = defineConfig([{ }, rules: { - "react-refresh/only-export-components": ["warn", { - allowConstantExport: true, + "react-refresh/only-export-components": "off", + + "react/prop-types": "off", + + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + + "react-hooks/exhaustive-deps": "off", + + "import/no-unresolved": ["error", { + ignore: [ + "\\.svg\\?react$", + ], }], - "import/order": ["error", { - groups: ["builtin", "external", "internal", "parent", "sibling"], - "newlines-between": "always", - }], + "import/order": "off", }, settings: { diff --git a/ui/index.html b/ui/index.html index 6629bdc..449e5c2 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,29 +2,9 @@ - + - - - + KVM @@ -49,7 +29,7 @@
diff --git a/ui/package-lock.json b/ui/package-lock.json index 746ae69..a227bbc 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,27 +18,34 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", + "antd": "^5.27.6", + "antd-style": "^3.7.1", "cva": "^1.0.0-beta.3", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.3", "framer-motion": "^12.11.4", + "jmuxer": "^2.1.0", "lodash.throttle": "^4.1.1", "lucide-react": "^0.522.0", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", "react-animate-height": "^3.2.3", + "react-device-detect": "^2.2.3", "react-dom": "^19.1.0", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", + "react-lazylog": "^4.5.3", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.8.72", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", + "styled-components": "^6.1.19", "tailwind-merge": "^3.3.0", "usehooks-ts": "^3.1.1", "validator": "^13.15.0", + "w-touch": "^2.0.0", "zustand": "^4.5.2" }, "devDependencies": { @@ -49,9 +56,11 @@ "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.7", - "@types/react": "^19.1.4", + "@types/jmuxer": "^2.0.7", + "@types/react": "^19.2.2", "@types/react-dom": "^19.1.5", "@types/semver": "^7.7.0", + "@types/styled-components": "^5.1.35", "@types/validator": "^13.15.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", @@ -71,6 +80,7 @@ "tailwindcss": "^4.1.7", "typescript": "^5.8.3", "vite": "^6.3.5", + "vite-plugin-svgr": "^4.5.0", "vite-tsconfig-paths": "^5.1.4" }, "engines": { @@ -104,6 +114,321 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", @@ -113,6 +438,241 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmmirror.com/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -843,35 +1403,30 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -881,20 +1436,31 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mattiasbuelens/web-streams-polyfill": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz", + "integrity": "sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg==", + "deprecated": "moved to web-streams-polyfill@2.0.0", + "license": "MIT", + "dependencies": { + "@types/whatwg-streams": "^0.0.7" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -933,6 +1499,154 @@ "node": ">= 8" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.0.1.tgz", + "integrity": "sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.0.tgz", + "integrity": "sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@react-aria/focus": { "version": "3.20.3", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.3.tgz", @@ -1031,14 +1745,50 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", @@ -1305,6 +2055,231 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "license": "MIT" }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@swc/core": { "version": "1.11.24", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", @@ -1956,6 +2931,29 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/jmuxer": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/jmuxer/-/jmuxer-2.0.7.tgz", + "integrity": "sha512-foRDAEs52drqYKy18t3haRB6UrozeXV1n6U0sEQqT+cmdbTWtsX/2dcLxCZQxjmAN/ey9gaw1OZPZfHmOXUhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1968,10 +2966,26 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "version": "19.2.2", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1993,6 +3007,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/styled-components": { + "version": "5.1.35", + "resolved": "https://registry.npmmirror.com/@types/styled-components/-/styled-components-5.1.35.tgz", + "integrity": "sha512-JeYII52nSFGXGaw/5Odf0TBUhT3024HduBewrZCQBoUFKBw8V6x1dbnZCpgJuzmiokWAlVo3kkS3k3jrEK1NyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", @@ -2000,6 +3032,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/whatwg-streams": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz", + "integrity": "sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", @@ -2380,6 +3418,91 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.27.6", + "resolved": "https://registry.npmmirror.com/antd/-/antd-5.27.6.tgz", + "integrity": "sha512-70HrjVbzDXvtiUQ5MP1XdNudr/wGAk9Ivaemk6f36yrAeJurJSmZ8KngOIilolLRHdGuNc6/Vk+4T1OZpSjpag==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.1", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.2", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/antd-style": { + "version": "3.7.1", + "resolved": "https://registry.npmmirror.com/antd-style/-/antd-style-3.7.1.tgz", + "integrity": "sha512-CQOfddVp4aOvBfCepa+Kj2e7ap+2XBINg1Kn2osdE3oQvrD7KJu/K0sfnLcFLkgCJygbxmuazYdWLKb+drPDYA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.24.1", + "@emotion/cache": "^11.11.0", + "@emotion/css": "^11.11.2", + "@emotion/react": "^11.11.4", + "@emotion/serialize": "^1.1.3", + "@emotion/utils": "^1.2.1", + "use-merge-value": "^1.2.0" + }, + "peerDependencies": { + "antd": ">=5.8.1", + "react": ">=18" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2600,6 +3723,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2718,10 +3881,32 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -2765,6 +3950,12 @@ "node": ">=18" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -2821,12 +4012,61 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2841,6 +4081,26 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3153,6 +4413,17 @@ "csstype": "^3.0.2" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3209,6 +4480,28 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -3809,6 +5102,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3891,6 +5191,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-readablestream": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz", + "integrity": "sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3916,6 +5222,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4084,6 +5396,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -4335,6 +5657,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/i18n-auto-extractor": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/i18n-auto-extractor/-/i18n-auto-extractor-1.2.3.tgz", @@ -4389,6 +5720,15 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4454,6 +5794,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -4858,6 +6204,11 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jmuxer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jmuxer/-/jmuxer-2.1.0.tgz", + "integrity": "sha512-iizwBTIV11RFKrOp0s/SKrb00yz2epwSOdWxdphSfV7gWlAi9ZXpDdNk/m67Dp0M3+4uGL0AcBQmhB2THxABpQ==" + }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -4878,9 +6229,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4889,12 +6240,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4907,6 +6276,15 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -5186,6 +6564,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5202,9 +6586,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.castarray": { @@ -5294,6 +6678,33 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.522.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz", @@ -5400,9 +6811,9 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -5412,21 +6823,11 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "license": "MIT" }, "node_modules/motion-dom": { "version": "12.12.1", @@ -5473,6 +6874,17 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -5764,6 +7176,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5788,6 +7218,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5862,7 +7301,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -6010,6 +7448,618 @@ ], "license": "MIT" }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmmirror.com/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.9.2", + "resolved": "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -6032,6 +8082,19 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -6076,13 +8139,39 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-lazylog": { + "version": "4.5.3", + "resolved": "https://registry.npmmirror.com/react-lazylog/-/react-lazylog-4.5.3.tgz", + "integrity": "sha512-lyov32A/4BqihgXgtNXTHCajXSXkYHPlIEmV8RbYjHIMxCFSnmtdg4kDCI3vATz7dURtiFTvrw5yonHnrS+NNg==", + "license": "MPL-2.0", + "dependencies": { + "@mattiasbuelens/web-streams-polyfill": "^0.2.0", + "fetch-readablestream": "^0.2.0", + "immutable": "^3.8.2", + "mitt": "^1.1.2", + "prop-types": "^15.6.1", + "react-string-replace": "^0.4.1", + "react-virtualized": "^9.21.0", + "text-encoding-utf-8": "^1.0.1", + "whatwg-fetch": "^2.0.4" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -6092,13 +8181,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -6133,6 +8222,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-string-replace": { + "version": "0.4.4", + "resolved": "https://registry.npmmirror.com/react-string-replace/-/react-string-replace-0.4.4.tgz", + "integrity": "sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -6155,6 +8256,33 @@ "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", "license": "MIT" }, + "node_modules/react-virtualized": { + "version": "9.22.6", + "resolved": "https://registry.npmmirror.com/react-virtualized/-/react-virtualized-9.22.6.tgz", + "integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-xtermjs": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.10.tgz", @@ -6244,6 +8372,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6422,6 +8556,15 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -6481,6 +8624,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6587,6 +8736,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6609,6 +8778,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -6785,6 +8960,86 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmmirror.com/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6809,6 +9064,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -6842,23 +9104,36 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { "node": ">=18" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -6920,6 +9195,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -7072,6 +9353,32 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmmirror.com/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -7090,6 +9397,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -7130,6 +9444,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-merge-value": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/use-merge-value/-/use-merge-value-1.2.0.tgz", + "integrity": "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.x" + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", @@ -7162,9 +9485,9 @@ "license": "MIT" }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -7193,9 +9516,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -7266,6 +9589,21 @@ } } }, + "node_modules/vite-plugin-svgr": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz", + "integrity": "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.2.0", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=2.6.0" + } + }, "node_modules/vite-tsconfig-paths": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", @@ -7312,6 +9650,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w-touch": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/w-touch/-/w-touch-2.0.0.tgz", + "integrity": "sha512-PYnngF+KHzZRBFI6qsm5PHwVNO5Lx3dYDrvNv//Ei9fvnYlaOWr9sKAriD/crlshWee9ZPsyvDMA4UnoR1LUHw==", + "license": "MIT" + }, + "node_modules/whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7431,6 +9781,20 @@ "node": ">=18" } }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index dc63733..67569c0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,7 @@ "node": "22.15.0" }, "scripts": { - "dev": "./dev_device.sh", + "dev": "dev_device.bat 192.168.0.105", "dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:cloud": "vite dev --mode=cloud-development", "build": "npm run build:prod", @@ -29,27 +29,34 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", + "antd": "^5.27.6", + "antd-style": "^3.7.1", "cva": "^1.0.0-beta.3", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.3", "framer-motion": "^12.11.4", + "jmuxer": "^2.1.0", "lodash.throttle": "^4.1.1", "lucide-react": "^0.522.0", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", "react-animate-height": "^3.2.3", + "react-device-detect": "^2.2.3", "react-dom": "^19.1.0", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", + "react-lazylog": "^4.5.3", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.8.72", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", + "styled-components": "^6.1.19", "tailwind-merge": "^3.3.0", "usehooks-ts": "^3.1.1", "validator": "^13.15.0", + "w-touch": "^2.0.0", "zustand": "^4.5.2" }, "devDependencies": { @@ -60,9 +67,11 @@ "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.7", - "@types/react": "^19.1.4", + "@types/jmuxer": "^2.0.7", + "@types/react": "^19.2.2", "@types/react-dom": "^19.1.5", "@types/semver": "^7.7.0", + "@types/styled-components": "^5.1.35", "@types/validator": "^13.15.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", @@ -82,6 +91,7 @@ "tailwindcss": "^4.1.7", "typescript": "^5.8.3", "vite": "^6.3.5", + "vite-plugin-svgr": "^4.5.0", "vite-tsconfig-paths": "^5.1.4" } } diff --git a/ui/public/sse.html b/ui/public/sse.html deleted file mode 120000 index 0a8b4f3..0000000 --- a/ui/public/sse.html +++ /dev/null @@ -1 +0,0 @@ -../../internal/logging/sse.html \ No newline at end of file diff --git a/ui/public/sse.html b/ui/public/sse.html new file mode 100644 index 0000000..0a8b4f3 --- /dev/null +++ b/ui/public/sse.html @@ -0,0 +1 @@ +../../internal/logging/sse.html \ No newline at end of file diff --git a/ui/src/assets/second/ConnectStatsPressed.svg b/ui/src/assets/second/ConnectStatsPressed.svg new file mode 100644 index 0000000..cf22ca1 --- /dev/null +++ b/ui/src/assets/second/ConnectStatsPressed.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/ConnectStatsSelected.svg b/ui/src/assets/second/ConnectStatsSelected.svg new file mode 100644 index 0000000..7a7d08d --- /dev/null +++ b/ui/src/assets/second/ConnectStatsSelected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/DisabledPressed.svg b/ui/src/assets/second/DisabledPressed.svg new file mode 100644 index 0000000..949e101 --- /dev/null +++ b/ui/src/assets/second/DisabledPressed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/IMG.svg b/ui/src/assets/second/IMG.svg new file mode 100644 index 0000000..e18cd6a --- /dev/null +++ b/ui/src/assets/second/IMG.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/KeyboardPressed.svg b/ui/src/assets/second/KeyboardPressed.svg new file mode 100644 index 0000000..927ec52 --- /dev/null +++ b/ui/src/assets/second/KeyboardPressed.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/KeyboardSelected.svg b/ui/src/assets/second/KeyboardSelected.svg new file mode 100644 index 0000000..ab54ca1 --- /dev/null +++ b/ui/src/assets/second/KeyboardSelected.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/MTPPressed.svg b/ui/src/assets/second/MTPPressed.svg new file mode 100644 index 0000000..6702ab0 --- /dev/null +++ b/ui/src/assets/second/MTPPressed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/MingLing.svg b/ui/src/assets/second/MingLing.svg new file mode 100644 index 0000000..64d680e --- /dev/null +++ b/ui/src/assets/second/MingLing.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/MousePressed.svg b/ui/src/assets/second/MousePressed.svg new file mode 100644 index 0000000..c98ba50 --- /dev/null +++ b/ui/src/assets/second/MousePressed.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/MouseSelected.svg b/ui/src/assets/second/MouseSelected.svg new file mode 100644 index 0000000..fb5626a --- /dev/null +++ b/ui/src/assets/second/MouseSelected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/SharedFoldersPressed.svg b/ui/src/assets/second/SharedFoldersPressed.svg new file mode 100644 index 0000000..2d62b17 --- /dev/null +++ b/ui/src/assets/second/SharedFoldersPressed.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/UAC.svg b/ui/src/assets/second/UAC.svg new file mode 100644 index 0000000..ecd7563 --- /dev/null +++ b/ui/src/assets/second/UAC.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/UACPressed.svg b/ui/src/assets/second/UACPressed.svg new file mode 100644 index 0000000..ea21758 --- /dev/null +++ b/ui/src/assets/second/UACPressed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/UACSelected.svg b/ui/src/assets/second/UACSelected.svg new file mode 100644 index 0000000..b03e880 --- /dev/null +++ b/ui/src/assets/second/UACSelected.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/VideoPressed.svg b/ui/src/assets/second/VideoPressed.svg new file mode 100644 index 0000000..7f6fc21 --- /dev/null +++ b/ui/src/assets/second/VideoPressed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/VideoSelected.svg b/ui/src/assets/second/VideoSelected.svg new file mode 100644 index 0000000..864f9b0 --- /dev/null +++ b/ui/src/assets/second/VideoSelected.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/VirtualStoragePressed.svg b/ui/src/assets/second/VirtualStoragePressed.svg new file mode 100644 index 0000000..0027388 --- /dev/null +++ b/ui/src/assets/second/VirtualStoragePressed.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/VirtualStorageSelected.svg b/ui/src/assets/second/VirtualStorageSelected.svg new file mode 100644 index 0000000..086e3e4 --- /dev/null +++ b/ui/src/assets/second/VirtualStorageSelected.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/access.svg b/ui/src/assets/second/access.svg new file mode 100644 index 0000000..c3963f0 --- /dev/null +++ b/ui/src/assets/second/access.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/assets/second/advanced.svg b/ui/src/assets/second/advanced.svg new file mode 100644 index 0000000..74b9ed1 --- /dev/null +++ b/ui/src/assets/second/advanced.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/second/copy.svg b/ui/src/assets/second/copy.svg new file mode 100644 index 0000000..2b57b73 --- /dev/null +++ b/ui/src/assets/second/copy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/delete.svg b/ui/src/assets/second/delete.svg new file mode 100644 index 0000000..790dc01 --- /dev/null +++ b/ui/src/assets/second/delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/down.svg b/ui/src/assets/second/down.svg new file mode 100644 index 0000000..8f8028d --- /dev/null +++ b/ui/src/assets/second/down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/dwon.svg b/ui/src/assets/second/dwon.svg new file mode 100644 index 0000000..f247767 --- /dev/null +++ b/ui/src/assets/second/dwon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/dwon2.svg b/ui/src/assets/second/dwon2.svg new file mode 100644 index 0000000..6a87315 --- /dev/null +++ b/ui/src/assets/second/dwon2.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/float_button1.svg b/ui/src/assets/second/float_button1.svg new file mode 100644 index 0000000..aee3dda --- /dev/null +++ b/ui/src/assets/second/float_button1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/second/float_button2.svg b/ui/src/assets/second/float_button2.svg new file mode 100644 index 0000000..ad10500 --- /dev/null +++ b/ui/src/assets/second/float_button2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/general.svg b/ui/src/assets/second/general.svg new file mode 100644 index 0000000..ffed84e --- /dev/null +++ b/ui/src/assets/second/general.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/gobottom.svg b/ui/src/assets/second/gobottom.svg new file mode 100644 index 0000000..9e4e552 --- /dev/null +++ b/ui/src/assets/second/gobottom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/hardware.svg b/ui/src/assets/second/hardware.svg new file mode 100644 index 0000000..11fda8c --- /dev/null +++ b/ui/src/assets/second/hardware.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/hdmi-cord.svg b/ui/src/assets/second/hdmi-cord.svg new file mode 100644 index 0000000..1efc6f5 --- /dev/null +++ b/ui/src/assets/second/hdmi-cord.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ui/src/assets/second/hdml.svg b/ui/src/assets/second/hdml.svg new file mode 100644 index 0000000..ef7e228 --- /dev/null +++ b/ui/src/assets/second/hdml.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/hdml2.svg b/ui/src/assets/second/hdml2.svg new file mode 100644 index 0000000..52deadd --- /dev/null +++ b/ui/src/assets/second/hdml2.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/keyboard.svg b/ui/src/assets/second/keyboard.svg new file mode 100644 index 0000000..7c1e240 --- /dev/null +++ b/ui/src/assets/second/keyboard.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/keyboard2.svg b/ui/src/assets/second/keyboard2.svg new file mode 100644 index 0000000..8268540 --- /dev/null +++ b/ui/src/assets/second/keyboard2.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/second/left.svg b/ui/src/assets/second/left.svg new file mode 100644 index 0000000..a3e59ac --- /dev/null +++ b/ui/src/assets/second/left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/second/media.svg b/ui/src/assets/second/media.svg new file mode 100644 index 0000000..69ea05e --- /dev/null +++ b/ui/src/assets/second/media.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/minglingdarkac.svg b/ui/src/assets/second/minglingdarkac.svg new file mode 100644 index 0000000..0f66090 --- /dev/null +++ b/ui/src/assets/second/minglingdarkac.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/mouse.svg b/ui/src/assets/second/mouse.svg new file mode 100644 index 0000000..b7ee459 --- /dev/null +++ b/ui/src/assets/second/mouse.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/network.svg b/ui/src/assets/second/network.svg new file mode 100644 index 0000000..84c8f6c --- /dev/null +++ b/ui/src/assets/second/network.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/noSD.svg b/ui/src/assets/second/noSD.svg new file mode 100644 index 0000000..7e24361 --- /dev/null +++ b/ui/src/assets/second/noSD.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/open.svg b/ui/src/assets/second/open.svg new file mode 100644 index 0000000..ae2c84e --- /dev/null +++ b/ui/src/assets/second/open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/refresh.svg b/ui/src/assets/second/refresh.svg new file mode 100644 index 0000000..140d4b7 --- /dev/null +++ b/ui/src/assets/second/refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/assets/second/rname.py b/ui/src/assets/second/rname.py new file mode 100644 index 0000000..9d8d62f --- /dev/null +++ b/ui/src/assets/second/rname.py @@ -0,0 +1,161 @@ +import os +import re + +def translate_chinese_to_english(text): + """ + 将特定的中文关键词翻译成英文 + """ + translation_dict = { + '类型': 'Type', + '状态': 'State', + '按下': 'Pressed', + '选中': 'Selected', + '键盘': 'Keyboard', + '鼠标': 'Mouse', + '虚拟存储': 'VirtualStorage', + '禁用': 'Disabled', + '连接统计': 'ConnectStats', + '共享文件夹': 'SharedFolders', + '视频': 'Video', + 'MTP': 'MTP', + 'UAC': 'UAC' + } + + for chinese, english in translation_dict.items(): + text = text.replace(chinese, english) + + return text + +def clean_filename(filename): + """ + 清理文件名:翻译中文关键词,移除空格、等号、逗号,并去除Type和State文字 + """ + # 分离文件名和扩展名[1,5](@ref) + name, ext = os.path.splitext(filename) + + # 1. 翻译中文关键词 + translated_name = translate_chinese_to_english(name) + + # 2. 使用正则表达式移除所有空格、等号、逗号[7](@ref) + cleaned_name = re.sub(r'[\s=,]+', '', translated_name) + + # 3. 去除Type和State文字(新增功能) + cleaned_name = cleaned_name.replace('Type', '').replace('State', '') + + return cleaned_name + ext + +def rename_files(directory_path='.'): + """ + 重命名指定目录下的所有文件[4,6](@ref) + """ + print(f"正在处理目录: {os.path.abspath(directory_path)}") + + try: + files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] + except FileNotFoundError: + print(f"错误:目录 '{directory_path}' 不存在。") + return + except PermissionError: + print(f"错误:没有权限访问目录 '{directory_path}'。") + return + + if not files: + print("指定目录中没有文件。") + return + + renamed_count = 0 + print("\n开始重命名...") + + for filename in files: + old_name = filename + new_name = clean_filename(old_name) + + # 只有当文件名确实发生变化时才进行重命名 + if old_name != new_name: + old_path = os.path.join(directory_path, old_name) + new_path = os.path.join(directory_path, new_name) + + # 检查新文件名是否已存在,避免覆盖 + if os.path.exists(new_path): + print(f"警告:目标文件已存在,跳过 {old_name} -> {new_name}") + continue + + try: + os.rename(old_path, new_path) + print(f"✓ 重命名: {old_name} -> {new_name}") + renamed_count += 1 + except OSError as e: + print(f"✗ 重命名失败 {old_name}: {e}") + + print(f"\n完成!成功重命名了 {renamed_count} 个文件。") + +def preview_renames(directory_path='.'): + """ + 预览重命名效果,而不实际执行重命名操作[4](@ref) + """ + print("=== 预览重命名效果(非实际执行)===") + print(f"预览目录: {os.path.abspath(directory_path)}") + print("-" * 60) + + try: + files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] + except Exception as e: + print(f"无法读取目录: {e}") + return + + if not files: + print("目录中没有文件可预览。") + return + + change_count = 0 + for filename in files: + new_filename = clean_filename(filename) + if filename != new_filename: + print(f"原文件名: {filename}") + print(f"新文件名: {new_filename}") + print("-" * 40) + change_count += 1 + + if change_count == 0: + print("未发现需要重命名的文件(所有文件名已符合规则)。") + else: + print(f"预览结束。共有 {change_count} 个文件将被重命名。") + +# 使用示例 +if __name__ == "__main__": + # 1. 先预览重命名效果(推荐) + print("=== 预览模式 ===") + preview_renames() # 处理当前目录 + + # 2. 测试您提供的示例文件名转换效果 + print("\n=== 示例文件名转换效果 ===") + example_files = [ + "类型=Connect Stats,状态=按下.svg", + "类型=Connect Stats,状态=选中.svg", + "类型=Disabled,状态=按下.svg", + "类型=MTP,状态=按下.svg", + "类型=Shared Folders,状态=按下.svg", + "类型=UAC,状态=按下.svg", + "类型=UAC,状态=选中.svg", + "类型=Video,状态=按下.svg", + "类型=Video,状态=选中.svg", + "类型=键盘,状态=按下.svg", + "类型=键盘,状态=选中.svg", + "类型=鼠标,状态=按下.svg", + "类型=鼠标,状态=选中.svg", + "类型=虚拟存储,状态=按下.svg", + "类型=虚拟存储,状态=选中.svg" + ] + + for old_name in example_files: + new_name = clean_filename(old_name) + print(f"{old_name}") + print(f"-> {new_name}") + print() + + # 3. 实际执行重命名(取消注释以下代码来执行) + + print("\n=== 执行重命名 ===") + # 确认预览结果无误后,取消下面的注释来执行重命名 + rename_files() # 重命名当前目录的文件 + diff --git a/ui/src/assets/second/set1.svg b/ui/src/assets/second/set1.svg new file mode 100644 index 0000000..f666958 --- /dev/null +++ b/ui/src/assets/second/set1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/set2.svg b/ui/src/assets/second/set2.svg new file mode 100644 index 0000000..466c5ae --- /dev/null +++ b/ui/src/assets/second/set2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/shuaxin.svg b/ui/src/assets/second/shuaxin.svg new file mode 100644 index 0000000..2e7e8d5 --- /dev/null +++ b/ui/src/assets/second/shuaxin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/assets/second/state.svg b/ui/src/assets/second/state.svg new file mode 100644 index 0000000..ba20ac2 --- /dev/null +++ b/ui/src/assets/second/state.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/swich_dir2.svg b/ui/src/assets/second/swich_dir2.svg new file mode 100644 index 0000000..a36ddf8 --- /dev/null +++ b/ui/src/assets/second/swich_dir2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/swich_dri1.svg b/ui/src/assets/second/swich_dri1.svg new file mode 100644 index 0000000..01e2fb0 --- /dev/null +++ b/ui/src/assets/second/swich_dri1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/tiaozhuan.svg b/ui/src/assets/second/tiaozhuan.svg new file mode 100644 index 0000000..c00b2ca --- /dev/null +++ b/ui/src/assets/second/tiaozhuan.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/to_down.svg b/ui/src/assets/second/to_down.svg new file mode 100644 index 0000000..7116e65 --- /dev/null +++ b/ui/src/assets/second/to_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/to_up.svg b/ui/src/assets/second/to_up.svg new file mode 100644 index 0000000..148c0ab --- /dev/null +++ b/ui/src/assets/second/to_up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/up.svg b/ui/src/assets/second/up.svg new file mode 100644 index 0000000..56cb836 --- /dev/null +++ b/ui/src/assets/second/up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/second/upload.svg b/ui/src/assets/second/upload.svg new file mode 100644 index 0000000..e2ce436 --- /dev/null +++ b/ui/src/assets/second/upload.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/usb.svg b/ui/src/assets/second/usb.svg new file mode 100644 index 0000000..8acea6c --- /dev/null +++ b/ui/src/assets/second/usb.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui/src/assets/second/usb2.svg b/ui/src/assets/second/usb2.svg new file mode 100644 index 0000000..82a6abd --- /dev/null +++ b/ui/src/assets/second/usb2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui/src/assets/second/vedio.svg b/ui/src/assets/second/vedio.svg new file mode 100644 index 0000000..dbfdc0e --- /dev/null +++ b/ui/src/assets/second/vedio.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/vedio2.svg b/ui/src/assets/second/vedio2.svg new file mode 100644 index 0000000..532ceef --- /dev/null +++ b/ui/src/assets/second/vedio2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/src/assets/second/version.svg b/ui/src/assets/second/version.svg new file mode 100644 index 0000000..5a838db --- /dev/null +++ b/ui/src/assets/second/version.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/xinhao.svg b/ui/src/assets/second/xinhao.svg new file mode 100644 index 0000000..7d03b1c --- /dev/null +++ b/ui/src/assets/second/xinhao.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/src/assets/second/zhongduan.svg b/ui/src/assets/second/zhongduan.svg new file mode 100644 index 0000000..b41aa6c --- /dev/null +++ b/ui/src/assets/second/zhongduan.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/second/zhongduan2.svg b/ui/src/assets/second/zhongduan2.svg new file mode 100644 index 0000000..bb5673e --- /dev/null +++ b/ui/src/assets/second/zhongduan2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx deleted file mode 100644 index d7deec6..0000000 --- a/ui/src/components/ActionBar.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { MdOutlineContentPasteGo } from "react-icons/md"; -import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; -import { FaKeyboard } from "react-icons/fa6"; -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import { Fragment, useCallback, useRef, useState, useEffect } from "react"; -import { CommandLineIcon } from "@heroicons/react/20/solid"; - -import { Button } from "@components/Button"; -import { - useHidStore, - useMountMediaStore, - useSettingsStore, - useUiStore, - useAudioModeStore, -} from "@/hooks/stores"; -import Container from "@components/Container"; -import { cx } from "@/cva.config"; -import PasteModal from "@/components/popovers/PasteModal"; -import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; -import MountPopopover from "@/components/popovers/MountPopover"; -import ExtensionPopover from "@/components/popovers/ExtensionPopover"; -import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; -import VolumeControl from "./VolumeControl"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; -import {useReactAt} from 'i18n-auto-extractor/react' - -export default function Actionbar({ - requestFullscreen, -}: { - requestFullscreen: () => Promise; -}) { - const { navigateTo } = useDeviceUiNavigation(); - const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); - - const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); - const toggleSidebarView = useUiStore(state => state.toggleSidebarView); - const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - const terminalType = useUiStore(state => state.terminalType); - const setTerminalType = useUiStore(state => state.setTerminalType); - const remoteVirtualMediaState = useMountMediaStore( - state => state.remoteVirtualMediaState, - ); - const developerMode = useSettingsStore(state => state.developerMode); - - // Audio related - const [send] = useJsonRpc(); - const audioMode = useAudioModeStore(state => state.audioMode); - const setAudioMode = useAudioModeStore(state => state.setAudioMode); - const { $at }= useReactAt(); - - // This is the only way to get a reliable state change for the popover - // at time of writing this there is no mount, or unmount event for the popover - const isOpen = useRef(false); - const checkIfStateChanged = useCallback( - (open: boolean) => { - if (open !== isOpen.current) { - isOpen.current = open; - if (!open) { - setTimeout(() => { - setDisableFocusTrap(false); - console.log("Popover is closing. Returning focus trap to video"); - }, 0); - } - } - }, - [setDisableFocusTrap], - ); - - useEffect(() => { - send("getAudioMode", {}, resp => { - if ("error" in resp) return; - setAudioMode(String(resp.result)); - }); - }, [send]); - - return ( - -
e.stopPropagation()} - onKeyDown={e => e.stopPropagation()} - className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-1.5" - > -
-
-
-
- - {(audioMode !== "disabled") && ( -
- -
- )} - -
- -
- - -
-
-
- {/* {useSettingsStore().actionBarCtrlAltDel && ( -
-
- )} */} -
-
- -
-
-
-
- -
- ); -} diff --git a/ui/src/components/AdaptiveContainer.tsx b/ui/src/components/AdaptiveContainer.tsx new file mode 100644 index 0000000..c5e5226 --- /dev/null +++ b/ui/src/components/AdaptiveContainer.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; + +interface AdaptiveContainerProps { + children: React.ReactNode; + minHeight?: number | string; + onKeyboardShow?: () => void; + onKeyboardHide?: () => void; + style?: React.CSSProperties; + differenceRange?: number; +} + +const AdaptiveContainer: React.FC = ({ + children, + minHeight = 0, + onKeyboardShow, + onKeyboardHide, + style = {}, + differenceRange = 50 + }) => { + const [containerHeight, setContainerHeight] = useState('100%'); + const [isKeyboardVisible, setIsKeyboardVisible] = useState(false); + const originalHeight = useRef(0); + const containerRef = useRef(null); + + const initHeight = useCallback(() => { + originalHeight.current = document.documentElement.clientHeight || document.body.clientHeight; + setContainerHeight('100%'); + }, []); + + const handleResize = useCallback(() => { + const currentHeight = document.documentElement.clientHeight || document.body.clientHeight; + if (Math.abs(originalHeight.current - currentHeight) > differenceRange) { + if (currentHeight < originalHeight.current) { + setContainerHeight(currentHeight); + setIsKeyboardVisible(true); + onKeyboardShow?.(); + console.log("currentHeight = ",currentHeight) + } + } else { + + if (isKeyboardVisible) { + setContainerHeight('100%'); + setIsKeyboardVisible(false); + onKeyboardHide?.(); + } + originalHeight.current = currentHeight; + } + }, [differenceRange, isKeyboardVisible, onKeyboardShow, onKeyboardHide]); + + useEffect(() => { + initHeight(); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [initHeight, handleResize]); + + const containerStyle: React.CSSProperties = { + height: containerHeight, + minHeight, + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + transition: 'height 0.3s ease', + ...style + }; + + return ( +
+ {children} +
+ ); +}; + +export default AdaptiveContainer; \ No newline at end of file diff --git a/ui/src/components/AuthLayout.tsx b/ui/src/components/AuthLayout.tsx deleted file mode 100644 index 6bf65df..0000000 --- a/ui/src/components/AuthLayout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useLocation, useNavigation, useSearchParams } from "react-router-dom"; - -import { Button, LinkButton } from "@components/Button"; -import { GoogleIcon } from "@components/Icons"; -import SimpleNavbar from "@components/SimpleNavbar"; -import Container from "@components/Container"; -import Fieldset from "@components/Fieldset"; -import GridBackground from "@components/GridBackground"; -import StepCounter from "@components/StepCounter"; - -interface AuthLayoutProps { - title: string; - description: string; - cta: string; - ctaHref: string; - showCounter?: boolean; -} - -export default function AuthLayout({ - title, - description, - cta, - ctaHref, - showCounter, -}: AuthLayoutProps) { - const [sq] = useSearchParams(); - const location = useLocation(); - - const returnTo = sq.get("returnTo") || location.state?.returnTo; - const deviceId = sq.get("deviceId") || location.state?.deviceId; - const navigation = useNavigation(); - - return ( - <> - - -
- - -
- } - /> - -
-
- {showCounter ? ( -
- -
- ) : null} -
-

- {title} -

-

{description}

-
-
-
-
- - - ); -} diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 97fcc5f..0bf116f 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -1,11 +1,13 @@ import React, { JSX } from "react"; import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom"; -import ExtLink from "@/components/ExtLink"; -import LoadingSpinner from "@/components/LoadingSpinner"; +import ExtLink from "@components/ExtLink"; +import LoadingSpinner from "@components/LoadingSpinner"; import { cva, cx } from "@/cva.config"; +import { button_primary_color } from "@/layout/theme_color"; const sizes = { + SS: "h-[24px] px-2 text-xs", XS: "h-[28px] px-2 text-xs", SM: "h-[36px] px-3 text-[13px]", MD: "h-[40px] px-3.5 text-sm", @@ -15,8 +17,8 @@ const sizes = { const themes = { primary: cx( - // Base styles - "bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm", + // Base styles bg-blue-700 + `${button_primary_color} dark:border-blue-600 border border-transparent text-white shadow-sm`, // Hover states "group-hover:bg-blue-800", // Active states @@ -73,7 +75,7 @@ const btnVariants = cva({ // Text classes "font-display text-center font-medium leading-tight", // States - "group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700", + "group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-400 dark:group-focus:ring-blue-700", "group-disabled:opacity-50 group-disabled:pointer-events-none", ), @@ -86,6 +88,7 @@ const btnVariants = cva({ const iconVariants = cva({ variants: { size: { + SS: "h-2.5", XS: "h-3.5", SM: "h-3.5", MD: "h-5", @@ -112,23 +115,41 @@ interface ButtonContentPropsType { size: keyof typeof sizes; theme: keyof typeof themes; loading?: boolean; + hideBorder?: boolean; } function ButtonContent(props: ButtonContentPropsType) { - const { text, LeadingIcon, TrailingIcon, fullWidth, className, textAlign, loading } = - props; + const { + text, + LeadingIcon, + TrailingIcon, + fullWidth, + className, + textAlign, + loading, + hideBorder + } = props; // Based on the size prop, we'll use the corresponding variant classnames const iconClassName = iconVariants(props); const btnClassName = btnVariants(props); + return ( -
+
{loading ? ( @@ -137,7 +158,7 @@ function ButtonContent(props: ButtonContentPropsType) {
) : ( LeadingIcon && ( - + ) )} @@ -154,7 +175,6 @@ function ButtonContent(props: ButtonContentPropsType) {
); } - type ButtonPropsType = Pick< JSX.IntrinsicElements["button"], | "type" @@ -169,25 +189,30 @@ type ButtonPropsType = Pick< | "onMouseLeave" > & React.ComponentProps & { - fetcher?: FetcherWithComponents; - }; - + fetcher?: FetcherWithComponents; + hideBorder?: boolean; + className?: string; +}; export const Button = React.forwardRef( - ({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => { + ({ className,type, onClick, formNoValidate, loading, fetcher, hideBorder, ...props }, ref) => { const classes = cx( "group outline-hidden", props.fullWidth ? "w-full" : "", loading ? "pointer-events-none" : "", + hideBorder ? "border-none" : "", + className ); + const navigation = useNavigation(); const loader = fetcher ? fetcher : navigation; + return (
); -}; +}; \ No newline at end of file diff --git a/ui/src/components/Card.tsx b/ui/src/components/Card.tsx index 9c27c00..8dc32e6 100644 --- a/ui/src/components/Card.tsx +++ b/ui/src/components/Card.tsx @@ -1,12 +1,13 @@ import React, { forwardRef } from "react"; import { cx } from "@/cva.config"; +import { dark_bg2_style } from "@/layout/theme_color"; interface CardPropsType { children: React.ReactNode; className?: string; } - +//bg-linear-to-tr export const GridCard = ({ children, cardClassName, @@ -15,11 +16,11 @@ export const GridCard = ({ cardClassName?: string; }) => { return ( - +
-
-
-
{children}
+
+
+
{children}
); @@ -30,7 +31,7 @@ const Card = forwardRef(({ children, className },
diff --git a/ui/src/components/CardHeader.tsx b/ui/src/components/CardHeader.tsx deleted file mode 100644 index c9ed3ce..0000000 --- a/ui/src/components/CardHeader.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -interface Props { - headline: string; - description?: string | React.ReactNode; - Button?: React.ReactNode; -} - -export const CardHeader = ({ headline, description, Button }: Props) => { - return ( -
-
-

{headline}

- {description &&
{description}
} -
- {Button &&
{Button}
} -
- ); -}; diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx deleted file mode 100644 index cf9855d..0000000 --- a/ui/src/components/Checkbox.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type { Ref } from "react"; -import React, { forwardRef, JSX } from "react"; -import clsx from "clsx"; - -import FieldLabel from "@/components/FieldLabel"; -import { cva, cx } from "@/cva.config"; - -const sizes = { - SM: "w-4 h-4", - MD: "w-5 h-5", -}; - -const checkboxVariants = cva({ - base: cx( - "form-checkbox block rounded", - - // Colors - "border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors", - - // Hover - "hover:bg-slate-200/50 dark:hover:bg-slate-700/50", - - // Active - "active:bg-slate-200 dark:active:bg-slate-700", - - // Focus - "focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900", - - // Disabled - "disabled:pointer-events-none disabled:opacity-30", - ), - variants: { size: sizes }, -}); - -type CheckBoxProps = { - size?: keyof typeof sizes; -} & Omit; - -const Checkbox = forwardRef(function Checkbox( - { size = "MD", className, ...props }, - ref, -) { - const classes = checkboxVariants({ size }); - return ( - - ); -}); -Checkbox.displayName = "Checkbox"; - -type CheckboxWithLabelProps = React.ComponentProps & - CheckBoxProps & { - fullWidth?: boolean; - disabled?: boolean; - }; - -const CheckboxWithLabel = forwardRef( - function CheckboxWithLabel( - { label, id, description, fullWidth, readOnly, ...props }, - ref: Ref, - ) { - return ( - - ); - }, -); -CheckboxWithLabel.displayName = "CheckboxWithLabel"; - -export default Checkbox; -export { CheckboxWithLabel, Checkbox }; diff --git a/ui/src/components/ConfirmDialog.tsx b/ui/src/components/ConfirmDialog.tsx index f6a3923..66dbb6d 100644 --- a/ui/src/components/ConfirmDialog.tsx +++ b/ui/src/components/ConfirmDialog.tsx @@ -3,9 +3,12 @@ import { ExclamationTriangleIcon, InformationCircleIcon, } from "@heroicons/react/24/outline"; +import { isMobile } from "react-device-detect"; +import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react"; +import React from "react"; -import { Button } from "@/components/Button"; -import Modal from "@/components/Modal"; +import { Button } from "@components/Button"; +import Modal from "@components/Modal"; import { cx } from "@/cva.config"; type Variant = "danger" | "success" | "warning" | "info"; @@ -70,10 +73,73 @@ export function ConfirmDialog({ }: ConfirmDialogProps) { const { icon: Icon, iconClass, iconBgClass, buttonTheme } = variantConfig[variant]; + if (!open) return null; + + if (isMobile) { + return ( + + +
+
+ +
+
+
+
+
+

+ {title} +

+
+
+ {description} +
+
+
+
+
+
+
+
+
+
+
+ ); + } + return (
-
+
{title} -
+
{description}
@@ -96,7 +162,7 @@ export function ConfirmDialog({
{cancelText && ( -
-
-
- - ); -} diff --git a/ui/src/components/Drawer.tsx b/ui/src/components/Drawer.tsx new file mode 100644 index 0000000..0544859 --- /dev/null +++ b/ui/src/components/Drawer.tsx @@ -0,0 +1,299 @@ +import React, { useState, useEffect, useRef, ReactNode, CSSProperties } from 'react'; +import { createStyles } from 'antd-style'; + +import { dark_bg2_style } from "@/layout/theme_color"; + +interface DrawerProps { + visible: boolean; + onClose: () => void; + children: ReactNode; + bottomOffset?: number; + height?: number | string; + mask?: boolean; + maskClosable?: boolean; + style?: CSSProperties; + className?: string; + maskStyle?: CSSProperties; + title?: ReactNode; + closable?: boolean; + closeIcon?: ReactNode; + afterOpen?: () => void; + afterClose?: () => void; + placement?: 'bottom' | 'right' | 'left' | 'top'; + width?: number | string; + getContainer?: HTMLElement | false; +} + +const useStyles = createStyles(({ css }) => ({ + drawerContainer: css` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + pointer-events: none; + overflow: hidden; + `, + drawerContainerAbsolute: css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + pointer-events: none; + overflow: hidden; + `, + drawerMask: css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.45); + opacity: 0; + transition: opacity 0.3s; + pointer-events: auto; + `, + drawerMaskVisible: css` + opacity: 1; + `, + drawerContentWrapper: css` + position: absolute; + background: #fff; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15); + transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1); + pointer-events: auto; + display: flex; + flex-direction: column; + `, + // Bottom specific styles + drawerWrapperBottom: css` + left: 0; + width: 100%; + transform: translateY(100%); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + `, + drawerOpenBottom: css` + transform: translateY(0); + `, + // Right specific styles + drawerWrapperRight: css` + right: 0; + top: 0; + height: 100%; + transform: translateX(100%); + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15); + `, + drawerOpenRight: css` + transform: translateX(0); + `, + // Left specific styles + drawerWrapperLeft: css` + left: 0; + top: 0; + height: 100%; + transform: translateX(-100%); + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); + `, + drawerOpenLeft: css` + transform: translateX(0); + `, + // Top specific styles + drawerWrapperTop: css` + left: 0; + top: 0; + width: 100%; + transform: translateY(-100%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + `, + drawerOpenTop: css` + transform: translateY(0); + `, + drawerHeader: css` + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 4px; + border-bottom: 1px solid #f0f0f0; + color: rgba(0, 0, 0, 0.85); + background: #fff; + border-radius: 8px 8px 0 0; + flex-shrink: 0; + `, + drawerTitle: css` + margin: 0; + font-weight: 500; + font-size: 16px; + line-height: 22px; + flex: 1; + `, + drawerClose: css` + line-height: 1; + text-align: center; + text-transform: none; + text-decoration: none; + background: transparent; + border: 0; + outline: 0; + cursor: pointer; + transition: color 0.3s; + padding: 0; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + &:hover: { + color: rgba(0, 0, 0, 0.75); + } + `, + drawerBody: css` + padding-left: 4px; + padding-right: 4px; + font-size: 14px; + line-height: 1.5715; + word-wrap: break-word; + height: calc(100% - 54px); + `, +})); + +// 抽屉组件 +const Drawer: React.FC = ({ + visible = false, + onClose, + children, + bottomOffset = 0, + height = 378, + mask = true, + maskClosable = true, + style = {}, + className = '', + maskStyle = {}, + title, + closable = true, + closeIcon, + afterOpen, + afterClose, + placement = 'bottom', + width = 378, + getContainer + }) => { + const { styles } = useStyles(); + const [isOpen, setIsOpen] = useState(false); + const [isMounted, setIsMounted] = useState(false); + const drawerRef = useRef(null); + const timerRef = useRef | null>(null); + + useEffect(() => { + if (visible) { + setIsMounted(true); + timerRef.current = setTimeout(() => { + setIsOpen(true); + afterOpen?.(); + }, 10); + } else { + setIsOpen(false); + timerRef.current = setTimeout(() => { + setIsMounted(false); + afterClose?.(); + }, 300); + } + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [visible, afterOpen, afterClose]); + + const handleMaskClick = () => { + if (maskClosable) { + onClose(); + } + }; + + const handleContentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const handleClose = () => { + onClose(); + }; + + const getPlacementClass = () => { + switch (placement) { + case 'left': + return `${styles.drawerWrapperLeft} ${isOpen ? styles.drawerOpenLeft : ''}`; + case 'right': + return `${styles.drawerWrapperRight} ${isOpen ? styles.drawerOpenRight : ''}`; + case 'top': + return `${styles.drawerWrapperTop} ${isOpen ? styles.drawerOpenTop : ''}`; + case 'bottom': + default: + return `${styles.drawerWrapperBottom} ${isOpen ? styles.drawerOpenBottom : ''}`; + } + }; + + const getDrawerStyle = (): CSSProperties => { + const baseStyle: CSSProperties = {}; + + if (placement === 'left' || placement === 'right') { + baseStyle.width = typeof width === 'number' ? `${width}px` : width; + } else { + baseStyle.height = typeof height === 'number' ? `${height}px` : height; + if (placement === 'bottom') { + baseStyle.bottom = `${bottomOffset}px`; + } + } + + return baseStyle; + }; + + if (!isMounted && !visible) { + return null; + } + + const getContainerStyle = (): CSSProperties => { + if (getContainer === false) { + return { position: 'absolute' }; + } + return {}; + }; + + return ( +
+ {mask && ( +
+ )} +
+
+
{title}
+ {closable && ( + + )} +
+
+ {children} +
+
+
+ ); +}; + +export default Drawer; \ No newline at end of file diff --git a/ui/src/components/EmptyCard.tsx b/ui/src/components/EmptyCard.tsx index ad3370e..3c65e22 100644 --- a/ui/src/components/EmptyCard.tsx +++ b/ui/src/components/EmptyCard.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { GridCard } from "@/components/Card"; +import { GridCard } from "@components/Card"; import { cx } from "../cva.config"; @@ -10,6 +10,7 @@ interface Props { description?: string | React.ReactNode; BtnElm?: React.ReactNode; className?: string; + iconClassName?: string; } export default function EmptyCard({ @@ -18,6 +19,7 @@ export default function EmptyCard({ description, BtnElm, className, + iconClassName, }: Props) { return ( @@ -30,13 +32,13 @@ export default function EmptyCard({
{IconElm && ( - + )}

{headline}

-

+

{description}

diff --git a/ui/src/components/ExtLink.tsx b/ui/src/components/ExtLink.tsx index 79eec8c..6301319 100644 --- a/ui/src/components/ExtLink.tsx +++ b/ui/src/components/ExtLink.tsx @@ -1,29 +1,29 @@ -import React from "react"; - -import { cx } from "@/cva.config"; - -export default function ExtLink({ - className, - href, - id, - target, - children, -}: { - className?: string; - href: string; - id?: string; - target?: string; - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} +import React from "react"; + +import { cx } from "@/cva.config"; + +export default function ExtLink({ + className, + href, + id, + target, + children, +}: { + className?: string; + href: string; + id?: string; + target?: string; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/ui/src/components/FeatureFlag.tsx b/ui/src/components/FeatureFlag.tsx deleted file mode 100644 index cc0c7c5..0000000 --- a/ui/src/components/FeatureFlag.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect } from "react"; - -import { useFeatureFlag } from "../hooks/useFeatureFlag"; - -export function FeatureFlag({ - minAppVersion, - name = "unnamed", - fallback = null, - children, -}: { - minAppVersion: string; - name?: string; - fallback?: React.ReactNode; - children: React.ReactNode; -}) { - const { isEnabled, appVersion } = useFeatureFlag(minAppVersion); - - useEffect(() => { - if (!appVersion) return; - console.log( - `Feature '${name}' ${isEnabled ? "ENABLED" : "DISABLED"}: ` + - `Current version: ${appVersion}, ` + - `Required min version: ${minAppVersion || "N/A"}`, - ); - }, [isEnabled, name, minAppVersion, appVersion]); - - return isEnabled ? children : fallback; -} diff --git a/ui/src/components/FieldLabel.tsx b/ui/src/components/FieldLabel.tsx index f9065a1..d6c3a0e 100644 --- a/ui/src/components/FieldLabel.tsx +++ b/ui/src/components/FieldLabel.tsx @@ -27,7 +27,7 @@ export default function FieldLabel({ > {label} {description && ( - + {description} )} @@ -40,7 +40,7 @@ export default function FieldLabel({ {label} {description && ( - + {description} )} diff --git a/ui/src/components/FileManager/FileUploader.tsx b/ui/src/components/FileManager/FileUploader.tsx new file mode 100644 index 0000000..6bbc730 --- /dev/null +++ b/ui/src/components/FileManager/FileUploader.tsx @@ -0,0 +1,469 @@ +import { useReactAt } from "i18n-auto-extractor/react"; +import { useEffect, useRef, useState } from "react"; +import { LuCheck, LuUpload } from "react-icons/lu"; +import { isMobile } from "react-device-detect"; + +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { useRTCStore } from "@/hooks/stores"; +import notifications from "@/notifications"; +import { DEVICE_API } from "@/ui.config"; +import { isOnDevice } from "@/main"; +import Card from "@components/Card"; +import { cx } from "@/cva.config"; +import { formatters } from "@/utils"; +import { text_primary_color } from "@/layout/theme_color"; + +import UploadSvg from "@/assets/second/upload.svg?react"; + + +export function FileUploader({ + onBack, + incompleteFileName, + media, + }: { + onBack: () => void; + incompleteFileName?: string; + media?: string; +}) +{ + const { $at }= useReactAt(); + const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success">( + "idle", + ); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadedFileName, setUploadedFileName] = useState(null); + const [uploadedFileSize, setUploadedFileSize] = useState(null); + const [uploadSpeed, setUploadSpeed] = useState(null); + const [fileError, setFileError] = useState(null); + const [uploadError, setUploadError] = useState(null); + + const [send] = useJsonRpc(); + const rtcDataChannelRef = useRef(null); + + const xhrRef = useRef(null); + + useEffect(() => { + const ref = rtcDataChannelRef.current; + return () => { + if (ref) { + ref.onopen = null; + ref.onerror = null; + ref.onmessage = null; + ref.onclose = null; + ref.close(); + } + if (xhrRef.current) { + xhrRef.current.abort(); + } + }; + }, []); + + function handleWebRTCUpload( + file: File, + alreadyUploadedBytes: number, + dataChannel: string, + ) { + const rtcDataChannel = useRTCStore + .getState() + .peerConnection?.createDataChannel(dataChannel); + + if (!rtcDataChannel) { + console.error("Failed to create data channel for file upload"); + notifications.error("Failed to create data channel for file upload"); + setUploadState("idle"); + console.log("Upload state set to 'idle'"); + + return; + } + + rtcDataChannelRef.current = rtcDataChannel; + + const lowWaterMark = 256 * 1024; + const highWaterMark = 1 * 1024 * 1024; + rtcDataChannel.bufferedAmountLowThreshold = lowWaterMark; + + let lastUploadedBytes = alreadyUploadedBytes; + let lastUpdateTime = Date.now(); + const speedHistory: number[] = []; + + rtcDataChannel.onmessage = e => { + try { + const { AlreadyUploadedBytes, Size } = JSON.parse(e.data) as { + AlreadyUploadedBytes: number; + Size: number; + }; + + const now = Date.now(); + const timeDiff = (now - lastUpdateTime) / 1000; // in seconds + const bytesDiff = AlreadyUploadedBytes - lastUploadedBytes; + + if (timeDiff > 0) { + const instantSpeed = bytesDiff / timeDiff; // bytes per second + + // Add to speed history, keeping last 5 readings + speedHistory.push(instantSpeed); + if (speedHistory.length > 5) { + speedHistory.shift(); + } + + // Calculate average speed + const averageSpeed = + speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length; + + setUploadSpeed(averageSpeed); + setUploadProgress((AlreadyUploadedBytes / Size) * 100); + } + + lastUploadedBytes = AlreadyUploadedBytes; + lastUpdateTime = now; + } catch (e) { + console.error("Error processing RTC Data channel message:", e); + } + }; + + rtcDataChannel.onopen = () => { + let pauseSending = false; // Pause sending when the buffered amount is high + const chunkSize = 4 * 1024; // 4KB chunks + + let offset = alreadyUploadedBytes; + const sendNextChunk = () => { + if (offset >= file.size) { + rtcDataChannel.close(); + setUploadState("success"); + return; + } + + if (pauseSending) return; + + const chunk = file.slice(offset, offset + chunkSize); + chunk.arrayBuffer().then(buffer => { + rtcDataChannel.send(buffer); + + if (rtcDataChannel.bufferedAmount >= highWaterMark) { + pauseSending = true; + } + + offset += buffer.byteLength; + console.log(`Chunk sent: ${offset} / ${file.size} bytes`); + sendNextChunk(); + }); + }; + + sendNextChunk(); + rtcDataChannel.onbufferedamountlow = () => { + console.log("RTC Data channel buffered amount low"); + pauseSending = false; // Now the data channel is ready to send more data + sendNextChunk(); + }; + }; + + rtcDataChannel.onerror = error => { + console.error("RTC Data channel error:", error); + notifications.error(`Upload failed: ${error}`); + setUploadState("idle"); + console.log("Upload state set to 'idle'"); + }; + } + + async function handleHttpUpload( + file: File, + alreadyUploadedBytes: number, + dataChannel: string, + ) { + const uploadUrl = `${DEVICE_API}/storage/upload?uploadId=${dataChannel}`; + + const xhr = new XMLHttpRequest(); + xhrRef.current = xhr; + xhr.open("POST", uploadUrl, true); + xhr.setRequestHeader('Content-Range', `bytes ${alreadyUploadedBytes}-${file.size-1}/${file.size}`); + + let lastUploadedBytes = alreadyUploadedBytes; + let lastUpdateTime = Date.now(); + const speedHistory: number[] = []; + + xhr.upload.onprogress = event => { + if (event.lengthComputable) { + const totalUploaded = alreadyUploadedBytes + event.loaded; + const totalSize = file.size; + + const now = Date.now(); + const timeDiff = (now - lastUpdateTime) / 1000; // in seconds + const bytesDiff = totalUploaded - lastUploadedBytes; + + if (timeDiff > 0) { + const instantSpeed = bytesDiff / timeDiff; // bytes per second + + // Add to speed history, keeping last 5 readings + speedHistory.push(instantSpeed); + if (speedHistory.length > 5) { + speedHistory.shift(); + } + + // Calculate average speed + const averageSpeed = + speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length; + + setUploadSpeed(averageSpeed); + setUploadProgress((totalUploaded / totalSize) * 100); + } + + lastUploadedBytes = totalUploaded; + lastUpdateTime = now; + } + }; + + xhr.onload = () => { + if (xhr.status === 200) { + setUploadState("success"); + setTimeout(() => { + onBack() + }, 1000) + } else { + console.error("Upload error:", xhr.statusText); + setUploadError(xhr.statusText); + setUploadState("idle"); + } + }; + + xhr.onerror = () => { + console.error("XHR error:", xhr.statusText); + setUploadError(xhr.statusText); + setUploadState("idle"); + }; + + xhr.onabort = () => { + console.log("Upload aborted"); + setUploadState("idle"); + } + + // Prepare the data to send + const blob = file.slice(alreadyUploadedBytes); + + // Send the file data + xhr.send(blob); + } + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Reset the upload error when a new file is selected + setUploadError(null); + + if ( + incompleteFileName && + file.name !== incompleteFileName.replace(".incomplete", "") + ) { + setFileError( + $at("Please select the file {{fileName}} to continue the upload.").replace("{{fileName}}", incompleteFileName.replace(".incomplete", "")), + ); + // Clear the input value to allow selecting the same file again if needed + event.target.value = ""; + return; + } + + setFileError(null); + console.log(`File selected: ${file.name}, size: ${file.size} bytes`); + setUploadedFileName(file.name); + setUploadedFileSize(file.size); + setUploadState("uploading"); + console.log("Upload state set to 'uploading'"); + + if ( media === "sd" ) { + send("startSDStorageFileUpload", { filename: file.name, size: file.size }, resp => { + console.log("startSDStorageFileUpload response:", resp); + if ("error" in resp) { + console.error("Upload error:", resp.error.message); + setUploadError(resp.error.data || resp.error.message); + setUploadState("idle"); + console.log("Upload state set to 'idle'"); + return; + } + + const { alreadyUploadedBytes, dataChannel } = resp.result as { + alreadyUploadedBytes: number; + dataChannel: string; + }; + + console.log( + `Already uploaded bytes: ${alreadyUploadedBytes}, Data channel: ${dataChannel}`, + ); + + if (isOnDevice) { + handleHttpUpload(file, alreadyUploadedBytes, dataChannel); + } else { + handleWebRTCUpload(file, alreadyUploadedBytes, dataChannel); + } + }); + } + else { + send("startStorageFileUpload", { filename: file.name, size: file.size }, resp => { + console.log("startStorageFileUpload response:", resp); + if ("error" in resp) { + console.error("Upload error:", resp.error.message); + setUploadError(resp.error.data || resp.error.message); + setUploadState("idle"); + console.log("Upload state set to 'idle'"); + return; + } + + const { alreadyUploadedBytes, dataChannel } = resp.result as { + alreadyUploadedBytes: number; + dataChannel: string; + }; + + console.log( + `Already uploaded bytes: ${alreadyUploadedBytes}, Data channel: ${dataChannel}`, + ); + + if (isOnDevice) { + handleHttpUpload(file, alreadyUploadedBytes, dataChannel); + } else { + handleWebRTCUpload(file, alreadyUploadedBytes, dataChannel); + } + }); + } + } + // Clear the input value to allow selecting the same file again if needed + event.target.value = ""; + }; + + return ( +
+
+
{ + if (uploadState === "idle") { + document.getElementById("file-upload")?.click(); + } + }} + className="block select-none" + > +
+ +
+
+ {uploadState === "idle" && ( +
+
+ +
+ +
+ +
+
+ {incompleteFileName + ? ( +
+ {$at("Resume Upload")} + {$at("Click here to select {{fileName}} to resume upload").replace("{{fileName}}", formatters.truncateMiddle(incompleteFileName.replace(".incomplete", ""), 30))} +
+ ) + : $at("Click here to upload a new image") + } +
+ {/*

*/} + {/* {$at("Do not support directories")}*/} + {/*

*/} +
+ )} + + {uploadState === "uploading" && ( +
+
+ +
+ +
+
+
+

+ {$at("Uploading")} {formatters.truncateMiddle(uploadedFileName, 30)} +

+

+ {formatters.bytes(uploadedFileSize || 0)} +

+
+
+
+
+
+ {$at("Uploading...")}... + + {uploadSpeed !== null + ? `${formatters.bytes(uploadSpeed)}/s` + : $at("Calculating...")} + +
+
+
+ )} + + {uploadState === "success" && ( +
+
+ +
+ +
+
+
+

+ {$at("Upload Successful")} +

+

+ {formatters.truncateMiddle(uploadedFileName, 40)} {$at("Uploaded")} +

+
+ )} +
+
+
+
+
+ + {fileError && ( +

{fileError}

+ )} +
+ + {/* Display upload error if present */} + {uploadError && ( +
+ Error: {uploadError} +
+ )} + +
+
+ {isMobile&&
} +
+ ); +} \ No newline at end of file diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/components/FileManager/Mount.tsx similarity index 97% rename from ui/src/routes/devices.$id.mount.tsx rename to ui/src/components/FileManager/Mount.tsx index 461ca38..10db9d9 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/components/FileManager/Mount.tsx @@ -1,7 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - LuGlobe, - LuLink, LuRadioReceiver, LuHardDrive, LuCheck, @@ -11,36 +9,36 @@ import { import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { TrashIcon } from "@heroicons/react/16/solid"; import { useNavigate } from "react-router-dom"; +import {Checkbox} from "antd"; +import { useReactAt } from 'i18n-auto-extractor/react' -import Card, { GridCard } from "@/components/Card"; +import Card, { GridCard } from "@components/Card"; import { Button } from "@components/Button"; import LogoLuckfox from "@/assets/logo-luckfox.png"; import { formatters } from "@/utils"; import AutoHeight from "@components/AutoHeight"; -import { InputFieldWithLabel } from "@/components/InputField"; +import { InputFieldWithLabel } from "@components/InputField"; import DebianIcon from "@/assets/debian-icon.png"; import UbuntuIcon from "@/assets/ubuntu-icon.png"; import FedoraIcon from "@/assets/fedora-icon.png"; import OpenSUSEIcon from "@/assets/opensuse-icon.png"; import ArchIcon from "@/assets/arch-icon.png"; import NetBootIcon from "@/assets/netboot-icon.svg"; -import Fieldset from "@/components/Fieldset"; +import Fieldset from "@components/Fieldset"; import { DEVICE_API } from "@/ui.config"; +import { UploadDialog } from "@components/FileManager/UploadDialog"; +import { SettingsItem } from "@components/Settings/SettingsView"; -import { useJsonRpc } from "../hooks/useJsonRpc"; -import notifications from "../notifications"; -import { isOnDevice } from "../main"; -import { cx } from "../cva.config"; +import { useJsonRpc } from "../../hooks/useJsonRpc"; +import notifications from "../../notifications"; +import { isOnDevice } from "../../main"; +import { cx } from "../../cva.config"; import { MountMediaState, RemoteVirtualMediaState, useMountMediaStore, useRTCStore, -} from "../hooks/stores"; -import { UploadDialog } from "@/components/UploadDialog"; -import { SettingsItem } from "./devices.$id.settings"; -import { Checkbox } from "@/components/Checkbox"; -import { useReactAt } from 'i18n-auto-extractor/react' +} from "../../hooks/stores"; export default function MountRoute() { const navigate = useNavigate(); @@ -360,28 +358,12 @@ function ModeSelectionView({

{$at("Virtual Media Source")}

-
+
{$at("Choose how you want to mount your virtual media")}
{[ - //{ - // label: "Browser Mount", - // value: "browser", - // description: "Stream files directly from your browser", - // icon: LuGlobe, - // tag: "Coming Soon", - // disabled: true, - //}, - //{ - // label: "URL Mount", - // value: "url", - // description: "Mount files from any public web address", - // icon: LuLink, - // tag: "Experimental", - // disabled: false, - //}, { label: "KVM Storage Mount", value: "device", @@ -436,7 +418,7 @@ function ModeSelectionView({

{label}

-

+

{description}

@@ -720,7 +702,7 @@ function UrlView({ {formatters.truncateMiddle(image.name, 40)} {image.description && ( -

+

{image.description}

)} @@ -748,12 +730,13 @@ function DeviceFileView({ mountInProgress, onBack, onNewImageClick, -}: { +}: { onMountStorageFile: (name: string, mode: RemoteVirtualMediaState["mode"]) => void; mountInProgress: boolean; onBack: () => void; onNewImageClick: (incompleteFileName?: string) => void; -}) { +}) +{ const [onStorageFiles, setOnStorageFiles] = useState< { name: string; @@ -1114,7 +1097,8 @@ function SDFileView({ mountInProgress: boolean; onBack: () => void; onNewImageClick: (incompleteFileName?: string) => void; -}) { +}) +{ const [onStorageFiles, setOnStorageFiles] = useState< { name: string; @@ -1528,12 +1512,14 @@ function UploadFileView({ onCancelUpload, incompleteFileName, media, -}: { +}: + { onBack: () => void; onCancelUpload: () => void; incompleteFileName?: string; media?: string; -}) { +}) +{ const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success">( "idle", ); @@ -1547,11 +1533,10 @@ function UploadFileView({ const [send] = useJsonRpc(); const rtcDataChannelRef = useRef(null); - +console.log("incompleteFileName",incompleteFileName) useEffect(() => { const ref = rtcDataChannelRef.current; return () => { - console.log("unmounting"); if (ref) { ref.onopen = null; ref.onerror = null; @@ -1893,7 +1878,7 @@ function UploadFileView({ style={{ width: `${uploadProgress}%` }} >
-
+
{$at("Uploading...")} {uploadSpeed !== null @@ -1994,7 +1979,8 @@ function ErrorView({ errorMessage: string | null; onClose: () => void; onRetry: () => void; -}) { +}) +{ return (
@@ -2019,7 +2005,7 @@ function ErrorView({ ); } -function PreUploadedImageItem({ +export function PreUploadedImageItem({ name, size, uploadedAt, @@ -2065,11 +2051,11 @@ function PreUploadedImageItem({ {formatters.truncateMiddle(name, 45)}
-
+
{formatters.date(new Date(uploadedAt), { month: "short" })}
-
{size}
+
{size}
@@ -2122,14 +2108,14 @@ function ViewHeader({ title, description }: { title: string; description: string

{title}

-
+
{description}
); } -function UsbModeSelector({ +export function UsbModeSelector({ usbMode, setUsbMode, }: { @@ -2150,7 +2136,7 @@ function UsbModeSelector({ name="mountType" onChange={() => setUsbMode("CDROM")} checked={usbMode === "CDROM"} - className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" + className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-[rgba(22,152,217,1)] transition-opacity focus:ring-[rgba(45,106,229,1)] disabled:opacity-30 dark:text-[rgba(45,106,229,1)]" /> CD/DVD @@ -2163,7 +2149,7 @@ function UsbModeSelector({ name="mountType" checked={usbMode === "Disk"} onChange={() => setUsbMode("Disk")} - className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" + className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-[rgba(22,152,217,1)] transition-opacity focus:ring-[rgba(45,106,229,1)] disabled:opacity-30 dark:text-[rgba(45,106,229,1)]" /> Disk diff --git a/ui/src/components/UploadDialog.tsx b/ui/src/components/FileManager/UploadDialog.tsx similarity index 84% rename from ui/src/components/UploadDialog.tsx rename to ui/src/components/FileManager/UploadDialog.tsx index a8e0c78..724d919 100644 --- a/ui/src/components/UploadDialog.tsx +++ b/ui/src/components/FileManager/UploadDialog.tsx @@ -1,10 +1,10 @@ -import Modal from "@/components/Modal"; +import Modal from "@components/Modal"; interface UploadDialogProps { open: boolean; title: string; - description: React.ReactNode - children?: React.ReactNode; + description: React.ReactNode; + children?: React.ReactNode; } @@ -16,7 +16,7 @@ export function UploadDialog({ }: UploadDialogProps) { return ( - {} }> + undefined}>
@@ -25,7 +25,7 @@ export function UploadDialog({

{title}

-
+
{description}
@@ -36,4 +36,4 @@ export function UploadDialog({
); -} \ No newline at end of file +} diff --git a/ui/src/components/GridBackground.tsx b/ui/src/components/GridBackground.tsx deleted file mode 100644 index 7b4349e..0000000 --- a/ui/src/components/GridBackground.tsx +++ /dev/null @@ -1,41 +0,0 @@ -export default function GridBackground() { - return ( -
- -
- ); -} diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header/Header.tsx similarity index 91% rename from ui/src/components/Header.tsx rename to ui/src/components/Header/Header.tsx index 37d0e94..5e29d48 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header/Header.tsx @@ -1,28 +1,24 @@ -import { use, useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect} from "react"; import { useNavigate } from "react-router-dom"; import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid"; import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { LuMonitorSmartphone } from "react-icons/lu"; - -import Container from "@/components/Container"; -import Card from "@/components/Card"; -import { useHidStore, useRTCStore, useUserStore, useVpnStore } from "@/hooks/stores"; -import LogoLuckfox from "@/assets/logo-luckfox.png"; -import USBStateStatus from "@components/USBStateStatus"; -import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard"; -import VpnConnectionStatusCard from "@components/VpnConnectionStatusCard"; -import { SelectMenuBasic } from "./SelectMenuBasic"; -import { DEVICE_API } from "@/ui.config"; -import { useSettingsStore } from "@/hooks/stores"; - -import api from "../api"; -import { isOnDevice } from "../main"; - -import { LinkButton } from "./Button"; - import { useReactAt } from 'i18n-auto-extractor/react' -import enJSON from '../locales/en.json' -import zhJSON from '../locales/zh.json' + +import Container from "@components/Container"; +import Card from "@components/Card"; +import { useHidStore, useRTCStore, useUserStore, useVpnStore , useSettingsStore } from "@/hooks/stores"; +import LogoLuckfox from "@/assets/logo-luckfox.png"; +import USBStateStatus from "@components/Header/USBStateStatus"; +import PeerConnectionStatusCard from "@components/Header/PeerConnectionStatusCard"; +import VpnConnectionStatusCard from "@components/Header/VpnConnectionStatusCard"; +import { DEVICE_API } from "@/ui.config"; + +import { SelectMenuBasic } from "../SelectMenuBasic"; +import api from "../../api"; +import { LinkButton } from "../Button"; +import enJSON from '../../locales/en.json' +import zhJSON from '../../locales/zh.json' interface NavbarProps { isLoggedIn: boolean; @@ -67,7 +63,7 @@ export default function DashboardNavbar({ ]; // default language - const {setCurrentLang,$at,langSet}= useReactAt(); + const { setCurrentLang }= useReactAt(); const handleLangChange = (lang: string) => { setLanguage(lang) @@ -89,9 +85,9 @@ export default function DashboardNavbar({
- diff --git a/ui/src/components/PeerConnectionStatusCard.tsx b/ui/src/components/Header/PeerConnectionStatusCard.tsx similarity index 96% rename from ui/src/components/PeerConnectionStatusCard.tsx rename to ui/src/components/Header/PeerConnectionStatusCard.tsx index 72b57c9..9b83697 100644 --- a/ui/src/components/PeerConnectionStatusCard.tsx +++ b/ui/src/components/Header/PeerConnectionStatusCard.tsx @@ -1,4 +1,4 @@ -import StatusCard from "@components/StatusCards"; +import StatusCard from "@components/Header/StatusCards"; const PeerConnectionStatusMap = { connected: "Connected", diff --git a/ui/src/components/StatusCards.tsx b/ui/src/components/Header/StatusCards.tsx similarity index 100% rename from ui/src/components/StatusCards.tsx rename to ui/src/components/Header/StatusCards.tsx diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/Header/USBStateStatus.tsx similarity index 98% rename from ui/src/components/USBStateStatus.tsx rename to ui/src/components/Header/USBStateStatus.tsx index 18b86f7..a0bcf8f 100644 --- a/ui/src/components/USBStateStatus.tsx +++ b/ui/src/components/Header/USBStateStatus.tsx @@ -3,7 +3,7 @@ import React from "react"; import { cx } from "@/cva.config"; import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png"; import LoadingSpinner from "@components/LoadingSpinner"; -import StatusCard from "@components/StatusCards"; +import StatusCard from "@components/Header/StatusCards"; import { HidState } from "@/hooks/stores"; type USBStates = HidState["usbState"]; diff --git a/ui/src/components/VpnConnectionStatusCard.tsx b/ui/src/components/Header/VpnConnectionStatusCard.tsx similarity index 64% rename from ui/src/components/VpnConnectionStatusCard.tsx rename to ui/src/components/Header/VpnConnectionStatusCard.tsx index d819de6..b8d0009 100644 --- a/ui/src/components/VpnConnectionStatusCard.tsx +++ b/ui/src/components/Header/VpnConnectionStatusCard.tsx @@ -1,5 +1,4 @@ -import StatusCard from "@components/StatusCards"; - +import StatusCard from "@components/Header/StatusCards"; import TailscaleIcon from "@/assets/tailscale.png"; import ZeroTierIcon from "@/assets/zerotier.png"; @@ -13,11 +12,9 @@ const VpnConnectionStatusMap = { export type VpnConnections = keyof typeof VpnConnectionStatusMap; -type StatusProps = { - [key in VpnConnections]: { +type StatusProps = Record; export default function VpnConnectionStatusCard({ state, @@ -46,24 +43,6 @@ export default function VpnConnectionStatusCard({ }; const props = StatusCardProps[state]; if (!props) return; - - const Icon = () => { - if (title === "ZeroTier") { - return ( - - zerotier - - ); - } - if (title === "TailScale") { - return ( - - tailscale - - ); - } - return null; - }; return ( ( - - - -); - -export const GoogleIcon = ({ className }: { className?: string }) => ( - - - - - - - - -); - -export const UbuntuIcon = ({ className }: { className?: string }) => ( - - - - - -); - -export const DebianIcon = ({ className }: { className?: string }) => ( - - - -); - -export const FedoraIcon = ({ className }: { className?: string }) => ( - - - - - - - - - - -); - -export const XLogo = ({ className }: { className?: string }) => ( - - - - - - - - - -); - -export const YCombinatorIcon = ({ className }: { className?: string }) => ( - - - - - - - - - - - - - - - - - - - - -); - -export const GcpIcon = ({ className }: { className?: string }) => ( - - - - - - -); - -export const DoIcon = ({ className }: { className?: string }) => ( - - - - - - - - - - - - - -); - -export const AwsIcon = ({ className }: { className?: string }) => ( - - - - - - - - - - - - -); diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx deleted file mode 100644 index 4decbb1..0000000 --- a/ui/src/components/InfoBar.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useEffect } from "react"; - -import { cx } from "@/cva.config"; -import { - useHidStore, - useMouseStore, - useRTCStore, - useSettingsStore, - useVideoStore, -} from "@/hooks/stores"; -import { keys, modifiers } from "@/keyboardMappings"; -import { useReactAt } from 'i18n-auto-extractor/react' - -export default function InfoBar() { - const { $at } = useReactAt(); - const activeKeys = useHidStore(state => state.activeKeys); - const activeModifiers = useHidStore(state => state.activeModifiers); - const mouseX = useMouseStore(state => state.mouseX); - const mouseY = useMouseStore(state => state.mouseY); - const mouseMove = useMouseStore(state => state.mouseMove); - - const videoClientSize = useVideoStore( - state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, - ); - - const videoSize = useVideoStore( - state => `${Math.round(state.width)}x${Math.round(state.height)}`, - ); - - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); - - const settings = useSettingsStore(); - const showPressedKeys = useSettingsStore(state => state.showPressedKeys); - - useEffect(() => { - if (!rpcDataChannel) return; - rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); - rpcDataChannel.onerror = e => - console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); - }, [rpcDataChannel]); - - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); - - const usbState = useHidStore(state => state.usbState); - const hdmiState = useVideoStore(state => state.hdmiState); - - return ( -
-
-
-
- {settings.debugMode ? ( -
- {$at("Resolution")}:{" "} - {videoSize} -
- ) : null} - - {settings.debugMode ? ( -
- {$at("Video Size")}: - {videoClientSize} -
- ) : null} - - {(settings.debugMode && settings.mouseMode == "absolute") ? ( -
- {$at("Pointer")}: - - {mouseX},{mouseY} - -
- ) : null} - - {(settings.debugMode && settings.mouseMode == "relative") ? ( -
- {$at("Last Move")}: - - {mouseMove ? - `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : - "N/A"} - -
- ) : null} - - {settings.debugMode && ( -
- {$at("USB State")}: - {usbState} -
- )} - {settings.debugMode && ( -
- {$at("HDMI State")}: - {hdmiState} -
- )} - - {showPressedKeys && ( -
- {$at("Keys")}: -

- {[ - ...activeKeys.map( - x => Object.entries(keys).filter(y => y[1] === x)[0][0], - ), - activeModifiers.map( - x => Object.entries(modifiers).filter(y => y[1] === x)[0][0], - ), - ].join(", ")} -

-
- )} -
-
-
- {isTurnServerInUse && ( -
- Relayed by Cloudflare -
- )} - - {keyboardLedStateSyncAvailable ? ( -
- {keyboardLedSync === "browser" ? "Browser" : "Host"} -
- ) : null} -
- Caps Lock -
-
- Num Lock -
-
- Scroll Lock -
- {keyboardLedState?.compose ? ( -
- Compose -
- ) : null} - {keyboardLedState?.kana ? ( -
- Kana -
- ) : null} -
-
-
- ); -} diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx index b89ab06..d1e77a4 100644 --- a/ui/src/components/InputField.tsx +++ b/ui/src/components/InputField.tsx @@ -2,8 +2,8 @@ import type { Ref } from "react"; import React, { forwardRef, JSX } from "react"; import clsx from "clsx"; -import FieldLabel from "@/components/FieldLabel"; -import Card from "@/components/Card"; +import FieldLabel from "@components/FieldLabel"; +import Card from "@components/Card"; import { cva } from "@/cva.config"; const sizes = { diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index d2d8c50..011032a 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -2,10 +2,10 @@ import { MdConnectWithoutContact } from "react-icons/md"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Link } from "react-router-dom"; import { LuEllipsisVertical } from "react-icons/lu"; +import {useReactAt} from 'i18n-auto-extractor/react' import Card from "@components/Card"; import { Button, LinkButton } from "@components/Button"; -import {useReactAt} from 'i18n-auto-extractor/react' function getRelativeTimeString(date: Date | number, lang = navigator.language): string { // Allow dates or times to be passed diff --git a/ui/src/components/LoadingSpinner.tsx b/ui/src/components/LoadingSpinner.tsx index 261d755..f58e6d5 100644 --- a/ui/src/components/LoadingSpinner.tsx +++ b/ui/src/components/LoadingSpinner.tsx @@ -21,7 +21,6 @@ export default function LoadingSpinner({ strokeWidth="4" /> diff --git a/ui/src/components/LogDialog.tsx b/ui/src/components/LogDialog.tsx index b5d518c..e6c0053 100644 --- a/ui/src/components/LogDialog.tsx +++ b/ui/src/components/LogDialog.tsx @@ -3,9 +3,12 @@ import { ExclamationTriangleIcon, InformationCircleIcon, } from "@heroicons/react/24/outline"; +import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react"; +import React from "react"; +import { isMobile } from "react-device-detect"; -import { Button } from "@/components/Button"; -import Modal from "@/components/Modal"; +import { Button } from "@components/Button"; +import Modal from "@components/Modal"; import { cx } from "@/cva.config"; type Variant = "danger" | "success" | "warning" | "info"; @@ -72,6 +75,7 @@ export default function Ansi({ children, className }: AnsiProps) { const lines: { text: string; style: (React.CSSProperties | undefined)[] }[] = []; let col = 0; + const ESC = "\u001b"; const applyCode = (code: number) => { if (code === 0) { curColor = undefined; curBold = false; } @@ -94,11 +98,49 @@ export default function Ansi({ children, className }: AnsiProps) { return stylePool[key]; }; - const tokens = children.split(/(\x1b\[[0-9;]*m|\r\n?|\n)/g); + const tokens: string[] = []; + let i = 0; + while (i < children.length) { + const ch = children[i]; + if (ch === "\r") { + if (children[i + 1] === "\n") { + tokens.push("\r\n"); + i += 2; + continue; + } + tokens.push("\r"); + i += 1; + continue; + } + if (ch === "\n") { + tokens.push("\n"); + i += 1; + continue; + } + if (ch === ESC && children[i + 1] === "[") { + let j = i + 2; + while (j < children.length && children[j] !== "m") j += 1; + if (j < children.length) { + tokens.push(children.slice(i, j + 1)); + i = j + 1; + continue; + } + } + let j = i; + while (j < children.length) { + const c = children[j]; + const isNewline = c === "\n" || c === "\r"; + const isEsc = c === ESC && children[j + 1] === "["; + if (isNewline || isEsc) break; + j += 1; + } + tokens.push(children.slice(i, j)); + i = j; + } let currentLine = { text: '', style: [] as (React.CSSProperties | undefined)[] }; for (const chunk of tokens) { - if (chunk.startsWith('\x1b[') && chunk.endsWith('m')) { + if (chunk.startsWith(`${ESC}[`) && chunk.endsWith('m')) { const codes = chunk.slice(2, -1).split(';').map(Number); codes.forEach(applyCode); } else if (chunk === '\r\n' || chunk === '\n') { @@ -153,10 +195,63 @@ export function LogDialog({ }: LogDialogProps) { const { icon: Icon, iconClass, iconBgClass } = variantConfig[variant]; + if (!open) return null; + + if (isMobile) { + return ( + + +
+
+ +
+
+
+
+
+

+ {title} +

+
+
+ {description} +
+
+
+
+
+
+
+
+
+
+
+ ); + } + return (
-
+
{title} -
+
{description}
@@ -179,7 +274,7 @@ export function LogDialog({
{cancelText && ( -
diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Macro/Combobox.tsx similarity index 96% rename from ui/src/components/Combobox.tsx rename to ui/src/components/Macro/Combobox.tsx index 3fce228..822846d 100644 --- a/ui/src/components/Combobox.tsx +++ b/ui/src/components/Macro/Combobox.tsx @@ -9,7 +9,7 @@ import { import { cva } from "@/cva.config"; -import Card from "./Card"; +import Card from "../Card"; export interface ComboboxOption { value: string; @@ -78,7 +78,7 @@ export function Combobox({ // Disabled disabled && - "pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800", + "pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-[#ffffff]/80 dark:disabled:hover:bg-slate-800", )} placeholder={disabled ? disabledMessage : placeholder} displayValue={displayValue} @@ -112,7 +112,7 @@ export function Combobox({ {options().length === 0 && inputRef.current?.value && (
-
{emptyMessage}
+
{emptyMessage}
)} diff --git a/ui/src/components/MacroBar.tsx b/ui/src/components/Macro/MacroBar.tsx similarity index 100% rename from ui/src/components/MacroBar.tsx rename to ui/src/components/Macro/MacroBar.tsx diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/Macro/MacroForm.tsx similarity index 87% rename from ui/src/components/MacroForm.tsx rename to ui/src/components/Macro/MacroForm.tsx index 14dbc0f..2fae7ce 100644 --- a/ui/src/components/MacroForm.tsx +++ b/ui/src/components/Macro/MacroForm.tsx @@ -1,19 +1,20 @@ import { useState } from "react"; import { LuPlus } from "react-icons/lu"; +import {useReactAt} from 'i18n-auto-extractor/react' +import {Button as AntdButton} from "antd"; +import { isMobile } from "react-device-detect"; import { KeySequence } from "@/hooks/stores"; -import { Button } from "@/components/Button"; -import { InputFieldWithLabel, FieldError } from "@/components/InputField"; -import Fieldset from "@/components/Fieldset"; -import { MacroStepCard } from "@/components/MacroStepCard"; +import { Button } from "@components/Button"; +import { InputFieldWithLabel, FieldError } from "@components/InputField"; +import Fieldset from "@components/Fieldset"; +import { MacroStepCard } from "@components/Macro/MacroStepCard"; import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP, } from "@/constants/macros"; -import FieldLabel from "@/components/FieldLabel"; -import {useReactAt} from 'i18n-auto-extractor/react' - +import FieldLabel from "@components/FieldLabel"; interface ValidationErrors { name?: string; steps?: Record< @@ -173,6 +174,7 @@ export function MacroForm({ return ( <>
+
+
@@ -199,7 +202,7 @@ export function MacroForm({ description={$at("Keys/modifiers executed in sequence with a delay between each step.")} />
- + {macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
@@ -218,10 +221,10 @@ export function MacroForm({ onDelete={ macro.steps && macro.steps.length > 1 ? () => { - const newSteps = [...(macro.steps || [])]; - newSteps.splice(stepIndex, 1); - setMacro(prev => ({ ...prev, steps: newSteps })); - } + const newSteps = [...(macro.steps || [])]; + newSteps.splice(stepIndex, 1); + setMacro(prev => ({ ...prev, steps: newSteps })); + } : undefined } onMoveUp={() => handleStepMove(stepIndex, "up")} @@ -273,17 +276,26 @@ export function MacroForm({
)} -
-
+ {isMobile&&
}
); diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/Macro/MacroStepCard.tsx similarity index 88% rename from ui/src/components/MacroStepCard.tsx rename to ui/src/components/Macro/MacroStepCard.tsx index 4163d0f..e3257d4 100644 --- a/ui/src/components/MacroStepCard.tsx +++ b/ui/src/components/Macro/MacroStepCard.tsx @@ -1,13 +1,17 @@ -import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; +import { LuX } from "react-icons/lu"; +import { Button as AntdButton } from "antd"; +import { useReactAt } from 'i18n-auto-extractor/react' +import UpSVG from "@assets/second/up.svg?react"; +import DownSVG from "@assets/second/down.svg?react"; +import DeleteSVG from "@assets/second/delete.svg?react"; -import { Button } from "@/components/Button"; -import { Combobox } from "@/components/Combobox"; -import { SelectMenuBasic } from "@/components/SelectMenuBasic"; -import Card from "@/components/Card"; +import { Button } from "@components/Button"; +import { Combobox } from "@components/Macro/Combobox"; +import { SelectMenuBasic } from "@components/SelectMenuBasic"; +import Card from "@components/Card"; import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; -import FieldLabel from "@/components/FieldLabel"; -import {useReactAt} from 'i18n-auto-extractor/react' +import FieldLabel from "@components/FieldLabel"; // Filter out modifier keys since they're handled in the modifiers section const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; @@ -104,9 +108,11 @@ export function MacroStepCard({
- - {stepIndex + 1} - + {stepIndex + 1} +
@@ -116,25 +122,23 @@ export function MacroStepCard({ theme="light" onClick={onMoveUp} disabled={stepIndex === 0} - LeadingIcon={LuArrowUp} + LeadingIcon={UpSVG} />
- {onDelete && ( -
)}
@@ -145,7 +149,7 @@ export function MacroStepCard({
{Object.entries(groupedModifiers).map(([group, mods]) => (
- + {group}
diff --git a/ui/src/components/MousePanel.tsx b/ui/src/components/MousePanel.tsx new file mode 100644 index 0000000..c1c4be4 --- /dev/null +++ b/ui/src/components/MousePanel.tsx @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from "react"; +import { Divider } from "antd"; +import { isMobile } from "react-device-detect"; +import { useReactAt } from "i18n-auto-extractor/react"; + +import ScrollThrottlingSelect, { Option } from "@components/ScrollThrottlingSelect"; +import { useSettingsStore } from "@/hooks/stores"; +import { useFeatureFlag } from "@/hooks/useFeatureFlag"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "@/notifications"; +import { dark_bd_style, dark_bg2_style, dark_line_style, dark_bg_style_fun } from "@/layout/theme_color"; +import { useThemeSettings } from "@routes/login_page/useLocalAuth"; + + +const scrollThrottlingOptions = [ + { value: "0", label: "Off" }, + { value: "10", label: "Low" }, + { value: "25", label: "Medium" }, + { value: "50", label: "High" }, + { value: "100", label: "Very High" }, +]; + +const inputModeOptions: Option[] = [ + { label: "Absolute", value: "absolute" }, + { label: "Relative", value: "relative" }, +]; + +const othersOptions: Option[] = [ + { label: "Hide Cursor", value: "hide-cursor" }, + { label: "Jiggler", value: "jiggler" }, +]; + +const MousePanel: React.FC = () => { + const { $at } = useReactAt(); + const hideCursor: boolean = useSettingsStore(state => state.isCursorHidden); + const setHideCursor = useSettingsStore(state => state.setCursorVisibility); + const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8"); + const [send] = useJsonRpc(); + const [others, setOthers] = useState([]); + + useEffect(() => { + send("getJigglerState", {}, (resp) => { + if (!("error" in resp) && resp.result) { + setOthers((prevItems: string[]) => [...prevItems, "jiggler"]); + } else { + setOthers((prevItems) => prevItems.filter(item => item !== "jiggler")); + } + }); + }, [isScrollSensitivityEnabled, send]); + + useEffect(() => { + if (hideCursor) { + setOthers((prevItems: string[]) => [...prevItems, "hide-cursor"]); + } else { + setOthers((prevItems) => prevItems.filter(item => item !== "hide-cursor")); + } + }, [hideCursor]); + + const handleOtherChange = (data: string[] | string) => { + console.log(data); + console.log(data.includes("jiggler")); + console.log(others.includes("jiggler")); + if (data.includes("hide-cursor") != others.includes("hide-cursor")) { + handlehideCursorChange(data.includes("hide-cursor")); + } + if (data.includes("jiggler") != others.includes("jiggler")) { + handleJigglerChange(data.includes("jiggler")); + } + }; + + const handlehideCursorChange = (enabled: boolean) => { + console.log("handlehideCursorChange", enabled); + setHideCursor(enabled); + }; + + const handleJigglerChange = (enabled: boolean) => { + console.log("handleJigglerChange", enabled); + send("setJigglerState", { enabled }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, + ); + } else { + if (enabled) { + console.log("handleJigglerChange if", enabled); + setOthers((prevItems: string[]) => [...prevItems, "jiggler"]); + } else { + console.log("handleJigglerChange el", enabled); + setOthers((prevItems) => prevItems.filter(item => item !== "jiggler")); + } + } + }); + }; + const mouseMode = useSettingsStore(state => state.mouseMode); + const setMouseMode = useSettingsStore(state => state.setMouseMode); + const [modeData, setModeData] = useState(mouseMode); + + useEffect(() => { + setModeData(mouseMode); + }, [mouseMode]); + + const handleModeChange = (data: string[] | string) => { + setMouseMode(data as string); + }; + const scrollThrottling = useSettingsStore(state => state.scrollThrottling); + const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling); + const [scrollData, setScrollData] = useState(String(scrollThrottling)); + + useEffect(() => { + setScrollData(String(scrollThrottling)); + }, [scrollThrottling]); + + const handleScrollChange = (data: string[] | string) => { + setScrollThrottling(Number(data as string)); + }; + const DividerLine = ({isMobile = false}: {isMobile?: boolean}) => { + if (isMobile) { + return ( +
+ +
+ ); + } + return
+ }; + + const { isDark } = useThemeSettings(); + + if (isMobile) { + return ( +
+
+
+ +
+ + + + {/* Input Modes */} +
+ +
+ + + + {/* Others */} +
+ +
+
+ +
+ ); + } + +
+ return ( +
+
+ + + + + + + +
+
+ ); +}; + +export default MousePanel; diff --git a/ui/src/components/Network/DhcpLeaseCard.tsx b/ui/src/components/Network/DhcpLeaseCard.tsx new file mode 100644 index 0000000..ec4a738 --- /dev/null +++ b/ui/src/components/Network/DhcpLeaseCard.tsx @@ -0,0 +1,389 @@ +import { Button } from "antd"; +import {useReactAt} from 'i18n-auto-extractor/react' +import { isMobile } from "react-device-detect"; + +import { GridCard } from "@components/Card"; +import { LifeTimeLabel } from "@/layout/components_setting/network/NetworkContent"; +import { NetworkState } from "@/hooks/stores"; + +export default function DhcpLeaseCard({ + networkState, + setShowRenewLeaseConfirm, +}: { + networkState: NetworkState; + setShowRenewLeaseConfirm: (show: boolean) => void; +}) { + const { $at }= useReactAt(); + return ( + +
+
+

+ {$at("DHCP Lease Information")} +

+ {isMobile ? +
+
+ {networkState?.dhcp_lease?.ip && ( +
+ + {$at("IP Address")} + + + {networkState?.dhcp_lease?.ip} + +
+ )} + + {networkState?.dhcp_lease?.netmask && ( +
+ + {$at("Subnet Mask")} + + + {networkState?.dhcp_lease?.netmask} + +
+ )} + + {networkState?.dhcp_lease?.dns && ( +
+ + {$at("DNS Servers")} + + + {networkState?.dhcp_lease?.dns.map(dns =>
{dns}
)} +
+
+ )} + + {networkState?.dhcp_lease?.broadcast && ( +
+ + {$at("Broadcast Address")} + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + + {networkState?.dhcp_lease?.domain && ( +
+ + {$at("Domain")} + + + {networkState?.dhcp_lease?.domain} + +
+ )} + + {networkState?.dhcp_lease?.ntp_servers && + networkState?.dhcp_lease?.ntp_servers.length > 0 && ( +
+
+ {$at("NTP Servers")} +
+
+ {networkState?.dhcp_lease?.ntp_servers.map(server => ( +
{server}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.hostname && ( +
+ + {$at("Hostname")} + + + {networkState?.dhcp_lease?.hostname} + +
+ )} + + {networkState?.dhcp_lease?.routers && + networkState?.dhcp_lease?.routers.length > 0 && ( +
+ + {$at("Gateways")} + + + {networkState?.dhcp_lease?.routers.map(router => ( +
{router}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.server_id && ( +
+ + {$at("DHCP Server")} + + + {networkState?.dhcp_lease?.server_id} + +
+ )} + + {networkState?.dhcp_lease?.lease_expiry && ( +
+ + {$at("Lease Expiry")} + + + + +
+ )} + + {networkState?.dhcp_lease?.mtu && ( +
+ MTU + + {networkState?.dhcp_lease?.mtu} + +
+ )} + + {networkState?.dhcp_lease?.ttl && ( +
+ TTL + + {networkState?.dhcp_lease?.ttl} + +
+ )} + + {networkState?.dhcp_lease?.bootp_next_server && ( +
+ + {$at("Boot Next Server")} + + + {networkState?.dhcp_lease?.bootp_next_server} + +
+ )} + + {networkState?.dhcp_lease?.bootp_server_name && ( +
+ + {$at("Boot Server Name")} + + + {networkState?.dhcp_lease?.bootp_server_name} + +
+ )} + + {networkState?.dhcp_lease?.bootp_file && ( +
+ + {$at("Boot File")} + + + {networkState?.dhcp_lease?.bootp_file} + +
+ )} +
+
+ : +
+
+ {networkState?.dhcp_lease?.ip && ( +
+ + {$at("IP Address")} + + + {networkState?.dhcp_lease?.ip} + +
+ )} + + {networkState?.dhcp_lease?.netmask && ( +
+ + {$at("Subnet Mask")} + + + {networkState?.dhcp_lease?.netmask} + +
+ )} + + {networkState?.dhcp_lease?.dns && ( +
+ + {$at("DNS Servers")} + + + {networkState?.dhcp_lease?.dns.map(dns =>
{dns}
)} +
+
+ )} + + {networkState?.dhcp_lease?.broadcast && ( +
+ + {$at("Broadcast Address")} + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + + {networkState?.dhcp_lease?.domain && ( +
+ + {$at("Domain")} + + + {networkState?.dhcp_lease?.domain} + +
+ )} + + {networkState?.dhcp_lease?.ntp_servers && + networkState?.dhcp_lease?.ntp_servers.length > 0 && ( +
+
+ {$at("NTP Servers")} +
+
+ {networkState?.dhcp_lease?.ntp_servers.map(server => ( +
{server}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.hostname && ( +
+ + {$at("Hostname")} + + + {networkState?.dhcp_lease?.hostname} + +
+ )} +
+ +
+ {networkState?.dhcp_lease?.routers && + networkState?.dhcp_lease?.routers.length > 0 && ( +
+ + {$at("Gateways")} + + + {networkState?.dhcp_lease?.routers.map(router => ( +
{router}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.server_id && ( +
+ + {$at("DHCP Server")} + + + {networkState?.dhcp_lease?.server_id} + +
+ )} + + {networkState?.dhcp_lease?.lease_expiry && ( +
+ + {$at("Lease Expiry")} + + + + +
+ )} + + {networkState?.dhcp_lease?.mtu && ( +
+ MTU + + {networkState?.dhcp_lease?.mtu} + +
+ )} + + {networkState?.dhcp_lease?.ttl && ( +
+ TTL + + {networkState?.dhcp_lease?.ttl} + +
+ )} + + {networkState?.dhcp_lease?.bootp_next_server && ( +
+ + {$at("Boot Next Server")} + + + {networkState?.dhcp_lease?.bootp_next_server} + +
+ )} + + {networkState?.dhcp_lease?.bootp_server_name && ( +
+ + {$at("Boot Server Name")} + + + {networkState?.dhcp_lease?.bootp_server_name} + +
+ )} + + {networkState?.dhcp_lease?.bootp_file && ( +
+ + {$at("Boot File")} + + + {networkState?.dhcp_lease?.bootp_file} + +
+ )} +
+
+ } + + +
+ +
+
+
+
+ ); +} diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Network/Ipv6NetworkCard.tsx similarity index 93% rename from ui/src/components/Ipv6NetworkCard.tsx rename to ui/src/components/Network/Ipv6NetworkCard.tsx index fca88e1..30225c0 100644 --- a/ui/src/components/Ipv6NetworkCard.tsx +++ b/ui/src/components/Network/Ipv6NetworkCard.tsx @@ -1,9 +1,9 @@ -import { NetworkState } from "../hooks/stores"; -import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; - -import { GridCard } from "./Card"; import { useReactAt } from 'i18n-auto-extractor/react' +import { NetworkState } from "../../hooks/stores"; +import { LifeTimeLabel } from "../../layout/components_setting/network/NetworkContent"; +import { GridCard } from "../Card"; + export default function Ipv6NetworkCard({ networkState, }: { @@ -21,7 +21,7 @@ export default function Ipv6NetworkCard({
{networkState?.dhcp_lease?.ip && (
- + {$at("Link-local")} @@ -43,7 +43,7 @@ export default function Ipv6NetworkCard({ >
- + Address {addr.address} @@ -51,7 +51,7 @@ export default function Ipv6NetworkCard({ {addr.valid_lifetime && (
- + Valid Lifetime @@ -67,7 +67,7 @@ export default function Ipv6NetworkCard({ )} {addr.preferred_lifetime && (
- + Preferred Lifetime diff --git a/ui/src/components/NotFoundPage.tsx b/ui/src/components/NotFoundPage.tsx index b499b11..dc5044a 100644 --- a/ui/src/components/NotFoundPage.tsx +++ b/ui/src/components/NotFoundPage.tsx @@ -1,6 +1,6 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import EmptyCard from "@/components/EmptyCard"; +import EmptyCard from "@components/EmptyCard"; export default function NotFoundPage() { return ( diff --git a/ui/src/components/PinchZoom.tsx b/ui/src/components/PinchZoom.tsx new file mode 100644 index 0000000..b6a6424 --- /dev/null +++ b/ui/src/components/PinchZoom.tsx @@ -0,0 +1,201 @@ +import React, { useRef, useEffect, useState, useCallback } from "react"; + +interface PinchZoomProps { + children: React.ReactElement; + minScale?: number; + maxScale?: number; + initialScale?: number; + onScaleChange?: (scale: number) => void; +} + + + +const PinchZoom: React.FC = ({ + children, + minScale = 0.5, + maxScale = 3, + initialScale = 1, + onScaleChange, +}) => { + const containerRef = useRef(null); + const contentRef = useRef(null); + const overlayRef = useRef(null); + const [scale, setScale] = useState(initialScale); + const [position, setPosition] = useState({ x: 0, y: 0 }); + + const touchStateRef = useRef({ + isDragging: false, + isPinching: false, + startX: 0, + startY: 0, + lastX: 0, + lastY: 0, + initialDistance: 0, + initialScale: initialScale, + lastTouchTime: 0, + touchCount: 0 + }); + + const clampPosition = useCallback((x: number, y: number, currentScale: number) => { + if (!containerRef.current || !contentRef.current) return { x: 0, y: 0 }; + + const containerRect = containerRef.current.getBoundingClientRect(); + const contentRect = contentRef.current.getBoundingClientRect(); + + const scaledWidth = contentRect.width * currentScale; + const scaledHeight = contentRect.height * currentScale; + + const maxX = Math.max(0, (scaledWidth - containerRect.width) / 2 / currentScale); + const maxY = Math.max(0, (scaledHeight - containerRect.height) / 2 / currentScale); + + if (scaledWidth <= containerRect.width) { + x = 0; + } else { + x = Math.max(-maxX, Math.min(maxX, x)); + } + + if (scaledHeight <= containerRect.height) { + y = 0; + } else { + y = Math.max(-maxY, Math.min(maxY, y)); + } + + return { x, y }; + }, []); + + const getDistance = useCallback((touch1: Touch, touch2: Touch) => { + const dx = touch1.clientX - touch2.clientX; + const dy = touch1.clientY - touch2.clientY; + return Math.sqrt(dx * dx + dy * dy); + }, []); + + const handleTouchStart = useCallback((e: TouchEvent) => { + e.preventDefault(); + const touches = e.touches; + const state = touchStateRef.current; + + state.touchCount = touches.length; + state.lastTouchTime = Date.now(); + + if (touches.length === 1) { + state.isDragging = true; + state.startX = touches[0].clientX - position.x; + state.startY = touches[0].clientY - position.y; + state.lastX = touches[0].clientX; + state.lastY = touches[0].clientY; + } else if (touches.length === 2) { + state.isPinching = true; + state.isDragging = false; + state.initialDistance = getDistance(touches[0], touches[1]); + state.initialScale = scale; + } + }, [position, scale, getDistance]); + + const handleTouchMove = useCallback((e: TouchEvent) => { + e.preventDefault(); + const touches = e.touches; + const state = touchStateRef.current; + + if (state.isDragging && touches.length === 1) { + const touch = touches[0]; + const deltaX = touch.clientX - state.lastX; + const deltaY = touch.clientY - state.lastY; + + const newX = position.x + deltaX; + const newY = position.y + deltaY; + + const clamped = clampPosition(newX, newY, scale); + setPosition(clamped); + + state.lastX = touch.clientX; + state.lastY = touch.clientY; + } else if (state.isPinching && touches.length === 2) { + const currentDistance = getDistance(touches[0], touches[1]); + const scaleFactor = currentDistance / state.initialDistance; + let newScale = state.initialScale * scaleFactor; + + newScale = Math.max(minScale, Math.min(maxScale, newScale)); + + setScale(newScale); + onScaleChange?.(newScale); + } + }, [position, scale, clampPosition, getDistance, minScale, maxScale, onScaleChange]); + + const handleTouchEnd = useCallback((e: TouchEvent) => { + const state = touchStateRef.current; + const currentTime = Date.now(); + + if (state.touchCount === 1 && currentTime - state.lastTouchTime < 300) { + const newScale = scale === initialScale ? 1.5 : initialScale; + setScale(newScale); + setPosition({ x: 0, y: 0 }); + onScaleChange?.(newScale); + } + + state.isDragging = false; + state.isPinching = false; + state.touchCount = e.touches.length; + }, [scale, initialScale, onScaleChange]); + + useEffect(() => { + const overlay = overlayRef.current; + if (!overlay) return; + + overlay.addEventListener('touchstart', handleTouchStart, { passive: false }); + overlay.addEventListener('touchmove', handleTouchMove, { passive: false }); + overlay.addEventListener('touchend', handleTouchEnd, { passive: false }); + + return () => { + overlay.removeEventListener('touchstart', handleTouchStart); + overlay.removeEventListener('touchmove', handleTouchMove); + overlay.removeEventListener('touchend', handleTouchEnd); + }; + }, [handleTouchStart, handleTouchMove, handleTouchEnd]); + + return ( +
1 ? "grab" : "default", + }} + > +
+ {children} +
+ +
1 ? "grab" : "default", + }} + /> +
+ ); +}; + +export default PinchZoom; \ No newline at end of file diff --git a/ui/src/components/PopoverButton.tsx b/ui/src/components/PopoverButton.tsx new file mode 100644 index 0000000..e98a1ec --- /dev/null +++ b/ui/src/components/PopoverButton.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; + +import { useUiStore } from "@/hooks/stores"; +import { selected_bt_bg } from "@/layout/theme_color"; + +interface PopoverButtonProps { + buttonText?: string; + buttonIconNode?: React.ReactNode; + panelContent: React.ReactNode; + align?: "left" | "right"; + buttonClassName?: string; + panelClassName?: string; +} + +const BottomPopoverButton: React.FC = ({ + buttonText, + buttonIconNode, + panelContent, + align = "left", + }) => { + const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + return ( + +
+ + {({ open }) => ( + <> + +
{ + setDisableFocusTrap(true); + }} + className={`flex items-center justify-center text-xs h-[24px] cursor-pointer hover:bg-[rgba(0,0,0,0.06)] dark:hover:bg-[rgba(255,255,255,0.06)] transition-colors ${open ? selected_bt_bg : ""}`} + > + {buttonIconNode && ( +
+ {buttonIconNode} +
+ )} + {buttonText && ( + {buttonText} + )} +
+
+ + + {panelContent} + + + )} +
+
+ ) + ; +}; + +export default BottomPopoverButton; \ No newline at end of file diff --git a/ui/src/components/PreUploadedImageItem.tsx b/ui/src/components/PreUploadedImageItem.tsx new file mode 100644 index 0000000..489315a --- /dev/null +++ b/ui/src/components/PreUploadedImageItem.tsx @@ -0,0 +1,109 @@ +import { useReactAt } from "i18n-auto-extractor/react"; +import { useState } from "react"; +import { Button as AntButton } from "antd"; +import { DeleteOutlined, DownloadOutlined } from "@ant-design/icons"; + +import { cx } from "@/cva.config"; +import { formatters } from "@/utils"; +import { Button } from "@components/Button"; + +export function PreUploadedImageItem({ + name, + size, + uploadedAt, + isSelected, + isIncomplete, + onDownload, + onDelete, + onContinueUpload, + onSelected + }: { + name: string; + size: string; + uploadedAt: string; + isSelected: boolean; + isIncomplete: boolean; + onDownload: () => void; + onDelete: () => void; + onContinueUpload: () => void; + onSelected?: () => void; +}) { + const { $at }= useReactAt(); + const [isHovering, setIsHovering] = useState(false); + return ( + + ); +} \ No newline at end of file diff --git a/ui/src/components/ScrollThrottlingSelect.tsx b/ui/src/components/ScrollThrottlingSelect.tsx new file mode 100644 index 0000000..2eef76b --- /dev/null +++ b/ui/src/components/ScrollThrottlingSelect.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import { CheckOutlined } from '@ant-design/icons'; +import { isMobile } from "react-device-detect"; + +import { dark_bg2_style, dark_font_style } from "@/layout/theme_color"; +import { useThemeSettings } from "@routes/login_page/useLocalAuth"; + + +export interface Option { + label: string; + value: string; +} + +interface ScrollThrottlingSelectProps { + mode?: 'single' | 'multiple'; + value?: string | string[]; + onChange?: (value: string | string[]) => void; + options?: Option[]; + title?: string; + disabled?: boolean; + specialOptionText?: string; + specialOptionIcon?: React.ReactNode; + onSpecialOptionClick?: () => void; + maxShowCount?: number; +} + +const defaultOptions: Option[] = [ + { label: 'off', value: 'off' }, +]; + +const ScrollThrottlingSelect: React.FC = ({ + mode = 'single', + value, + onChange, + options = defaultOptions, + title = 'Scroll Throttling', + disabled = false, + specialOptionText, + specialOptionIcon, + onSpecialOptionClick, + maxShowCount + }) => { + const { isDark } = useThemeSettings(); + const handleSingleSelect = (selectedValue: string) => { + if (disabled) return; + onChange?.(selectedValue); + }; + + const handleMultipleSelect = (selectedValue: string) => { + if (disabled) return; + + const currentValues = Array.isArray(value) ? value : []; + const newValues = currentValues.includes(selectedValue) + ? currentValues.filter(v => v !== selectedValue) + : [...currentValues, selectedValue]; + + onChange?.(newValues); + }; + + const isSelected = (optionValue: string): boolean => { + if (mode === 'single') { + return value === optionValue; + } else { + return Array.isArray(value) && value.includes(optionValue); + } + }; + + const handleSpecialOptionClick = () => { + if (disabled) return; + onSpecialOptionClick?.(); + }; + + const getVisibleOptions = () => { + if (!maxShowCount || maxShowCount >= options.length) { + return { + visibleOptions: options, + hiddenCount: 0 + }; + } + + return { + visibleOptions: options.slice(0, maxShowCount), + hiddenCount: options.length - maxShowCount + }; + }; + + const { visibleOptions } = getVisibleOptions(); + + if (isMobile) { + return ( +
+
+ {title} +
+ +
+ {visibleOptions.map(option => ( +
mode === 'single' + ? handleSingleSelect(option.value) + : handleMultipleSelect(option.value) + } + className={` + flex items-center justify-between py-4 w-full + transition-all duration-200 ease-in-out + ${isDark ? 'text-white' : 'text-black'} + ${disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer opacity-100'} + `} + > + + {option.label} + + + ✓ + +
+ ))} + + {specialOptionText && ( +
+ + {specialOptionText} + + + {specialOptionIcon} + +
+ )} +
+
+ ); + } + + return ( +
+
+ {title} +
+ +
+ {visibleOptions.map(option => ( +
mode === 'single' + ? handleSingleSelect(option.value) + : handleMultipleSelect(option.value) + } + className={`transition-colors duration-100 ease-in-out`} + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '8px 12px', + fontWeight:400, + borderRadius: 4, + cursor: disabled ? 'not-allowed' : 'pointer', + border: '1px solid transparent', + transition: 'background-color 0.1s ease-in-out', + }} + onMouseDown={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'; + } + }} + onMouseUp={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; + } + }} + onMouseEnter={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; + } + }} + onMouseLeave={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = 'transparent'; + e.currentTarget.style.boxShadow = 'none'; + } + }} + > + + {option.label} + + + {isSelected(option.value) && ( + + )} +
+ ))} + + {specialOptionText && ( +
{ + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'; + } + }} + onMouseUp={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; + } + }} + onMouseEnter={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; + } + }} + onMouseLeave={(e) => { + if (!disabled) { + e.currentTarget.style.backgroundColor = 'transparent'; + e.currentTarget.style.boxShadow = 'none'; + } + }} + > + + {specialOptionText} + + + {specialOptionIcon && ( + + {specialOptionIcon} + + )} +
+ )} +
+
+ ); +}; + +export default ScrollThrottlingSelect; \ No newline at end of file diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index 1d66565..d6fda22 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -1,7 +1,7 @@ import React, { JSX } from "react"; import clsx from "clsx"; -import FieldLabel from "@/components/FieldLabel"; +import FieldLabel from "@components/FieldLabel"; import { cva } from "@/cva.config"; import Card from "./Card"; @@ -77,8 +77,9 @@ export const SelectMenuBasic = React.forwardRef +
+
+
+ {title} + {badge && ( + + {badge} + + )} +
+ {loading && } +
+
{description}
+
+ {children ? <>{children} : null} + + ); +} +export function SettingsItemNew(props: SettingsItemProps) { + const { title, description, badge, children, className, loading } = props; + + return ( + + ); +} \ No newline at end of file diff --git a/ui/src/components/Sidebar/SideTabs.tsx b/ui/src/components/Sidebar/SideTabs.tsx new file mode 100644 index 0000000..1c3dd89 --- /dev/null +++ b/ui/src/components/Sidebar/SideTabs.tsx @@ -0,0 +1,110 @@ +import React, { useState } from "react"; +import { Tabs } from "antd"; +import type { TabsProps } from "antd"; + +import { useThemeSettings } from "@routes/login_page/useLocalAuth"; +import { button_primary_color, dark_font_style } from "@/layout/theme_color"; + +interface SideTabsProps { + tab1Label: string; + tab2Label: string; + tab1Content: React.ReactNode; + tab2Content: React.ReactNode; + defaultActiveKey?: string; +} + +const SideTabs: React.FC = ({ + tab1Label, + tab2Label, + tab1Content, + tab2Content, + defaultActiveKey = "1", + }) => { + const [activeTab, setActiveTab] = useState(defaultActiveKey); + const { isDark } = useThemeSettings(); + const renderCustomTabBar: TabsProps["renderTabBar"] = () => { + return ( +
+
setActiveTab("1")} + > + {tab1Label} +
+ +
setActiveTab("2")} + > + {tab2Label} +
+
+ ); + }; + + const tabItems: TabsProps["items"] = [ + { + key: "1", + label: tab1Label, + children: tab1Content, + }, + { + key: "2", + label: tab2Label, + children: tab2Content, + }, + ]; + + return ( + + ); +}; + +export default SideTabs; \ No newline at end of file diff --git a/ui/src/components/Sidebar/SidebarDrawer.tsx b/ui/src/components/Sidebar/SidebarDrawer.tsx new file mode 100644 index 0000000..afc0355 --- /dev/null +++ b/ui/src/components/Sidebar/SidebarDrawer.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Drawer, DrawerProps } from 'antd'; + +import { useUiStore } from "@/hooks/stores"; +import StatsSidebarHeader from "@components/StatsSidebarHeader"; +import { dark_bg2_style } from "@/layout/theme_color"; + +export interface EnhancedSidebarDrawerProps extends Omit { + title?: string; + className?: string; + targetView: string; + + placement: 'top' | 'bottom' | 'left' | 'right'; + drawerRender: () => React.ReactNode; +} + +const EnhancedDrawer: React.FC = ({ + title='', + className = 'p-[20px]', + targetView, + placement, + drawerRender, + ...drawerProps + }) => { + const sidebarView = useUiStore(state => state.sidebarView); + const setSidebarView = useUiStore(state => state.setSidebarView); + + return ( + ( +
+
+ +
+
+
+ {drawerRender()} +
+
+
+ )} + /> + ); +}; + +export default EnhancedDrawer; \ No newline at end of file diff --git a/ui/src/components/SidebarHeader.tsx b/ui/src/components/Sidebar/SidebarHeader.tsx similarity index 99% rename from ui/src/components/SidebarHeader.tsx rename to ui/src/components/Sidebar/SidebarHeader.tsx index 83ecb45..265a3a5 100644 --- a/ui/src/components/SidebarHeader.tsx +++ b/ui/src/components/Sidebar/SidebarHeader.tsx @@ -1,7 +1,8 @@ +import {useReactAt} from 'i18n-auto-extractor/react' + import { Button } from "@components/Button"; import { cx } from "@/cva.config"; import { AvailableSidebarViews } from "@/hooks/stores"; -import {useReactAt} from 'i18n-auto-extractor/react' export default function SidebarHeader({ title, diff --git a/ui/src/components/Sidebar/SlideAnimation.tsx b/ui/src/components/Sidebar/SlideAnimation.tsx new file mode 100644 index 0000000..f18f65e --- /dev/null +++ b/ui/src/components/Sidebar/SlideAnimation.tsx @@ -0,0 +1,104 @@ +import { AnimatePresence, motion, MotionProps } from "framer-motion"; +import React from "react"; + +import { cx } from "@/cva.config"; + +export type AnimationDirection = "up" | "down" | "left" | "right"; +interface SlideAnimationProps { + direction: AnimationDirection; + children: React.ReactNode; + isVisible: boolean; + className?: string; + onAnimationComplete?: () => void; +} + +const SlideAnimation: React.FC = ({ + direction, + children, + isVisible, + className = "", + onAnimationComplete + }) => { + const getAnimationVariants = (): MotionProps["variants"] => { + const distance = 50; + const sizeValue = 0.0001; + const variants = { + up: { + initial: { y: -distance, opacity: 0, height: sizeValue }, + animate: { y: 0, opacity: 1, height: "auto" }, + exit: { y: -distance, opacity: 0, height: sizeValue } + }, + down: { + initial: { y: distance, opacity: 0, height: sizeValue }, + animate: { y: 0, opacity: 1, height: "auto" }, + exit: { y: distance, opacity: 0, height: sizeValue } + }, + left: { + initial: { x: -distance, opacity: 0, width: sizeValue }, + animate: { x: 0, opacity: 1, width: "auto" }, + exit: { x: -distance, opacity: 0, width: sizeValue } + }, + right: { + initial: { x: distance, opacity: 0, width: sizeValue }, + animate: { x: 0, opacity: 1, width: "auto" }, + exit: { x: distance, opacity: 0, width: sizeValue } + } + }; + + return { + initial: variants[direction].initial, + animate: { + ...variants[direction].animate, + transition: { + duration: 0.3, + ease: "easeOut", + width: { duration: 0.3, ease: "easeOut" }, + height: { duration: 0.3, ease: "easeOut" } + } + }, + exit: { + ...variants[direction].exit, + transition: { + duration: 0.2, + ease: "easeIn", + width: { duration: 0.2, ease: "easeIn" }, + height: { duration: 0.2, ease: "easeIn" } + } + } + }; + }; + + return ( + + {isVisible && ( + + {children} + + )} + + ); +} +export default SlideAnimation; \ No newline at end of file diff --git a/ui/src/components/Sidebar/StatsTopbar.tsx b/ui/src/components/Sidebar/StatsTopbar.tsx new file mode 100644 index 0000000..70494ff --- /dev/null +++ b/ui/src/components/Sidebar/StatsTopbar.tsx @@ -0,0 +1,46 @@ +import { useReactAt } from "i18n-auto-extractor/react"; + +import { useUiStore } from "@/hooks/stores"; +import { dark_font_style } from "@/layout/theme_color"; + +interface StatsSidebarProps { + title: string; + targetView: string; + children: React.ReactNode; +} + +import { theme as AntTheme } from "antd"; + +import SlideAnimation from "@components/Sidebar/SlideAnimation"; + +const StatsTobbar = ({ title, targetView, children }: StatsSidebarProps) => { + const token = AntTheme.useToken(); + const sidebarView = useUiStore(state => state.sidebarView); + const topBarView = useUiStore(state => state.topBarView); + + const { $at } = useReactAt(); + + return( + + +
+ +
+ {title !== "" && +
{$at(title)}
} +
+ {children} +
+
+
+
+ ) +}; +export default StatsTobbar; \ No newline at end of file diff --git a/ui/src/components/SimpleNavbar.tsx b/ui/src/components/SimpleNavbar.tsx index 363b5b4..624427c 100644 --- a/ui/src/components/SimpleNavbar.tsx +++ b/ui/src/components/SimpleNavbar.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import React from "react"; -import Container from "@/components/Container"; +import Container from "@components/Container"; import LogoLuckfox from "@/assets/logo-luckfox.png"; interface Props { logoHref?: string; actionElement?: React.ReactNode } diff --git a/ui/src/components/SmartButton.tsx b/ui/src/components/SmartButton.tsx new file mode 100644 index 0000000..966eef8 --- /dev/null +++ b/ui/src/components/SmartButton.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useState, useLayoutEffect } from 'react'; +import { Button, Tooltip } from 'antd'; +import type { ButtonProps } from 'antd'; + +import { dark_bd_style } from "@/layout/theme_color"; + +interface SmartButtonProps extends Omit { + text: string; + maxWidth?: number | string; +} + +const useIsOverflow = (deps: any[]) => { + const ref = useRef(null); + const [isOverflow, setIsOverflow] = useState(false); + + useLayoutEffect(() => { + const element = ref.current; + if (element) { + const isOverflowing = element.scrollWidth > element.clientWidth; + setIsOverflow(isOverflowing); + } + }, deps); + + return { ref, isOverflow }; +}; + +const SmartButton: React.FC = ({ + text, + maxWidth, + style, + ...buttonProps + }) => { + const { ref, isOverflow } = useIsOverflow([text, maxWidth, style]); + + const buttonStyle = maxWidth ? { ...style, maxWidth } : style; + + const buttonElement = ( + + ); + + return isOverflow ? ( + + {buttonElement} + + ) : ( + buttonElement + ); +}; + +export default SmartButton; \ No newline at end of file diff --git a/ui/src/components/StatChart.tsx b/ui/src/components/StatChart.tsx index 2c403e3..0a2c7c6 100644 --- a/ui/src/components/StatChart.tsx +++ b/ui/src/components/StatChart.tsx @@ -11,90 +11,107 @@ import { } from "recharts"; import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip"; +import { primary_color, primary_dark_color } from "@/layout/theme_color"; +import { useThemeSettings } from "@routes/login_page/useLocalAuth"; export default function StatChart({ - data, - domain, - unit, - referenceValue, -}: { + data, + domain, + unit, + referenceValue, + }: { data: { date: number; stat: number | null | undefined }[]; domain?: [string | number, string | number]; unit?: string; referenceValue?: number; }) { + const { isDark } = useThemeSettings(); return ( - - - - - {referenceValue && ( - + + + + + {referenceValue && ( + + )} + { + return new Date(date * 1000).toLocaleString("en-US", { + hourCycle: "h23", + hour: "numeric", + minute: "2-digit", + }); + }} + ticks={data + .filter(d => { + return d.date % 60 === 0; + }) + .map(x => x.date)} + /> + - )} - { - return new Date(date * 1000).toLocaleString("en-US", { - hourCycle: "h23", - hour: "numeric", - minute: "2-digit", - }); - }} - ticks={data - .filter(d => { - return d.date % 60 === 0; - }) - .map(x => x.date)} - /> - - { - return ; - }} - /> - - - + { + return ; + }} + /> + + + + {unit && ( +
+ {unit} +
+ )} +
); -} +} \ No newline at end of file diff --git a/ui/src/components/StatsSidebarHeader.tsx b/ui/src/components/StatsSidebarHeader.tsx new file mode 100644 index 0000000..2d23b4e --- /dev/null +++ b/ui/src/components/StatsSidebarHeader.tsx @@ -0,0 +1,68 @@ +import { isMobile } from "react-device-detect"; + +import { Button } from "@components/Button"; +import { cx } from "@/cva.config"; +import { AvailableSidebarViews } from "@/hooks/stores"; +export default function StatsSidebarHeader({ + title, + setSidebarView, + }: { + title: string; + setSidebarView: (view: AvailableSidebarViews | null) => void; +}) { + if(isMobile){ + return ( +
+
+

{title}

+
+
+
+ + ) + } + + return ( +
+
+

{title}

+
+ +
+ ); +} diff --git a/ui/src/components/StepCounter.tsx b/ui/src/components/StepCounter.tsx index 9033c56..fccb914 100644 --- a/ui/src/components/StepCounter.tsx +++ b/ui/src/components/StepCounter.tsx @@ -1,7 +1,7 @@ import { CheckIcon } from "@heroicons/react/16/solid"; import { cva, cx } from "@/cva.config"; -import Card from "@/components/Card"; +import Card from "@components/Card"; interface Props { nSteps: number; @@ -57,7 +57,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props) return ( tab.id === activeTab)?.content; - - return ( -
- {/* Tab buttons */} -
- {tabs.map(tab => ( - - ))} -
- - {/* Tab content */} -
{activeTabContent}
-
- ); -} - diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx deleted file mode 100644 index e5adc4b..0000000 --- a/ui/src/components/Terminal.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import "react-simple-keyboard/build/css/index.css"; -import { LuPin, LuPinOff } from 'react-icons/lu' -import { useEffect, useRef, useState } from "react"; -import { useXTerm } from "react-xtermjs"; -import { FitAddon } from "@xterm/addon-fit"; -import { WebLinksAddon } from "@xterm/addon-web-links"; -import { WebglAddon } from "@xterm/addon-webgl"; -import { Unicode11Addon } from "@xterm/addon-unicode11"; -import { ClipboardAddon } from "@xterm/addon-clipboard"; - -import { cx } from "@/cva.config"; -import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores"; - -import { Button } from "./Button"; - -const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2"); - -// Terminal theme configuration -const SOLARIZED_THEME = { - background: "#0f172a", // Solarized base03 - foreground: "#839496", // Solarized base0 - cursor: "#93a1a1", // Solarized base1 - cursorAccent: "#002b36", // Solarized base03 - black: "#073642", // Solarized base02 - red: "#dc322f", // Solarized red - green: "#859900", // Solarized green - yellow: "#b58900", // Solarized yellow - blue: "#268bd2", // Solarized blue - magenta: "#d33682", // Solarized magenta - cyan: "#2aa198", // Solarized cyan - white: "#eee8d5", // Solarized base2 - brightBlack: "#002b36", // Solarized base03 - brightRed: "#cb4b16", // Solarized orange - brightGreen: "#586e75", // Solarized base01 - brightYellow: "#657b83", // Solarized base00 - brightBlue: "#839496", // Solarized base0 - brightMagenta: "#6c71c4", // Solarized violet - brightCyan: "#93a1a1", // Solarized base1 - brightWhite: "#fdf6e3", // Solarized base3 -} as const; - -const TERMINAL_CONFIG = { - theme: SOLARIZED_THEME, - fontFamily: "'Fira Code', Menlo, Monaco, 'Courier New', monospace", - fontSize: 13, - allowProposedApi: true, - scrollback: 1000, - cursorBlink: true, - smoothScrollDuration: 100, - macOptionIsMeta: true, - macOptionClickForcesSelection: true, - convertEol: true, - linuxMode: false, - // Add these configurations: - cursorStyle: "block", - rendererType: "canvas", // Ensure we're using the canvas renderer -} as const; - -function Terminal({ - title, - dataChannel, - type, -}: { - readonly title: string; - readonly dataChannel: RTCDataChannel; - readonly type: AvailableTerminalTypes; -}) { - const enableTerminal = useUiStore(state => state.terminalType == type); - const setTerminalType = useUiStore(state => state.setTerminalType); - const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - - const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG }); - - const [pinned, setPinned] = useState(false); - const containerRef = useRef(null); - useEffect(() => { - if (!enableTerminal) return; - - const handleClickOutside = (e: MouseEvent) => { - if (pinned) return; - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setTerminalType("none"); - setDisableKeyboardFocusTrap(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [enableTerminal, pinned, setTerminalType, setDisableKeyboardFocusTrap]); - - useEffect(() => { - setTimeout(() => { - setDisableKeyboardFocusTrap(enableTerminal); - }, 500); - - return () => { - setDisableKeyboardFocusTrap(false); - }; - }, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]); - - const readyState = dataChannel.readyState; - useEffect(() => { - if (!instance) return; - if (readyState !== "open") return; - - const abortController = new AbortController(); - const binaryType = dataChannel.binaryType; - dataChannel.addEventListener( - "message", - e => { - // Handle binary data differently based on browser implementation - // Firefox sends data as blobs, chrome sends data as arraybuffer - if (binaryType === "arraybuffer") { - instance.write(new Uint8Array(e.data)); - } else if (binaryType === "blob") { - const reader = new FileReader(); - reader.onload = () => { - if (!reader.result) return; - instance.write(new Uint8Array(reader.result as ArrayBuffer)); - }; - reader.readAsArrayBuffer(e.data); - } - }, - { signal: abortController.signal }, - ); - - const onDataHandler = instance.onData(data => { - dataChannel.send(data); - }); - - // Setup escape key handler - const onKeyHandler = instance.onKey(e => { - const { domEvent } = e; - if (domEvent.key === "Escape") { - setTerminalType("none"); - setDisableKeyboardFocusTrap(false); - domEvent.preventDefault(); - } - }); - - // Send initial terminal size - if (dataChannel.readyState === "open") { - dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols })); - } - - return () => { - abortController.abort(); - onDataHandler.dispose(); - onKeyHandler.dispose(); - }; - }, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]); - - useEffect(() => { - if (!instance) return; - - // Load the fit addon - const fitAddon = new FitAddon(); - instance.loadAddon(fitAddon); - - instance.loadAddon(new ClipboardAddon()); - instance.loadAddon(new Unicode11Addon()); - instance.loadAddon(new WebLinksAddon()); - instance.unicode.activeVersion = "11"; - - if (isWebGl2Supported) { - const webGl2Addon = new WebglAddon(); - webGl2Addon.onContextLoss(() => webGl2Addon.dispose()); - instance.loadAddon(webGl2Addon); - } - - const handleResize = () => fitAddon.fit(); - - // Handle resize event - window.addEventListener("resize", handleResize); - if (enableTerminal) { - setTimeout(handleResize, 50); - } - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [ref, instance, enableTerminal]); - - return ( -
e.stopPropagation()} - onKeyUp={e => e.stopPropagation()} - > -
-
-
-
-

- {title} -

-
-
-
- -
-
-
-
-
-
-
- ); -} - -export default Terminal; diff --git a/ui/src/components/TextArea.tsx b/ui/src/components/TextArea.tsx index 49dbdd7..e84d1c7 100644 --- a/ui/src/components/TextArea.tsx +++ b/ui/src/components/TextArea.tsx @@ -1,9 +1,9 @@ import React, { JSX } from "react"; import clsx from "clsx"; -import FieldLabel from "@/components/FieldLabel"; -import { FieldError } from "@/components/InputField"; -import Card from "@/components/Card"; +import FieldLabel from "@components/FieldLabel"; +import { FieldError } from "@components/InputField"; +import Card from "@components/Card"; import { cx } from "@/cva.config"; type TextAreaProps = JSX.IntrinsicElements["textarea"] & { @@ -15,9 +15,9 @@ const TextArea = React.forwardRef( return (