mirror of https://github.com/jetkvm/kvm.git
Merge ea43caae27 into b144d9926f
This commit is contained in:
commit
a3c1937776
|
|
@ -8,7 +8,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||||
|
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
|
||||||
],
|
],
|
||||||
"onCreateCommand": ".devcontainer/install-deps.sh",
|
"onCreateCommand": ".devcontainer/install-deps.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
|
|
@ -31,7 +32,10 @@
|
||||||
// Frontend
|
// Frontend
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"bradlc.vscode-tailwindcss"
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"codeandstuff.package-json-upgrade",
|
||||||
|
// Localization
|
||||||
|
"inlang.vs-code-extension"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// coding styles
|
||||||
|
"chrislajoie.vscode-modelines",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
// GitHub
|
||||||
|
"GitHub.vscode-pull-request-github",
|
||||||
|
"github.vscode-github-actions",
|
||||||
|
// Golang
|
||||||
|
"golang.go",
|
||||||
|
// C / C++
|
||||||
|
"ms-vscode.cpptools",
|
||||||
|
"ms-vscode.cpptools-extension-pack",
|
||||||
|
// CMake / Makefile
|
||||||
|
"ms-vscode.makefile-tools",
|
||||||
|
"ms-vscode.cmake-tools",
|
||||||
|
// Frontend
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"codeandstuff.package-json-upgrade",
|
||||||
|
// Localization
|
||||||
|
"inlang.vs-code-extension"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -121,12 +121,16 @@ tail -f /var/log/jetkvm.log
|
||||||
├── scripts/ # Bash shell scripts for building and deploying
|
├── scripts/ # Bash shell scripts for building and deploying
|
||||||
└── static/ # (react client build output)
|
└── static/ # (react client build output)
|
||||||
└── ui/ # React frontend
|
└── ui/ # React frontend
|
||||||
|
├── localization/ # Client UI localization (i18n)
|
||||||
|
│ ├── jetKVM.UI.inlang/ # Settings for inlang
|
||||||
|
│ └── messages/ # Messages localized
|
||||||
├── public/ # UI website static images and fonts
|
├── public/ # UI website static images and fonts
|
||||||
└── src/ # Client React UI
|
└── src/ # Client React UI
|
||||||
├── assets/ # UI in-page images
|
├── assets/ # UI in-page images
|
||||||
├── components/ # UI components
|
├── components/ # UI components
|
||||||
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
||||||
├── keyboardLayouts/ # Keyboard layout definitions
|
├── keyboardLayouts/ # Keyboard layout definitions
|
||||||
|
│ ├── paraglide/ # (localization compiled messages output)
|
||||||
├── providers/ # Feature flags
|
├── providers/ # Feature flags
|
||||||
└── routes/ # Pages (login, settings, etc.)
|
└── routes/ # Pages (login, settings, etc.)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,10 @@ module.exports = defineConfig([{
|
||||||
map: [
|
map: [
|
||||||
["@components", "./src/components"],
|
["@components", "./src/components"],
|
||||||
["@routes", "./src/routes"],
|
["@routes", "./src/routes"],
|
||||||
|
["@hooks", "./src/hooks"],
|
||||||
|
["@providers", "./src/providers"],
|
||||||
["@assets", "./src/assets"],
|
["@assets", "./src/assets"],
|
||||||
|
["@localizations", "./localization/paraglide"],
|
||||||
["@", "./src"],
|
["@", "./src"],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,31 +45,39 @@
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<meta name="apple-mobile-web-app-title" content="JetKVM" />
|
<meta name="apple-mobile-web-app-title" content="JetKVM" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/public/site.webmanifest" />
|
||||||
<meta name="theme-color" content="#051946" />
|
<meta name="theme-color" content="#051946" />
|
||||||
<meta name="description" content="A web-based KVM console for managing remote servers." />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A web-based KVM console for managing remote servers."
|
||||||
|
/>
|
||||||
<script>
|
<script>
|
||||||
function applyThemeFromPreference() {
|
function applyThemeFromPreference() {
|
||||||
// dark theme setup
|
// dark theme setup
|
||||||
var darkDesired = localStorage.theme === "dark" ||
|
var darkDesired =
|
||||||
|
localStorage.theme === "dark" ||
|
||||||
(!("theme" in localStorage) &&
|
(!("theme" in localStorage) &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
|
||||||
document.documentElement.classList.toggle("dark", darkDesired)
|
document.documentElement.classList.toggle("dark", darkDesired);
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial theme application
|
// initial theme application
|
||||||
applyThemeFromPreference();
|
applyThemeFromPreference();
|
||||||
|
|
||||||
// Listen for system theme changes
|
// Listen for system theme changes
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyThemeFromPreference);
|
window
|
||||||
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", applyThemeFromPreference);
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.addEventListener("change", applyThemeFromPreference);
|
||||||
|
window
|
||||||
|
.matchMedia("(prefers-color-scheme: light)")
|
||||||
|
.addEventListener("change", applyThemeFromPreference);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased dark:bg-slate-900 md:text-base"
|
class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased md:text-base dark:bg-slate-900"
|
||||||
>
|
>
|
||||||
<div id="root" class="w-full h-full"></div>
|
<div id="root" class="h-full w-full"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cache
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
TI1a2RjjH4qkImNj0w
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/project-settings",
|
||||||
|
"baseLocale": "en",
|
||||||
|
"sourceLanguageTag": "en",
|
||||||
|
"locales": [
|
||||||
|
"en",
|
||||||
|
"da",
|
||||||
|
"de",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"nb",
|
||||||
|
"sv",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"languageTags": [
|
||||||
|
"en",
|
||||||
|
"da",
|
||||||
|
"de",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"nb",
|
||||||
|
"sv",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"modules": [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
|
||||||
|
],
|
||||||
|
"plugin.inlang.messageFormat": {
|
||||||
|
"pathPattern": "./messages/{locale}.json"
|
||||||
|
},
|
||||||
|
"strategy": [
|
||||||
|
"cookie",
|
||||||
|
"localStorage",
|
||||||
|
"preferredLanguage",
|
||||||
|
"baseLocale"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "Åh nej!",
|
||||||
|
"something_went_wrong": "Noget gik galt. Prøv igen senere, eller kontakt support.",
|
||||||
|
"jetkvm_logo": "JetKVM-logo",
|
||||||
|
"load": "Indlæs",
|
||||||
|
"unknown_error": "Ukendt fejl",
|
||||||
|
"action_bar_virtual_media": "Virtuelle medier",
|
||||||
|
"action_bar_paste_text": "Indsæt tekst",
|
||||||
|
"action_bar_web_terminal": "Webterminal",
|
||||||
|
"action_bar_wake_on_lan": "Vågn på LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Virtuelt tastatur",
|
||||||
|
"action_bar_extension": "Udvidelse",
|
||||||
|
"action_bar_connection_stats": "Forbindelsesstatistik",
|
||||||
|
"action_bar_settings": "Indstillinger",
|
||||||
|
"action_bar_fullscreen": "Fuldskærm",
|
||||||
|
"action_bar_exit_fullscreen": "Afslut fuldskærm",
|
||||||
|
"extensions_popover_extensions": "Udvidelser",
|
||||||
|
"extension_popover_set_error_notification": "Kunne ikke angive aktiv udvidelse: {error}",
|
||||||
|
"extension_popover_unload_extension": "Fjern udvidelse",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Indlæs og administrer dine udvidelser",
|
||||||
|
"extensions_atx_power_control": "ATX-strømstyring",
|
||||||
|
"extensions_atx_power_control_description": "Styr din maskines strømtilstand via ATX-strømstyring.",
|
||||||
|
"extensions_dc_power_control": "DC-strømstyring",
|
||||||
|
"extensions_dc_power_control_description": "Styr din DC-strømforlænger",
|
||||||
|
"extension_serial_console": "Seriel konsol",
|
||||||
|
"extension_serial_console_description": "Få adgang til din serielle konsoludvidelse",
|
||||||
|
"atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}",
|
||||||
|
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Magt",
|
||||||
|
"atx_power_control_short_power_button": "Kort tryk",
|
||||||
|
"atx_power_control_long_power_button": "Langt tryk",
|
||||||
|
"atx_power_control_reset_button": "Nulstil",
|
||||||
|
"atx_power_control_power_led": "Strøm-LED",
|
||||||
|
"atx_power_control_hdd_led": "HDD-LED",
|
||||||
|
"dc_power_control_get_state_error": "Kunne ikke hente DC-strømtilstand: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Kunne ikke sende DC-strømstatus til {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Kunne ikke sende DC-strømgendannelsesstatus til {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Tænd",
|
||||||
|
"dc_power_control_power_off_button": "Sluk",
|
||||||
|
"dc_power_control_restore_power_state": "Gendan strømtab",
|
||||||
|
"dc_power_control_power_on_state": "Tænd",
|
||||||
|
"dc_power_control_power_off_state": "Sluk",
|
||||||
|
"dc_power_control_voltage": "Spænding",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Strøm",
|
||||||
|
"dc_power_control_current_unit": "A",
|
||||||
|
"dc_power_control_power": "Magt",
|
||||||
|
"dc_power_control_power_unit": "W",
|
||||||
|
"serial_console_get_state_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
|
||||||
|
"serial_console_set_power_state_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Konfigurer dine serielle konsolindstillinger",
|
||||||
|
"serial_console_open_console": "Åbn konsol",
|
||||||
|
"serial_console_baud_rate": "Baudhastighed",
|
||||||
|
"serial_console_data_bits": "Databits",
|
||||||
|
"serial_console_stop_bits": "Stopbits",
|
||||||
|
"serial_console_parity": "Paritet",
|
||||||
|
"serial_console_parity_even": "Lige paritet",
|
||||||
|
"serial_console_parity_odd": "Ulige paritet",
|
||||||
|
"serial_console_parity_none": "Ingen paritet",
|
||||||
|
"serial_console_parity_mark": "Mark Paritet",
|
||||||
|
"serial_console_parity_space": "Rumparitet",
|
||||||
|
"serial_console_get_settings_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
|
||||||
|
"serial_console_set_settings_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "Oh nein!",
|
||||||
|
"something_went_wrong": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an den Support.",
|
||||||
|
"jetkvm_logo": "JetKVM Logo",
|
||||||
|
"load": "Laden",
|
||||||
|
"unknown_error": "Unbekannter Fehler",
|
||||||
|
"action_bar_virtual_media": "Virtuelle Medien",
|
||||||
|
"action_bar_paste_text": "Text einfügen",
|
||||||
|
"action_bar_web_terminal": "Web-Terminal",
|
||||||
|
"action_bar_wake_on_lan": "Wake-on-LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Virtuelle Tastatur",
|
||||||
|
"action_bar_extension": "Verlängerung",
|
||||||
|
"action_bar_connection_stats": "Verbindungsstatistiken",
|
||||||
|
"action_bar_settings": "Einstellungen",
|
||||||
|
"action_bar_fullscreen": "Vollbild",
|
||||||
|
"action_bar_exit_fullscreen": "Vollbildmodus beenden",
|
||||||
|
"extensions_popover_extensions": "Erweiterungen",
|
||||||
|
"extension_popover_set_error_notification": "Fehler beim Festlegen der aktiven Erweiterung: {error}",
|
||||||
|
"extension_popover_unload_extension": "Erweiterung entladen",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Laden und verwalten Sie Ihre Erweiterungen",
|
||||||
|
"extensions_atx_power_control": "ATX-Stromsteuerung",
|
||||||
|
"extensions_atx_power_control_description": "Steuern Sie den Energiezustand Ihrer Maschine über die ATX-Energiesteuerung.",
|
||||||
|
"extensions_dc_power_control": "Gleichstromsteuerung",
|
||||||
|
"extensions_dc_power_control_description": "Steuern Sie Ihre DC-Stromerweiterung",
|
||||||
|
"extension_serial_console": "Serielle Konsole",
|
||||||
|
"extension_serial_console_description": "Greifen Sie auf Ihre serielle Konsolenerweiterung zu",
|
||||||
|
"atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}",
|
||||||
|
"atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}",
|
||||||
|
"atx_power_control_power_button": "Leistung",
|
||||||
|
"atx_power_control_short_power_button": "Kurzes Drücken",
|
||||||
|
"atx_power_control_long_power_button": "Langes Drücken",
|
||||||
|
"atx_power_control_reset_button": "Zurücksetzen",
|
||||||
|
"atx_power_control_power_led": "Betriebs-LED",
|
||||||
|
"atx_power_control_hdd_led": "Festplatten-LED",
|
||||||
|
"dc_power_control_get_state_error": "Der Gleichstromstatus konnte nicht abgerufen werden: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Der DC-Stromversorgungsstatus konnte nicht an {enabled} werden: {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Der Status zur Wiederherstellung der Gleichstromversorgung konnte nicht an {state} gesendet werden: {error}",
|
||||||
|
"dc_power_control_power_on_button": "Einschalten",
|
||||||
|
"dc_power_control_power_off_button": "Ausschalten",
|
||||||
|
"dc_power_control_restore_power_state": "Wiederherstellung nach Stromausfall",
|
||||||
|
"dc_power_control_power_on_state": "Einschalten",
|
||||||
|
"dc_power_control_power_off_state": "Ausschalten",
|
||||||
|
"dc_power_control_voltage": "Stromspannung",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Aktuell",
|
||||||
|
"dc_power_control_current_unit": "A",
|
||||||
|
"dc_power_control_power": "Leistung",
|
||||||
|
"dc_power_control_power_unit": "W",
|
||||||
|
"serial_console_get_state_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
|
||||||
|
"serial_console_set_power_state_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}",
|
||||||
|
"serial_console_configure_description": "Konfigurieren Sie die Einstellungen Ihrer seriellen Konsole",
|
||||||
|
"serial_console_open_console": "Konsole öffnen",
|
||||||
|
"serial_console_baud_rate": "Baudrate",
|
||||||
|
"serial_console_data_bits": "Datenbits",
|
||||||
|
"serial_console_stop_bits": "Stoppbits",
|
||||||
|
"serial_console_parity": "Parität",
|
||||||
|
"serial_console_parity_even": "Gerade Parität",
|
||||||
|
"serial_console_parity_odd": "Ungerade Parität",
|
||||||
|
"serial_console_parity_none": "Keine Parität",
|
||||||
|
"serial_console_parity_mark": "Parität markieren",
|
||||||
|
"serial_console_parity_space": "Raumparität",
|
||||||
|
"serial_console_get_settings_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
|
||||||
|
"serial_console_set_settings_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"jetkvm_logo": "JetKVM Logo",
|
||||||
|
"attach": "Attach",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"connect_to_kvm": "Connect to KVM",
|
||||||
|
"default": "Default",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deregister_from_cloud": "Deregister from cloud",
|
||||||
|
"detach": "Detach",
|
||||||
|
"dhcp_server": "DHCP Server",
|
||||||
|
"dns_servers": "DNS Servers",
|
||||||
|
"hide": "Hide",
|
||||||
|
"info_caps_lock": "Caps Lock",
|
||||||
|
"info_compose": "Compose",
|
||||||
|
"info_hdmi_state": "HDMI State:",
|
||||||
|
"info_hidrpc_state": "HidRPC State:",
|
||||||
|
"info_kana": "Kana",
|
||||||
|
"info_keys": "Keys:",
|
||||||
|
"info_last_move": "Last Move:",
|
||||||
|
"info_num_lock": "Num Lock",
|
||||||
|
"info_paste_enabled": "Enabled",
|
||||||
|
"info_paste_mode": "Paste Mode:",
|
||||||
|
"info_pointer": "Pointer:",
|
||||||
|
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
|
||||||
|
"info_resolution": "Resolution:",
|
||||||
|
"info_scroll_lock": "Scroll Lock",
|
||||||
|
"info_shift": "Shift",
|
||||||
|
"info_usb_state": "USB State:",
|
||||||
|
"info_video_size": "Video Size:",
|
||||||
|
"input_disabled": "Input disabled",
|
||||||
|
"ip_address": "IP Address",
|
||||||
|
"last_online": "Last online {time}",
|
||||||
|
"load": "Load",
|
||||||
|
"log_out": "Log out",
|
||||||
|
"logged_in_as": "Logged in as",
|
||||||
|
"never_seen_online": "Never seen online",
|
||||||
|
"no_results_found": "No results found",
|
||||||
|
"not_available": "N/A",
|
||||||
|
"not_found": "Not found",
|
||||||
|
"ntp_servers": "NTP Servers",
|
||||||
|
"oh_no": "Oh no!",
|
||||||
|
"online": "Online",
|
||||||
|
"page_not_found_description": "The page you were looking for does not exist.",
|
||||||
|
"rename": "Rename",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"search_placeholder": "Search...",
|
||||||
|
"something_went_wrong": "Something went wrong. Please try again later or contact support",
|
||||||
|
"subnet_mask": "Subnet Mask",
|
||||||
|
"troubleshoot_connection": "Troubleshoot Connection",
|
||||||
|
"unknown_error": "Unknown error",
|
||||||
|
"update_in_progress": "Update in Progress",
|
||||||
|
"updating_leave_device_on": "Please don't turn off your device...",
|
||||||
|
"usb": "USB",
|
||||||
|
"view_details": "View Details",
|
||||||
|
"action_bar_connection_stats": "Connection Stats",
|
||||||
|
"action_bar_exit_fullscreen": "Exit Fullscreen",
|
||||||
|
"action_bar_extension": "Extension",
|
||||||
|
"action_bar_fullscreen": "Fullscreen",
|
||||||
|
"action_bar_paste_text": "Paste text",
|
||||||
|
"action_bar_settings": "Settings",
|
||||||
|
"action_bar_virtual_keyboard": "Virtual Keyboard",
|
||||||
|
"action_bar_virtual_media": "Virtual Media",
|
||||||
|
"action_bar_wake_on_lan": "Wake on LAN",
|
||||||
|
"action_bar_web_terminal": "Web Terminal",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Load and manage your extensions",
|
||||||
|
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
|
||||||
|
"extension_popover_unload_extension": "Unload Extension",
|
||||||
|
"extension_serial_console_description": "Access your serial console extension",
|
||||||
|
"extension_serial_console": "Serial Console",
|
||||||
|
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
|
||||||
|
"extensions_atx_power_control": "ATX Power Control",
|
||||||
|
"extensions_dc_power_control_description": "Control your DC Power extension",
|
||||||
|
"extensions_dc_power_control": "DC Power Control",
|
||||||
|
"extensions_popover_extensions": "Extensions",
|
||||||
|
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
|
||||||
|
"atx_power_control_hdd_led": "HDD LED",
|
||||||
|
"atx_power_control_long_power_button": "Long Press",
|
||||||
|
"atx_power_control_power_button": "Power",
|
||||||
|
"atx_power_control_power_led": "Power LED",
|
||||||
|
"atx_power_control_reset_button": "Reset",
|
||||||
|
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
|
||||||
|
"atx_power_control_short_power_button": "Short Press",
|
||||||
|
"dc_power_control_current_unit": "A",
|
||||||
|
"dc_power_control_current": "Current",
|
||||||
|
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
|
||||||
|
"dc_power_control_power_off_button": "Power Off",
|
||||||
|
"dc_power_control_power_off_state": "Power OFF",
|
||||||
|
"dc_power_control_power_on_button": "Power On",
|
||||||
|
"dc_power_control_power_on_state": "Power ON",
|
||||||
|
"dc_power_control_power_unit": "W",
|
||||||
|
"dc_power_control_power": "Power",
|
||||||
|
"dc_power_control_restore_last_state": "Last State",
|
||||||
|
"dc_power_control_restore_power_state": "Restore Power Loss",
|
||||||
|
"dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_voltage": "Voltage",
|
||||||
|
"serial_console_baud_rate": "Baud Rate",
|
||||||
|
"serial_console_configure_description": "Configure your serial console settings",
|
||||||
|
"serial_console_data_bits": "Data Bits",
|
||||||
|
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
|
||||||
|
"serial_console_open_console": "Open Console",
|
||||||
|
"serial_console_parity_even": "Even Parity",
|
||||||
|
"serial_console_parity_mark": "Mark Parity",
|
||||||
|
"serial_console_parity_none": "No Parity",
|
||||||
|
"serial_console_parity_odd": "Odd Parity",
|
||||||
|
"serial_console_parity_space": "Space Parity",
|
||||||
|
"serial_console_parity": "Parity",
|
||||||
|
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
|
||||||
|
"serial_console_stop_bits": "Stop Bits",
|
||||||
|
"wake_on_lan_add_device_back": "Back",
|
||||||
|
"wake_on_lan_add_device_device_name": "Device Name",
|
||||||
|
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
|
||||||
|
"wake_on_lan_add_device_mac_address": "MAC Address",
|
||||||
|
"wake_on_lan_add_device_save_device": "Save Device",
|
||||||
|
"paste_modal_confirm_paste": "Confirm Paste",
|
||||||
|
"paste_modal_delay_between_keys": "Delay between keys",
|
||||||
|
"paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
|
||||||
|
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
|
||||||
|
"paste_modal_paste_from_host": "Paste from host",
|
||||||
|
"paste_modal_paste_text_description": "Paste text from your client to the remote host",
|
||||||
|
"paste_modal_paste_text": "Paste text",
|
||||||
|
"paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
|
||||||
|
"paste_modal_failed_paste": "Failed to paste text: {error}",
|
||||||
|
"mount_add_file_to_get_started": "Add a file to get started",
|
||||||
|
"mount_add_new_media": "Add New Media",
|
||||||
|
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
|
||||||
|
"mount_no_mounted_media": "No mounted media",
|
||||||
|
"mount_streaming_from_url": "Streaming from URL",
|
||||||
|
"mount_unmount": "Unmount",
|
||||||
|
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
|
||||||
|
"mount_virtual_media": "Virtual Media",
|
||||||
|
"mount_get_state_error": "Failed to get virtual media state: {error}",
|
||||||
|
"mount_mode_cdrom": "CD-ROM",
|
||||||
|
"mount_mode_disk": "Disk",
|
||||||
|
"mount_mounted_as": "Mounted as",
|
||||||
|
"mount_unmount_error": "Failed to unmount image: {error}",
|
||||||
|
"wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
|
||||||
|
"wake_on_lan_device_list_add_new_device": "Add New Device",
|
||||||
|
"wake_on_lan_device_list_delete_device": "Delete device",
|
||||||
|
"wake_on_lan_device_list_wake": "Wake",
|
||||||
|
"wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
|
||||||
|
"wake_on_lan_empty_add_new_device": "Add New Device",
|
||||||
|
"wake_on_lan_empty_no_devices_added": "No devices added",
|
||||||
|
"wake_on_lan_failed_add_device": "Failed to add device",
|
||||||
|
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
|
||||||
|
"wake_on_lan_invalid_mac": "Invalid MAC address",
|
||||||
|
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
|
||||||
|
"wake_on_lan": "Wake On LAN",
|
||||||
|
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
|
||||||
|
"connection_stats_badge_jitter": "Jitter",
|
||||||
|
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
|
||||||
|
"connection_stats_connection": "Connection",
|
||||||
|
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
|
||||||
|
"connection_stats_frames_per_second": "Frames per second",
|
||||||
|
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
|
||||||
|
"connection_stats_network_stability": "Network Stability",
|
||||||
|
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
|
||||||
|
"connection_stats_packets_lost": "Packets Lost",
|
||||||
|
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
|
||||||
|
"connection_stats_playback_delay": "Playback Delay",
|
||||||
|
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
|
||||||
|
"connection_stats_round_trip_time": "Round-Trip Time",
|
||||||
|
"connection_stats_sidebar": "Connection Stats",
|
||||||
|
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
|
||||||
|
"connection_stats_video": "Video",
|
||||||
|
"peer_connection_connected": "Connected",
|
||||||
|
"peer_connection_connecting": "Connecting",
|
||||||
|
"peer_connection_disconnected": "Disconnected",
|
||||||
|
"peer_connection_error": "Connection error",
|
||||||
|
"peer_connection_closing": "Closing",
|
||||||
|
"peer_connection_failed": "Connection failed",
|
||||||
|
"peer_connection_closed": "Closed",
|
||||||
|
"peer_connection_new": "Connecting",
|
||||||
|
"usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
|
||||||
|
"usb_device_keyboard_only": "Keyboard Only",
|
||||||
|
"usb_device_custom": "Custom",
|
||||||
|
"usb_device_title": "USB Device",
|
||||||
|
"usb_device_description": "USB devices to emulate on the target computer",
|
||||||
|
"usb_device_classes_title": "Classes",
|
||||||
|
"usb_device_classes_description": "USB device classes in the composite device",
|
||||||
|
"usb_device_enable_keyboard_title": "Enable Keyboard",
|
||||||
|
"usb_device_enable_keyboard_description": "Enable Keyboard",
|
||||||
|
"usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
|
||||||
|
"usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
|
||||||
|
"usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
|
||||||
|
"usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
|
||||||
|
"usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
|
||||||
|
"usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
|
||||||
|
"usb_device_update_classes": "Update USB Classes",
|
||||||
|
"usb_device_restore_default": "Restore to Default",
|
||||||
|
"usb_device_failed_load": "Failed to load USB devices: {error}",
|
||||||
|
"usb_device_failed_set": "Failed to set USB devices: {error}",
|
||||||
|
"usb_device_updated": "USB Devices updated",
|
||||||
|
"updates_failed_check": "Failed to check for updates: {error}",
|
||||||
|
"updates_failed_get_device_version": "Failed to get device version: {error}",
|
||||||
|
"auth_connect_to_cloud_action": "Log in & Connect device",
|
||||||
|
"auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
|
||||||
|
"auth_connect_to_cloud": "Connect your JetKVM to the cloud",
|
||||||
|
"auth_signup_connect_to_cloud_action": "Signup & Connect device",
|
||||||
|
"auth_login_action": "Log in",
|
||||||
|
"auth_login_description": "Log in to access and manage your devices securely",
|
||||||
|
"auth_login": "Log in to your JetKVM account",
|
||||||
|
"auth_header_cta_already_have_account": "Already have an account?",
|
||||||
|
"auth_header_cta_dont_have_account": "Don't have an account?",
|
||||||
|
"auth_header_cta_new_to_jetkvm": "New to JetKVM?",
|
||||||
|
"auth_signup_create_account_action": "Create Account",
|
||||||
|
"auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
|
||||||
|
"auth_signup_create_account": "Create your JetKVM account",
|
||||||
|
"video_overlay_autoplay_permissions_required": "Autoplay permissions required",
|
||||||
|
"video_overlay_conn_check_cables": "Check all cable connections for any loose or damaged wires",
|
||||||
|
"video_overlay_conn_ensure_network": "Ensure your network connection is stable and active",
|
||||||
|
"video_overlay_conn_restart": "Try restarting both the device and your computer",
|
||||||
|
"video_overlay_conn_verify_power": "Verify that the device is powered on and properly connected",
|
||||||
|
"video_overlay_connection_issue_title": "Connection Issue Detected",
|
||||||
|
"video_overlay_enable_autoplay_settings": "Please adjust browser settings to enable autoplay",
|
||||||
|
"video_overlay_hdmi_error_title": "HDMI signal error detected.",
|
||||||
|
"video_overlay_hdmi_incompatible_resolution": "Incompatible resolution or refresh rate settings",
|
||||||
|
"video_overlay_hdmi_loose_faulty": "A loose or faulty HDMI connection",
|
||||||
|
"video_overlay_hdmi_source_issue": "Issues with the source device's HDMI output",
|
||||||
|
"video_overlay_learn_more": "Learn more",
|
||||||
|
"video_overlay_loading_stream": "Loading video stream...",
|
||||||
|
"video_overlay_manually_start_stream": "Manually start stream",
|
||||||
|
"video_overlay_no_hdmi_adapter_compat": "If using an adapter, ensure it's compatible and functioning correctly",
|
||||||
|
"video_overlay_no_hdmi_ensure_cable": "Ensure the HDMI cable securely connected at both ends",
|
||||||
|
"video_overlay_no_hdmi_ensure_power": "Ensure source device is powered on and outputting a signal",
|
||||||
|
"video_overlay_no_hdmi_signal": "No HDMI signal detected.",
|
||||||
|
"video_overlay_pointerlock_click_to_enable": "Click on the video to enable mouse control",
|
||||||
|
"video_overlay_retrying_connection": "Retrying connection...",
|
||||||
|
"video_overlay_troubleshooting_guide": "Troubleshooting Guide",
|
||||||
|
"video_overlay_try_again": "Try again",
|
||||||
|
"video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
|
||||||
|
"video_pointer_lock_disabled": "Pointer lock disabled",
|
||||||
|
"ipv6_information": "IPv6 Information",
|
||||||
|
"ipv6_link_local": "Link-local",
|
||||||
|
"ipv6_addresses": "IPv6 Addresses",
|
||||||
|
"ipv6_address_label": "Address",
|
||||||
|
"ipv6_valid_lifetime": "Valid Lifetime",
|
||||||
|
"ipv6_preferred_lifetime": "Preferred Lifetime",
|
||||||
|
"dhcp_lease_boot_file": "Boot File",
|
||||||
|
"dhcp_lease_boot_next_server": "Boot Next Server",
|
||||||
|
"dhcp_lease_boot_server_name": "Boot Server Name",
|
||||||
|
"dhcp_lease_broadcast": "Broadcast",
|
||||||
|
"dhcp_lease_domain": "Domain",
|
||||||
|
"dhcp_lease_gateway": "Gateway",
|
||||||
|
"dhcp_lease_header": "DHCP Lease Information",
|
||||||
|
"dhcp_lease_hostname": "Hostname",
|
||||||
|
"dhcp_lease_lease_expires": "Lease Expires",
|
||||||
|
"dhcp_lease_maximum_transfer_unit": "MTU",
|
||||||
|
"dhcp_lease_renew": "Renew DHCP Lease",
|
||||||
|
"dhcp_lease_time_to_live": "TTL",
|
||||||
|
"step_counter_step": "Step {step}",
|
||||||
|
"macro_add_step": "Add Step{maxed_out}",
|
||||||
|
"macro_at_least_one_step_keys_or_modifiers": "At least one step must have keys or modifiers",
|
||||||
|
"macro_at_least_one_step_required": "At least one step is required",
|
||||||
|
"macro_max_steps_error": "You can only add a maximum of {max} steps per macro.",
|
||||||
|
"macro_max_steps_reached": "({max} max)",
|
||||||
|
"macro_name_label": "Macro Name",
|
||||||
|
"macro_name_required": "Name is required",
|
||||||
|
"macro_name_too_long": "Name must be less than 50 characters",
|
||||||
|
"macro_please_fix_validation_errors": "Please fix the validation errors",
|
||||||
|
"macro_save": "Save Macro",
|
||||||
|
"macro_save_error": "An error occurred while saving.",
|
||||||
|
"macro_step_count": "{steps} / {max} steps",
|
||||||
|
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
|
||||||
|
"macro_steps_label": "Steps",
|
||||||
|
"macro_step_duration_description": "Time to wait before executing the next step.",
|
||||||
|
"macro_step_duration_label": "Step Duration",
|
||||||
|
"macro_step_keys_description": "Maximum {max} keys per step.",
|
||||||
|
"macro_step_keys_label": "Keys",
|
||||||
|
"macro_step_max_keys_reached": "Maximum keys reached",
|
||||||
|
"macro_step_modifiers_description": "What modifiers (Shift/Ctrl/Alt/Meta) are pressed during this step.",
|
||||||
|
"macro_step_modifiers_label": "Modifiers",
|
||||||
|
"macro_step_no_matching_keys_found": "No matching keys found",
|
||||||
|
"macro_step_search_for_key": "Search for key...",
|
||||||
|
"jiggler_examples_label": "Examples",
|
||||||
|
"jiggler_more_examples": "More examples",
|
||||||
|
"jiggler_cron_schedule_label": "Cron Schedule",
|
||||||
|
"jiggler_cron_schedule_description": "Cron expression for scheduling",
|
||||||
|
"jiggler_example_business_hours_late": "Business Hours 9-17",
|
||||||
|
"jiggler_example_business_hours_early": "Business Hours 8-17",
|
||||||
|
"jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
|
||||||
|
"jiggler_inactivity_limit_description": "Inactivity time before jiggle",
|
||||||
|
"jiggler_random_delay_label": "Random delay",
|
||||||
|
"jiggler_random_delay_description": "To avoid recognizable patterns",
|
||||||
|
"jiggler_timezone_label": "Timezone",
|
||||||
|
"jiggler_timezone_description": "Timezone for cron schedule",
|
||||||
|
"jiggler_save_jiggler_config": "Save Jiggler Config",
|
||||||
|
"metric_waiting_for_data": "Waiting for data...",
|
||||||
|
"metric_not_supported": "Metric not supported",
|
||||||
|
"usb_config_custom": "Custom",
|
||||||
|
"usb_config_default": "JetKVM Default",
|
||||||
|
"usb_config_dell": "Dell Multimedia Pro Keyboard",
|
||||||
|
"usb_config_failed_load": "Failed to load USB Config: {error}",
|
||||||
|
"usb_config_failed_set": "Failed to set usb config: {error}",
|
||||||
|
"usb_config_identifiers_description": "USB device identifiers exposed to the target computer",
|
||||||
|
"usb_config_identifiers_title": "Identifiers",
|
||||||
|
"usb_config_logitech": "Logitech Universal Adapter",
|
||||||
|
"usb_config_manufacturer_label": "Manufacturer",
|
||||||
|
"usb_config_manufacturer_placeholder": "Enter Manufacturer",
|
||||||
|
"usb_config_microsoft": "Microsoft Wireless MultiMedia Keyboard",
|
||||||
|
"usb_config_product_id_label": "Product ID",
|
||||||
|
"usb_config_product_id_placeholder": "Enter Product ID",
|
||||||
|
"usb_config_product_name_label": "Product Name",
|
||||||
|
"usb_config_product_name_placeholder": "Enter Product Name",
|
||||||
|
"usb_config_restore_default": "Restore to Default",
|
||||||
|
"usb_config_serial_number_label": "Serial Number",
|
||||||
|
"usb_config_serial_number_placeholder": "Enter Serial Number",
|
||||||
|
"usb_config_set_success": "USB Config set to {manufacturer} {product}",
|
||||||
|
"usb_config_update_identifiers": "Update USB Identifiers",
|
||||||
|
"usb_config_vendor_id_label": "Vendor ID",
|
||||||
|
"usb_config_vendor_id_placeholder": "Enter Vendor ID",
|
||||||
|
"usb_state_connected": "Connected",
|
||||||
|
"usb_state_connecting": "Connecting",
|
||||||
|
"usb_state_disconnected": "Disconnected",
|
||||||
|
"usb_state_low_power_mode": "Low power mode",
|
||||||
|
"virtual_keyboard_header": "Virtual Keyboard",
|
||||||
|
"virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer."
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "¡Oh, no!",
|
||||||
|
"something_went_wrong": "Algo salió mal. Inténtalo de nuevo más tarde o contacta con el servicio de asistencia.",
|
||||||
|
"jetkvm_logo": "Logotipo de JetKVM",
|
||||||
|
"load": "Carga",
|
||||||
|
"unknown_error": "Error desconocido",
|
||||||
|
"action_bar_virtual_media": "Medios virtuales",
|
||||||
|
"action_bar_paste_text": "Pegar texto",
|
||||||
|
"action_bar_web_terminal": "Terminal web",
|
||||||
|
"action_bar_wake_on_lan": "Activación en LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Teclado virtual",
|
||||||
|
"action_bar_extension": "Extensión",
|
||||||
|
"action_bar_connection_stats": "Estadísticas de conexión",
|
||||||
|
"action_bar_settings": "Ajustes",
|
||||||
|
"action_bar_fullscreen": "Pantalla completa",
|
||||||
|
"action_bar_exit_fullscreen": "Salir de pantalla completa",
|
||||||
|
"extensions_popover_extensions": "Extensiones",
|
||||||
|
"extension_popover_set_error_notification": "No se pudo establecer la extensión activa: {error}",
|
||||||
|
"extension_popover_unload_extension": "Extensión de descarga",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Cargar y administrar sus extensiones",
|
||||||
|
"extensions_atx_power_control": "Control de alimentación ATX",
|
||||||
|
"extensions_atx_power_control_description": "Controle el estado de energía de su máquina a través del control de energía ATX.",
|
||||||
|
"extensions_dc_power_control": "Control de potencia de CC",
|
||||||
|
"extensions_dc_power_control_description": "Controle su extensión de alimentación de CC",
|
||||||
|
"extension_serial_console": "Consola serial",
|
||||||
|
"extension_serial_console_description": "Acceda a la extensión de su consola serie",
|
||||||
|
"atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}",
|
||||||
|
"atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Fuerza",
|
||||||
|
"atx_power_control_short_power_button": "Prensa corta",
|
||||||
|
"atx_power_control_long_power_button": "Pulsación larga",
|
||||||
|
"atx_power_control_reset_button": "Reiniciar",
|
||||||
|
"atx_power_control_power_led": "LED de encendido",
|
||||||
|
"atx_power_control_hdd_led": "LED del disco duro",
|
||||||
|
"dc_power_control_get_state_error": "No se pudo obtener el estado de la alimentación de CC: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "No se pudo enviar el estado de alimentación de CC a {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "No se pudo enviar el estado de restauración de energía de CC a {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Encendido",
|
||||||
|
"dc_power_control_power_off_button": "Apagado",
|
||||||
|
"dc_power_control_restore_power_state": "Restaurar pérdida de energía",
|
||||||
|
"dc_power_control_power_on_state": "Encendido",
|
||||||
|
"dc_power_control_power_off_state": "Apagado",
|
||||||
|
"dc_power_control_voltage": "Voltaje",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Amperio",
|
||||||
|
"dc_power_control_current_unit": "A",
|
||||||
|
"dc_power_control_power": "Vatio",
|
||||||
|
"dc_power_control_power_unit": "O",
|
||||||
|
"serial_console_get_state_error": "No se pudo obtener la configuración de la consola serial: {error}",
|
||||||
|
"serial_console_set_power_state_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Configure los ajustes de su consola serie",
|
||||||
|
"serial_console_open_console": "Consola abierta",
|
||||||
|
"serial_console_baud_rate": "Tasa de Baud",
|
||||||
|
"serial_console_data_bits": "Bits de datos",
|
||||||
|
"serial_console_stop_bits": "Bits de parada",
|
||||||
|
"serial_console_parity": "Paridad",
|
||||||
|
"serial_console_parity_even": "Paridad uniforme",
|
||||||
|
"serial_console_parity_odd": "Paridad impar",
|
||||||
|
"serial_console_parity_none": "Sin paridad",
|
||||||
|
"serial_console_parity_mark": "Paridad de marca",
|
||||||
|
"serial_console_parity_space": "Paridad espacial",
|
||||||
|
"serial_console_get_settings_error": "No se pudo obtener la configuración de la consola serial: {error}",
|
||||||
|
"serial_console_set_settings_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "Oh non!",
|
||||||
|
"something_went_wrong": "Une erreur s'est produite. Veuillez réessayer ultérieurement ou contacter le support.",
|
||||||
|
"jetkvm_logo": "Logo JetKVM",
|
||||||
|
"load": "Charger",
|
||||||
|
"unknown_error": "Erreur inconnue",
|
||||||
|
"action_bar_virtual_media": "Médias virtuels",
|
||||||
|
"action_bar_paste_text": "Coller du texte",
|
||||||
|
"action_bar_web_terminal": "Terminal Web",
|
||||||
|
"action_bar_wake_on_lan": "Réveil sur LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Clavier virtuel",
|
||||||
|
"action_bar_extension": "Extension",
|
||||||
|
"action_bar_connection_stats": "Statistiques de connexion",
|
||||||
|
"action_bar_settings": "Paramètres",
|
||||||
|
"action_bar_fullscreen": "Plein écran",
|
||||||
|
"action_bar_exit_fullscreen": "Quitter le plein écran",
|
||||||
|
"extensions_popover_extensions": "Extensions",
|
||||||
|
"extension_popover_set_error_notification": "Échec de la définition de l'extension active: {error}",
|
||||||
|
"extension_popover_unload_extension": "Extension de déchargement",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Chargez et gérez vos extensions",
|
||||||
|
"extensions_atx_power_control": "Contrôle d'alimentation ATX",
|
||||||
|
"extensions_atx_power_control_description": "Contrôlez l'état d'alimentation de votre machine via le contrôle d'alimentation ATX.",
|
||||||
|
"extensions_dc_power_control": "Contrôle de l'alimentation CC",
|
||||||
|
"extensions_dc_power_control_description": "Contrôlez votre extension d'alimentation CC",
|
||||||
|
"extension_serial_console": "Console série",
|
||||||
|
"extension_serial_console_description": "Accédez à votre extension de console série",
|
||||||
|
"atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}",
|
||||||
|
"atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Pouvoir",
|
||||||
|
"atx_power_control_short_power_button": "Appui court",
|
||||||
|
"atx_power_control_long_power_button": "Appui long",
|
||||||
|
"atx_power_control_reset_button": "Réinitialiser",
|
||||||
|
"atx_power_control_power_led": "LED d'alimentation",
|
||||||
|
"atx_power_control_hdd_led": "Voyant du disque dur",
|
||||||
|
"dc_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation CC : {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Échec de l'envoi de l'état d'alimentation CC à {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Échec de l'envoi de l'état de restauration de l'alimentation CC à {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Mise sous tension",
|
||||||
|
"dc_power_control_power_off_button": "Éteindre",
|
||||||
|
"dc_power_control_restore_power_state": "Restaurer la perte de puissance",
|
||||||
|
"dc_power_control_power_on_state": "Mise sous tension",
|
||||||
|
"dc_power_control_power_off_state": "Éteindre",
|
||||||
|
"dc_power_control_voltage": "Tension",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Ampère",
|
||||||
|
"dc_power_control_current_unit": "UN",
|
||||||
|
"dc_power_control_power": "Pouvoir",
|
||||||
|
"dc_power_control_power_unit": "W",
|
||||||
|
"serial_console_get_state_error": "Échec de l'obtention des paramètres de la console série : {error}",
|
||||||
|
"serial_console_set_power_state_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Configurez les paramètres de votre console série",
|
||||||
|
"serial_console_open_console": "Ouvrir la console",
|
||||||
|
"serial_console_baud_rate": "Débit en bauds",
|
||||||
|
"serial_console_data_bits": "Bits de données",
|
||||||
|
"serial_console_stop_bits": "Bits d'arrêt",
|
||||||
|
"serial_console_parity": "Parité",
|
||||||
|
"serial_console_parity_even": "Parité égale",
|
||||||
|
"serial_console_parity_odd": "Parité impaire",
|
||||||
|
"serial_console_parity_none": "Pas de parité",
|
||||||
|
"serial_console_parity_mark": "Marquer la parité",
|
||||||
|
"serial_console_parity_space": "Parité spatiale",
|
||||||
|
"serial_console_get_settings_error": "Échec de l'obtention des paramètres de la console série : {error}",
|
||||||
|
"serial_console_set_settings_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "Oh no!",
|
||||||
|
"something_went_wrong": "Qualcosa è andato storto. Riprova più tardi o contatta l'assistenza.",
|
||||||
|
"jetkvm_logo": "Logo JetKVM",
|
||||||
|
"load": "Carico",
|
||||||
|
"unknown_error": "Errore sconosciuto",
|
||||||
|
"action_bar_virtual_media": "Media virtuali",
|
||||||
|
"action_bar_paste_text": "Incolla il testo",
|
||||||
|
"action_bar_web_terminal": "Terminale Web",
|
||||||
|
"action_bar_wake_on_lan": "Wake on LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Tastiera virtuale",
|
||||||
|
"action_bar_extension": "Estensione",
|
||||||
|
"action_bar_connection_stats": "Statistiche di connessione",
|
||||||
|
"action_bar_settings": "Impostazioni",
|
||||||
|
"action_bar_fullscreen": "A schermo intero",
|
||||||
|
"action_bar_exit_fullscreen": "Esci dalla modalità a schermo intero",
|
||||||
|
"extensions_popover_extensions": "Estensioni",
|
||||||
|
"extension_popover_set_error_notification": "Impossibile impostare l'estensione attiva: {error}",
|
||||||
|
"extension_popover_unload_extension": "Estensione di scaricamento",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Carica e gestisci le tue estensioni",
|
||||||
|
"extensions_atx_power_control": "Controllo di potenza ATX",
|
||||||
|
"extensions_atx_power_control_description": "Controlla lo stato di alimentazione del tuo computer tramite il controllo di alimentazione ATX.",
|
||||||
|
"extensions_dc_power_control": "Controllo di potenza CC",
|
||||||
|
"extensions_dc_power_control_description": "Controlla la tua estensione di alimentazione CC",
|
||||||
|
"extension_serial_console": "Console seriale",
|
||||||
|
"extension_serial_console_description": "Accedi all'estensione della tua console seriale",
|
||||||
|
"atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}",
|
||||||
|
"atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Energia",
|
||||||
|
"atx_power_control_short_power_button": "Pressione breve",
|
||||||
|
"atx_power_control_long_power_button": "Pressione lunga",
|
||||||
|
"atx_power_control_reset_button": "Reset",
|
||||||
|
"atx_power_control_power_led": "LED di potenza",
|
||||||
|
"atx_power_control_hdd_led": "LED dell'HDD",
|
||||||
|
"dc_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione CC: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Impossibile inviare lo stato di alimentazione CC a {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Impossibile inviare lo stato di ripristino dell'alimentazione CC a {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Accensione",
|
||||||
|
"dc_power_control_power_off_button": "Spegnimento",
|
||||||
|
"dc_power_control_restore_power_state": "Ripristinare la perdita di potenza",
|
||||||
|
"dc_power_control_power_on_state": "Accensione",
|
||||||
|
"dc_power_control_power_off_state": "Spegnimento",
|
||||||
|
"dc_power_control_voltage": "Voltaggio",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Attuale",
|
||||||
|
"dc_power_control_current_unit": "UN",
|
||||||
|
"dc_power_control_power": "Energia",
|
||||||
|
"dc_power_control_power_unit": "O",
|
||||||
|
"serial_console_get_state_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
|
||||||
|
"serial_console_set_power_state_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Configura le impostazioni della tua console seriale",
|
||||||
|
"serial_console_open_console": "Apri console",
|
||||||
|
"serial_console_baud_rate": "Velocità in baud",
|
||||||
|
"serial_console_data_bits": "Bit di dati",
|
||||||
|
"serial_console_stop_bits": "Bit di stop",
|
||||||
|
"serial_console_parity": "Parità",
|
||||||
|
"serial_console_parity_even": "Parità pari",
|
||||||
|
"serial_console_parity_odd": "Parità dispari",
|
||||||
|
"serial_console_parity_none": "Nessuna parità",
|
||||||
|
"serial_console_parity_mark": "Segna la parità",
|
||||||
|
"serial_console_parity_space": "Parità spaziale",
|
||||||
|
"serial_console_get_settings_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
|
||||||
|
"serial_console_set_settings_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "Å nei!",
|
||||||
|
"something_went_wrong": "Noe gikk galt. Prøv igjen senere, eller kontakt kundestøtte.",
|
||||||
|
"jetkvm_logo": "JetKVM-logo",
|
||||||
|
"load": "Laste",
|
||||||
|
"unknown_error": "Ukjent feil",
|
||||||
|
"action_bar_virtual_media": "Virtuelle medier",
|
||||||
|
"action_bar_paste_text": "Lim inn tekst",
|
||||||
|
"action_bar_web_terminal": "Nettterminal",
|
||||||
|
"action_bar_wake_on_lan": "Vekk på LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Virtuelt tastatur",
|
||||||
|
"action_bar_extension": "Forlengelse",
|
||||||
|
"action_bar_connection_stats": "Tilkoblingsstatistikk",
|
||||||
|
"action_bar_settings": "Innstillinger",
|
||||||
|
"action_bar_fullscreen": "Fullskjerm",
|
||||||
|
"action_bar_exit_fullscreen": "Avslutt fullskjerm",
|
||||||
|
"extensions_popover_extensions": "Utvidelser",
|
||||||
|
"extension_popover_set_error_notification": "Klarte ikke å angi aktiv utvidelse: {error}",
|
||||||
|
"extension_popover_unload_extension": "Fjern utvidelse",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Last inn og administrer utvidelsene dine",
|
||||||
|
"extensions_atx_power_control": "ATX-strømstyring",
|
||||||
|
"extensions_atx_power_control_description": "Kontroller maskinens strømstatus via ATX-strømkontroll.",
|
||||||
|
"extensions_dc_power_control": "DC-strømkontroll",
|
||||||
|
"extensions_dc_power_control_description": "Kontroller DC-strømutvidelsen din",
|
||||||
|
"extension_serial_console": "Seriell konsoll",
|
||||||
|
"extension_serial_console_description": "Få tilgang til seriekonsollutvidelsen din",
|
||||||
|
"atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}",
|
||||||
|
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Makt",
|
||||||
|
"atx_power_control_short_power_button": "Kort trykk",
|
||||||
|
"atx_power_control_long_power_button": "Langt trykk",
|
||||||
|
"atx_power_control_reset_button": "Tilbakestill",
|
||||||
|
"atx_power_control_power_led": "Strøm-LED",
|
||||||
|
"atx_power_control_hdd_led": "HDD-LED",
|
||||||
|
"dc_power_control_get_state_error": "Klarte ikke å hente likestrømsstatus: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Kunne ikke sende likestrømsstatus til {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Kunne ikke sende gjenopprettingsstatus for likestrøm til {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Slå på",
|
||||||
|
"dc_power_control_power_off_button": "Slå av",
|
||||||
|
"dc_power_control_restore_power_state": "Gjenopprett strømtap",
|
||||||
|
"dc_power_control_power_on_state": "Slå PÅ",
|
||||||
|
"dc_power_control_power_off_state": "Slå av",
|
||||||
|
"dc_power_control_voltage": "Spenning",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Nåværende",
|
||||||
|
"dc_power_control_current_unit": "EN",
|
||||||
|
"dc_power_control_power": "Makt",
|
||||||
|
"dc_power_control_power_unit": "V",
|
||||||
|
"serial_console_get_state_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
|
||||||
|
"serial_console_set_power_state_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Konfigurer innstillingene for seriekonsollen",
|
||||||
|
"serial_console_open_console": "Åpne konsollen",
|
||||||
|
"serial_console_baud_rate": "Baudhastighet",
|
||||||
|
"serial_console_data_bits": "Databiter",
|
||||||
|
"serial_console_stop_bits": "Stoppbiter",
|
||||||
|
"serial_console_parity": "Paritet",
|
||||||
|
"serial_console_parity_even": "Paritet",
|
||||||
|
"serial_console_parity_odd": "Oddeparitet",
|
||||||
|
"serial_console_parity_none": "Ingen paritet",
|
||||||
|
"serial_console_parity_mark": "Mark Paritet",
|
||||||
|
"serial_console_parity_space": "Romparitet",
|
||||||
|
"serial_console_get_settings_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
|
||||||
|
"serial_console_set_settings_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "nej då!",
|
||||||
|
"something_went_wrong": "Något gick fel. Försök igen senare eller kontakta supporten.",
|
||||||
|
"jetkvm_logo": "JetKVM-logotyp",
|
||||||
|
"load": "Ladda",
|
||||||
|
"unknown_error": "Okänt fel",
|
||||||
|
"action_bar_virtual_media": "Virtuella medier",
|
||||||
|
"action_bar_paste_text": "Klistra in text",
|
||||||
|
"action_bar_web_terminal": "Webbterminal",
|
||||||
|
"action_bar_wake_on_lan": "Vakna på LAN",
|
||||||
|
"action_bar_virtual_keyboard": "Virtuellt tangentbord",
|
||||||
|
"action_bar_extension": "Förlängning",
|
||||||
|
"action_bar_connection_stats": "Anslutningsstatistik",
|
||||||
|
"action_bar_settings": "Inställningar",
|
||||||
|
"action_bar_fullscreen": "Helskärm",
|
||||||
|
"action_bar_exit_fullscreen": "Avsluta helskärm",
|
||||||
|
"extensions_popover_extensions": "Tillägg",
|
||||||
|
"extension_popover_set_error_notification": "Misslyckades med att ange aktivt tillägg: {error}",
|
||||||
|
"extension_popover_unload_extension": "Avlasta tillägg",
|
||||||
|
"extension_popover_load_and_manage_extensions": "Ladda och hantera dina tillägg",
|
||||||
|
"extensions_atx_power_control": "ATX-strömkontroll",
|
||||||
|
"extensions_atx_power_control_description": "Styr din maskins strömförsörjning via ATX-strömkontroll.",
|
||||||
|
"extensions_dc_power_control": "DC-strömstyrning",
|
||||||
|
"extensions_dc_power_control_description": "Styr din DC-strömförlängning",
|
||||||
|
"extension_serial_console": "Seriell konsol",
|
||||||
|
"extension_serial_console_description": "Åtkomst till din seriella konsoltillägg",
|
||||||
|
"atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}",
|
||||||
|
"atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "Driva",
|
||||||
|
"atx_power_control_short_power_button": "Kort tryck",
|
||||||
|
"atx_power_control_long_power_button": "Långt tryck",
|
||||||
|
"atx_power_control_reset_button": "Återställa",
|
||||||
|
"atx_power_control_power_led": "Ström-LED",
|
||||||
|
"atx_power_control_hdd_led": "Hårddisk-LED",
|
||||||
|
"dc_power_control_get_state_error": "Misslyckades med att hämta likströmsstatus: {error}",
|
||||||
|
"dc_power_control_set_power_state_error": "Misslyckades med att skicka likströmsstatus till {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "Misslyckades med att skicka återställningsstatus för likström till {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "Slå på",
|
||||||
|
"dc_power_control_power_off_button": "Stäng av",
|
||||||
|
"dc_power_control_restore_power_state": "Återställ strömförlust",
|
||||||
|
"dc_power_control_power_on_state": "Slå på",
|
||||||
|
"dc_power_control_power_off_state": "Stäng av",
|
||||||
|
"dc_power_control_voltage": "Spänning",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "Nuvarande",
|
||||||
|
"dc_power_control_current_unit": "En",
|
||||||
|
"dc_power_control_power": "Driva",
|
||||||
|
"dc_power_control_power_unit": "V",
|
||||||
|
"serial_console_get_state_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
|
||||||
|
"serial_console_set_power_state_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "Konfigurera dina seriella konsolinställningar",
|
||||||
|
"serial_console_open_console": "Öppna konsolen",
|
||||||
|
"serial_console_baud_rate": "Baudhastighet",
|
||||||
|
"serial_console_data_bits": "Databitar",
|
||||||
|
"serial_console_stop_bits": "Stoppbitar",
|
||||||
|
"serial_console_parity": "Paritet",
|
||||||
|
"serial_console_parity_even": "Jämn paritet",
|
||||||
|
"serial_console_parity_odd": "Udda paritet",
|
||||||
|
"serial_console_parity_none": "Ingen paritet",
|
||||||
|
"serial_console_parity_mark": "Markera paritet",
|
||||||
|
"serial_console_parity_space": "Rymdparitet",
|
||||||
|
"serial_console_get_settings_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
|
||||||
|
"serial_console_set_settings_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"jetkvm": "JetKVM",
|
||||||
|
"oh_no": "噢不!",
|
||||||
|
"something_went_wrong": "出了点问题。请稍后重试或联系客服",
|
||||||
|
"jetkvm_logo": "JetKVM 徽标",
|
||||||
|
"load": "加载",
|
||||||
|
"unknown_error": "未知错误",
|
||||||
|
"action_bar_virtual_media": "虚拟媒体",
|
||||||
|
"action_bar_paste_text": "粘贴文本",
|
||||||
|
"action_bar_web_terminal": "网页终端",
|
||||||
|
"action_bar_wake_on_lan": "局域网唤醒",
|
||||||
|
"action_bar_virtual_keyboard": "虚拟键盘",
|
||||||
|
"action_bar_extension": "扩展",
|
||||||
|
"action_bar_connection_stats": "连接统计",
|
||||||
|
"action_bar_settings": "设置",
|
||||||
|
"action_bar_fullscreen": "全屏",
|
||||||
|
"action_bar_exit_fullscreen": "退出全屏",
|
||||||
|
"extensions_popover_extensions": "扩展",
|
||||||
|
"extension_popover_set_error_notification": "无法设置活动扩展:{error}",
|
||||||
|
"extension_popover_unload_extension": "卸载扩展",
|
||||||
|
"extension_popover_load_and_manage_extensions": "加载和管理您的扩展",
|
||||||
|
"extensions_atx_power_control": "ATX 电源控制",
|
||||||
|
"extensions_atx_power_control_description": "通过 ATX 电源控制来控制机器的电源状态。",
|
||||||
|
"extensions_dc_power_control": "直流电源控制",
|
||||||
|
"extensions_dc_power_control_description": "控制您的直流电源扩展",
|
||||||
|
"extension_serial_console": "串行控制台",
|
||||||
|
"extension_serial_console_description": "访问串行控制台扩展",
|
||||||
|
"atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}",
|
||||||
|
"atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}",
|
||||||
|
"atx_power_control_power_button": "电源",
|
||||||
|
"atx_power_control_short_power_button": "短按",
|
||||||
|
"atx_power_control_long_power_button": "长按",
|
||||||
|
"atx_power_control_reset_button": "重置",
|
||||||
|
"atx_power_control_power_led": "电源 LED",
|
||||||
|
"atx_power_control_hdd_led": "硬盘指示灯",
|
||||||
|
"dc_power_control_get_state_error": "无法获取直流电源状态:{error}",
|
||||||
|
"dc_power_control_set_power_state_error": "无法将直流电源状态发送到 {enabled} : {error}",
|
||||||
|
"dc_power_control_set_restore_state_error": "无法将直流电源恢复状态发送到 {state} : {error}",
|
||||||
|
"dc_power_control_power_on_button": "开机",
|
||||||
|
"dc_power_control_power_off_button": "关闭电源",
|
||||||
|
"dc_power_control_restore_power_state": "恢复断电",
|
||||||
|
"dc_power_control_power_on_state": "开启电源",
|
||||||
|
"dc_power_control_power_off_state": "关闭电源",
|
||||||
|
"dc_power_control_voltage": "电压",
|
||||||
|
"dc_power_control_voltage_unit": "V",
|
||||||
|
"dc_power_control_current": "电流",
|
||||||
|
"dc_power_control_current_unit": "A",
|
||||||
|
"dc_power_control_power": "瓦特",
|
||||||
|
"dc_power_control_power_unit": "W",
|
||||||
|
"serial_console_get_state_error": "无法获取串行控制台设置: {error}",
|
||||||
|
"serial_console_set_power_state_error": "无法将串行控制台设置设置为 {settings} : {error}",
|
||||||
|
"serial_console_configure_description": "配置串行控制台设置",
|
||||||
|
"serial_console_open_console": "打开控制台",
|
||||||
|
"serial_console_baud_rate": "波特率",
|
||||||
|
"serial_console_data_bits": "数据位",
|
||||||
|
"serial_console_stop_bits": "停止位",
|
||||||
|
"serial_console_parity": "奇偶校验位",
|
||||||
|
"serial_console_parity_even": "偶校验",
|
||||||
|
"serial_console_parity_odd": "奇校验",
|
||||||
|
"serial_console_parity_none": "无",
|
||||||
|
"serial_console_parity_mark": "Mark",
|
||||||
|
"serial_console_parity_space": "Space",
|
||||||
|
"serial_console_get_settings_error": "无法获取串行控制台设置: {error}",
|
||||||
|
"serial_console_set_settings_error": "无法将串行控制台设置设置为 {settings} : {error}"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.10.01.1900",
|
"version": "2025.10.07.1700",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.15.0"
|
"node": "^22.15.0"
|
||||||
|
|
@ -11,12 +11,14 @@
|
||||||
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
||||||
"dev:cloud": "vite dev --mode=cloud-development",
|
"dev:cloud": "vite dev --mode=cloud-development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "npm run paraglide && tsc && vite build --mode=device --emptyOutDir",
|
||||||
"build:staging": "tsc && vite build --mode=cloud-staging",
|
"build:staging": "npm run paraglide && tsc && vite build --mode=cloud-staging",
|
||||||
"build:prod": "tsc && vite build --mode=cloud-production",
|
"build:prod": "npm run paraglide && tsc && vite build --mode=cloud-production",
|
||||||
"lint": "eslint './src/**/*.{ts,tsx}'",
|
"lint": "npm run paraglide && eslint './src/**/*.{ts,tsx}'",
|
||||||
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix",
|
"lint:fix": "npm run paraglide && eslint './src/**/*.{ts,tsx}' --fix",
|
||||||
"preview": "vite preview"
|
"paraglide": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
|
||||||
|
"validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
|
||||||
|
"machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "^2.2.9",
|
||||||
|
|
@ -36,13 +38,13 @@
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
"react-simple-keyboard": "^3.8.125",
|
"react-simple-keyboard": "^3.8.126",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
|
|
@ -54,32 +56,37 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.0",
|
"@eslint/compat": "^1.4.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.37.0",
|
||||||
|
"@inlang/cli": "^3.0.12",
|
||||||
|
"@inlang/paraglide-js": "^2.4.0",
|
||||||
|
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
||||||
|
"@inlang/plugin-message-format": "^4.0.0",
|
||||||
|
"@inlang/sdk": "^2.4.9",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/react": "^19.1.17",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.10",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.3",
|
"@types/validator": "^13.15.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||||
"@typescript-eslint/parser": "^8.45.0",
|
"@typescript-eslint/parser": "^8.46.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^6.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.22",
|
"eslint-plugin-react-refresh": "^0.4.23",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.9",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { MdOutlineContentPasteGo } from "react-icons/md";
|
import { MdOutlineContentPasteGo } from "react-icons/md";
|
||||||
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||||
import { FaKeyboard } from "react-icons/fa6";
|
import { FaKeyboard } from "react-icons/fa6";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
} from "@/hooks/stores";
|
} from "@hooks/stores";
|
||||||
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { cx } from "@/cva.config";
|
import PasteModal from "@components/popovers/PasteModal";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import WakeOnLanModal from "@components/popovers/WakeOnLan/Index";
|
||||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
import MountPopopover from "@components/popovers/MountPopover";
|
||||||
import MountPopopover from "@/components/popovers/MountPopover";
|
import ExtensionPopover from "@components/popovers/ExtensionPopover";
|
||||||
import ExtensionPopover from "@/components/popovers/ExtensionPopover";
|
import { m } from "@localizations/messages.js";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
|
||||||
|
|
||||||
export default function Actionbar({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
|
@ -28,10 +29,7 @@ export default function Actionbar({
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||||
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
|
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
|
||||||
|
const { remoteVirtualMediaState } = useMountMediaStore();
|
||||||
const remoteVirtualMediaState = useMountMediaStore(
|
|
||||||
state => state.remoteVirtualMediaState,
|
|
||||||
);
|
|
||||||
const { developerMode } = useSettingsStore();
|
const { developerMode } = useSettingsStore();
|
||||||
|
|
||||||
// This is the only way to get a reliable state change for the popover
|
// This is the only way to get a reliable state change for the popover
|
||||||
|
|
@ -64,7 +62,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Web Terminal"
|
text={m.action_bar_web_terminal()}
|
||||||
LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
|
LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
|
||||||
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
|
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
|
||||||
/>
|
/>
|
||||||
|
|
@ -74,7 +72,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Paste text"
|
text={m.action_bar_paste_text()}
|
||||||
LeadingIcon={MdOutlineContentPasteGo}
|
LeadingIcon={MdOutlineContentPasteGo}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
|
|
@ -105,7 +103,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Virtual Media"
|
text={m.action_bar_virtual_media()}
|
||||||
LeadingIcon={({ className }) => {
|
LeadingIcon={({ className }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -148,7 +146,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Wake on LAN"
|
text={m.action_bar_wake_on_lan()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}}
|
}}
|
||||||
|
|
@ -198,7 +196,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Virtual Keyboard"
|
text={m.action_bar_virtual_keyboard()}
|
||||||
LeadingIcon={FaKeyboard}
|
LeadingIcon={FaKeyboard}
|
||||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -211,7 +209,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Extension"
|
text={m.action_bar_extension()}
|
||||||
LeadingIcon={LuCable}
|
LeadingIcon={LuCable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
|
|
@ -237,7 +235,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Virtual Keyboard"
|
text={m.action_bar_virtual_keyboard()}
|
||||||
LeadingIcon={FaKeyboard}
|
LeadingIcon={FaKeyboard}
|
||||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -246,7 +244,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Connection Stats"
|
text={m.action_bar_connection_stats()}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<LuSignal
|
<LuSignal
|
||||||
className={cx(className, "mb-0.5 text-green-500")}
|
className={cx(className, "mb-0.5 text-green-500")}
|
||||||
|
|
@ -262,7 +260,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Settings"
|
text={m.action_bar_settings()}
|
||||||
LeadingIcon={LuSettings}
|
LeadingIcon={LuSettings}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
|
|
@ -276,7 +274,7 @@ export default function Actionbar({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Fullscreen"
|
text={m.action_bar_fullscreen()}
|
||||||
LeadingIcon={LuMaximize}
|
LeadingIcon={LuMaximize}
|
||||||
onClick={() => requestFullscreen()}
|
onClick={() => requestFullscreen()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import { Link, useNavigation } from "react-router";
|
import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
|
||||||
import type { FetcherWithComponents, LinkProps } from "react-router";
|
|
||||||
|
|
||||||
import ExtLink from "@/components/ExtLink";
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
|
||||||
import { cva, cx } from "@/cva.config";
|
import { cva, cx } from "@/cva.config";
|
||||||
|
|
||||||
|
import ExtLink from "@components/ExtLink";
|
||||||
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
XS: "h-[28px] px-2 text-xs",
|
XS: "h-[28px] px-2 text-xs",
|
||||||
SM: "h-[36px] px-3 text-[13px]",
|
SM: "h-[36px] px-3 text-[13px]",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { Ref } from "react";
|
||||||
import React, { forwardRef, JSX } from "react";
|
import React, { forwardRef, JSX } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import { cva, cx } from "@/cva.config";
|
import { cva, cx } from "@/cva.config";
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import {
|
||||||
ComboboxOptions,
|
ComboboxOptions,
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
import Card from "@components/Card";
|
||||||
import { cva } from "@/cva.config";
|
import { cva } from "@/cva.config";
|
||||||
|
|
||||||
import Card from "./Card";
|
|
||||||
|
|
||||||
export interface ComboboxOption {
|
export interface ComboboxOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -44,11 +44,11 @@ export function Combobox({
|
||||||
displayValue,
|
displayValue,
|
||||||
options,
|
options,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = "Search...",
|
placeholder = m.search_placeholder(),
|
||||||
emptyMessage = "No results found",
|
emptyMessage = m.no_results_found(),
|
||||||
size = "MD",
|
size = "MD",
|
||||||
onChange,
|
onChange,
|
||||||
disabledMessage = "Input disabled",
|
disabledMessage = m.input_disabled(),
|
||||||
...otherProps
|
...otherProps
|
||||||
}: ComboboxProps) {
|
}: ComboboxProps) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import {
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { m } from "@localizations/messages.js";
|
||||||
import Modal from "@/components/Modal";
|
import { Button } from "@components/Button";
|
||||||
|
import Modal from "@components/Modal";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type Variant = "danger" | "success" | "warning" | "info";
|
type Variant = "danger" | "success" | "warning" | "info";
|
||||||
|
|
@ -63,8 +64,8 @@ export function ConfirmDialog({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
variant = "info",
|
variant = "info",
|
||||||
confirmText = "Confirm",
|
confirmText = m.confirm(),
|
||||||
cancelText = "Cancel",
|
cancelText = m.cancel(),
|
||||||
onConfirm,
|
onConfirm,
|
||||||
isConfirming = false,
|
isConfirming = false,
|
||||||
}: ConfirmDialogProps) {
|
}: ConfirmDialogProps) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { LuRefreshCcw } from "react-icons/lu";
|
import { LuRefreshCcw } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
|
||||||
import { NetworkState } from "@/hooks/stores";
|
import { NetworkState } from "@hooks/stores";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function DhcpLeaseCard({
|
export default function DhcpLeaseCard({
|
||||||
networkState,
|
networkState,
|
||||||
|
|
@ -17,7 +18,7 @@ export default function DhcpLeaseCard({
|
||||||
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
DHCP Lease Information
|
{m.dhcp_lease_header()}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex gap-x-6 gap-y-2">
|
<div className="flex gap-x-6 gap-y-2">
|
||||||
|
|
@ -25,7 +26,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.ip && (
|
{networkState?.dhcp_lease?.ip && (
|
||||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
IP Address
|
{m.ip_address()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.ip}
|
{networkState?.dhcp_lease?.ip}
|
||||||
|
|
@ -36,7 +37,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.netmask && (
|
{networkState?.dhcp_lease?.netmask && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Subnet Mask
|
{m.subnet_mask()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.netmask}
|
{networkState?.dhcp_lease?.netmask}
|
||||||
|
|
@ -47,7 +48,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.dns && (
|
{networkState?.dhcp_lease?.dns && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
DNS Servers
|
{m.dns_servers()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right text-sm font-medium">
|
<span className="text-right text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
||||||
|
|
@ -58,7 +59,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.broadcast && (
|
{networkState?.dhcp_lease?.broadcast && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Broadcast
|
{m.dhcp_lease_broadcast()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.broadcast}
|
{networkState?.dhcp_lease?.broadcast}
|
||||||
|
|
@ -69,7 +70,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.domain && (
|
{networkState?.dhcp_lease?.domain && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Domain
|
{m.dhcp_lease_domain()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.domain}
|
{networkState?.dhcp_lease?.domain}
|
||||||
|
|
@ -81,7 +82,7 @@ export default function DhcpLeaseCard({
|
||||||
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
||||||
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||||
NTP Servers
|
{m.ntp_servers()}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink text-right text-sm font-medium">
|
<div className="shrink text-right text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||||
|
|
@ -94,7 +95,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.hostname && (
|
{networkState?.dhcp_lease?.hostname && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Hostname
|
{m.dhcp_lease_hostname()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.hostname}
|
{networkState?.dhcp_lease?.hostname}
|
||||||
|
|
@ -108,7 +109,7 @@ export default function DhcpLeaseCard({
|
||||||
networkState?.dhcp_lease?.routers.length > 0 && (
|
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||||
<div className="flex justify-between pt-2">
|
<div className="flex justify-between pt-2">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Gateway
|
{m.dhcp_lease_gateway()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right text-sm font-medium">
|
<span className="text-right text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.routers.map(router => (
|
{networkState?.dhcp_lease?.routers.map(router => (
|
||||||
|
|
@ -121,7 +122,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.server_id && (
|
{networkState?.dhcp_lease?.server_id && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
DHCP Server
|
{m.dhcp_server()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.server_id}
|
{networkState?.dhcp_lease?.server_id}
|
||||||
|
|
@ -132,7 +133,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.lease_expiry && (
|
{networkState?.dhcp_lease?.lease_expiry && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Lease Expires
|
{m.dhcp_lease_lease_expires()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
<LifeTimeLabel
|
<LifeTimeLabel
|
||||||
|
|
@ -146,7 +147,7 @@ export default function DhcpLeaseCard({
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.mtu}
|
{m.dhcp_lease_maximum_transfer_unit()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -155,7 +156,7 @@ export default function DhcpLeaseCard({
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
|
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.ttl}
|
{m.dhcp_lease_time_to_live()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -163,7 +164,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.bootp_next_server && (
|
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Boot Next Server
|
{m.dhcp_lease_boot_next_server()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.bootp_next_server}
|
{networkState?.dhcp_lease?.bootp_next_server}
|
||||||
|
|
@ -174,7 +175,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.bootp_server_name && (
|
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Boot Server Name
|
{m.dhcp_lease_boot_server_name()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.bootp_server_name}
|
{networkState?.dhcp_lease?.bootp_server_name}
|
||||||
|
|
@ -185,7 +186,7 @@ export default function DhcpLeaseCard({
|
||||||
{networkState?.dhcp_lease?.bootp_file && (
|
{networkState?.dhcp_lease?.bootp_file && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Boot File
|
{m.dhcp_lease_boot_file()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.bootp_file}
|
{networkState?.dhcp_lease?.bootp_file}
|
||||||
|
|
@ -194,13 +195,12 @@ export default function DhcpLeaseCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
className="text-red-500"
|
className="text-red-500"
|
||||||
text="Renew DHCP Lease"
|
text={m.dhcp_lease_renew()}
|
||||||
LeadingIcon={LuRefreshCcw}
|
LeadingIcon={LuRefreshCcw}
|
||||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
import { cx } from "../cva.config";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
IconElm?: React.FC<{ className: string | undefined }>;
|
IconElm?: React.FC<{ className: string | undefined }>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
import { useFeatureFlag } from "@hooks/useFeatureFlag";
|
||||||
|
|
||||||
export function FeatureFlag({
|
export function FeatureFlag({
|
||||||
minAppVersion,
|
minAppVersion,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
|
||||||
import { useNavigation } from "react-router";
|
import { useNavigation } from "react-router";
|
||||||
import type { FetcherWithComponents } from "react-router";
|
import type { FetcherWithComponents } from "react-router";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function Fieldset({
|
export default function Fieldset({
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,19 @@ import { useNavigate } from "react-router";
|
||||||
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
|
import LogoBlueIcon from "@assets/logo-blue.svg";
|
||||||
|
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||||
|
|
||||||
import Container from "@/components/Container";
|
import { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
|
||||||
import Card from "@/components/Card";
|
import Card from "@components/Card";
|
||||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
import Container from "@components/Container";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
import { LinkButton } from "@components/Button";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
|
||||||
import USBStateStatus from "@components/USBStateStatus";
|
|
||||||
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||||
|
import USBStateStatus from "@components/USBStateStatus";
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
import api from "@/api";
|
||||||
import api from "../api";
|
import { isOnDevice } from "@/main";
|
||||||
import { isOnDevice } from "../main";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
import { LinkButton } from "./Button";
|
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
|
@ -131,7 +130,7 @@ export default function DashboardNavbar({
|
||||||
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<div className="font-display text-xs">
|
<div className="font-display text-xs">
|
||||||
Logged in as
|
{m.logged_in_as()}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
|
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
|
||||||
{userEmail}
|
{userEmail}
|
||||||
|
|
@ -146,7 +145,7 @@ export default function DashboardNavbar({
|
||||||
>
|
>
|
||||||
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||||
<ArrowLeftEndOnRectangleIcon className="size-4" />
|
<ArrowLeftEndOnRectangleIcon className="size-4" />
|
||||||
<div className="font-display">Log out</div>
|
<div className="font-display">{m.log_out()}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMouseStore,
|
useMouseStore,
|
||||||
|
|
@ -8,9 +7,11 @@ import {
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
VideoState
|
VideoState
|
||||||
} from "@/hooks/stores";
|
} from "@hooks/stores";
|
||||||
|
import { useHidRpc } from "@hooks/useHidRpc";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
import { useHidRpc } from "@/hooks/useHidRpc";
|
import { cx } from "@/cva.config";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function InfoBar() {
|
export default function InfoBar() {
|
||||||
const { keysDownState } = useHidStore();
|
const { keysDownState } = useHidStore();
|
||||||
|
|
@ -41,13 +42,16 @@ export default function InfoBar() {
|
||||||
const { hdmiState } = useVideoStore();
|
const { hdmiState } = useVideoStore();
|
||||||
|
|
||||||
const displayKeys = useMemo(() => {
|
const displayKeys = useMemo(() => {
|
||||||
if (!showPressedKeys)
|
if (!showPressedKeys) return "";
|
||||||
return "";
|
|
||||||
|
|
||||||
const activeModifierMask = keysDownState.modifier || 0;
|
const activeModifierMask = keysDownState.modifier || 0;
|
||||||
const keysDown = keysDownState.keys || [];
|
const keysDown = keysDownState.keys || [];
|
||||||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
const modifierNames = Object.entries(modifiers)
|
||||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
|
||||||
|
.map(([name]) => name);
|
||||||
|
const keyNames = Object.entries(keys)
|
||||||
|
.filter(([_, value]) => keysDown.includes(value))
|
||||||
|
.map(([name]) => name);
|
||||||
|
|
||||||
return [...modifierNames, ...keyNames].join(", ");
|
return [...modifierNames, ...keyNames].join(", ");
|
||||||
}, [keysDownState, showPressedKeys]);
|
}, [keysDownState, showPressedKeys]);
|
||||||
|
|
@ -59,76 +63,75 @@ export default function InfoBar() {
|
||||||
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
||||||
{debugMode ? (
|
{debugMode ? (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-xs font-semibold">Resolution:</span>{" "}
|
<span className="text-xs font-semibold">{m.info_resolution()}</span>{" "}
|
||||||
<span className="text-xs">{videoSize}</span>
|
<span className="text-xs">{videoSize}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{debugMode ? (
|
{debugMode ? (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-xs font-semibold">Video Size: </span>
|
<span className="text-xs font-semibold">{m.info_video_size()}</span>
|
||||||
<span className="text-xs">{videoClientSize}</span>
|
<span className="text-xs">{videoClientSize}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(debugMode && mouseMode == "absolute") ? (
|
{(debugMode && mouseMode == "absolute") ? (
|
||||||
<div className="flex w-[118px] items-center gap-x-1">
|
<div className="flex w-[118px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Pointer:</span>
|
<span className="text-xs font-semibold">{m.info_pointer()}</span>
|
||||||
<span className="text-xs">
|
<span className="text-xs">{mouseX},{mouseY}</span>
|
||||||
{mouseX},{mouseY}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(debugMode && mouseMode == "relative") ? (
|
{(debugMode && mouseMode == "relative") ? (
|
||||||
<div className="flex w-[118px] items-center gap-x-1">
|
<div className="flex w-[118px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Last Move:</span>
|
<span className="text-xs font-semibold">{m.info_last_move()}</span>
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
{mouseMove ?
|
{mouseMove ? `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : "N/A"}
|
||||||
`${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` :
|
|
||||||
"N/A"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{debugMode && (
|
{debugMode && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">USB State:</span>
|
<span className="text-xs font-semibold">{m.info_usb_state()}</span>
|
||||||
<span className="text-xs">{usbState}</span>
|
<span className="text-xs">{usbState}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{debugMode && (
|
{debugMode && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">HDMI State:</span>
|
<span className="text-xs font-semibold">{m.info_hdmi_state()}</span>
|
||||||
<span className="text-xs">{hdmiState}</span>
|
<span className="text-xs">{hdmiState}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{debugMode && (
|
{debugMode && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">HidRPC State:</span>
|
<span className="text-xs font-semibold">{m.info_hidrpc_state()}</span>
|
||||||
<span className="text-xs">{rpcHidStatus}</span>
|
<span className="text-xs">{rpcHidStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPasteInProgress && (
|
{isPasteInProgress && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Paste Mode:</span>
|
<span className="text-xs font-semibold">{m.info_paste_mode()}</span>
|
||||||
<span className="text-xs">Enabled</span>
|
<span className="text-xs">{m.info_paste_enabled()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPressedKeys && (
|
{showPressedKeys && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Keys:</span>
|
<span className="text-xs font-semibold">{m.info_keys()}</span>
|
||||||
<h2 className="text-xs">
|
<h2 className="text-xs">{displayKeys}</h2>
|
||||||
{displayKeys}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
{isTurnServerInUse && (
|
{isTurnServerInUse && (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs text-black dark:text-white">
|
<div className="shrink-0 p-1 px-1.5 text-xs text-black dark:text-white">
|
||||||
Relayed by Cloudflare
|
{m.info_relayed_by_cloudflare()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -140,8 +143,9 @@ export default function InfoBar() {
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Caps Lock
|
{m.info_caps_lock()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
|
|
@ -150,8 +154,9 @@ export default function InfoBar() {
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Num Lock
|
{m.info_num_lock()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
|
|
@ -160,22 +165,19 @@ export default function InfoBar() {
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Scroll Lock
|
{m.info_scroll_lock()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{keyboardLedState.compose ? (
|
{keyboardLedState.compose ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_compose()}</div>
|
||||||
Compose
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{keyboardLedState.kana ? (
|
{keyboardLedState.kana ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
|
||||||
Kana
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{keyboardLedState.shift ? (
|
{keyboardLedState.shift ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_shift()}</div>
|
||||||
Shift
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { NetworkState } from "../hooks/stores";
|
import { NetworkState } from "@hooks/stores";
|
||||||
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
import { GridCard } from "@components/Card";
|
||||||
|
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
|
||||||
import { GridCard } from "./Card";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function Ipv6NetworkCard({
|
export default function Ipv6NetworkCard({
|
||||||
networkState,
|
networkState,
|
||||||
|
|
@ -13,14 +13,14 @@ export default function Ipv6NetworkCard({
|
||||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
IPv6 Information
|
{m.ipv6_information()}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||||
{networkState?.ipv6_link_local && (
|
{networkState?.ipv6_link_local && (
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Link-local
|
{m.ipv6_link_local()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{networkState?.ipv6_link_local}
|
{networkState?.ipv6_link_local}
|
||||||
|
|
@ -42,7 +42,7 @@ export default function Ipv6NetworkCard({
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||||
<div className="col-span-2 flex flex-col justify-between">
|
<div className="col-span-2 flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Address
|
{m.ipv6_address_label()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">{addr.address}</span>
|
<span className="text-sm font-medium">{addr.address}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -50,12 +50,12 @@ export default function Ipv6NetworkCard({
|
||||||
{addr.valid_lifetime && (
|
{addr.valid_lifetime && (
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Valid Lifetime
|
{m.ipv6_valid_lifetime()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{addr.valid_lifetime === "" ? (
|
{addr.valid_lifetime === "" ? (
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
N/A
|
{m.not_available()}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||||
|
|
@ -66,12 +66,12 @@ export default function Ipv6NetworkCard({
|
||||||
{addr.preferred_lifetime && (
|
{addr.preferred_lifetime && (
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Preferred Lifetime
|
{m.ipv6_preferred_lifetime()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{addr.preferred_lifetime === "" ? (
|
{addr.preferred_lifetime === "" ? (
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
N/A
|
{m.not_available()}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { LuExternalLink } from "react-icons/lu";
|
import { LuExternalLink } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { InputFieldWithLabel } from "./InputField";
|
import { m } from "@localizations/messages.js";
|
||||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
|
||||||
|
|
||||||
export interface JigglerConfig {
|
export interface JigglerConfig {
|
||||||
inactivity_limit_seconds: number;
|
inactivity_limit_seconds: number;
|
||||||
|
|
@ -51,7 +51,7 @@ export function JigglerSetting({
|
||||||
|
|
||||||
const exampleConfigs = [
|
const exampleConfigs = [
|
||||||
{
|
{
|
||||||
name: "Business Hours 9-17",
|
name: m.jiggler_example_business_hours_late(),
|
||||||
config: {
|
config: {
|
||||||
inactivity_limit_seconds: 60,
|
inactivity_limit_seconds: 60,
|
||||||
jitter_percentage: 25,
|
jitter_percentage: 25,
|
||||||
|
|
@ -60,7 +60,7 @@ export function JigglerSetting({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Business Hours 8-17",
|
name: m.jiggler_example_business_hours_early(),
|
||||||
config: {
|
config: {
|
||||||
inactivity_limit_seconds: 60,
|
inactivity_limit_seconds: 60,
|
||||||
jitter_percentage: 25,
|
jitter_percentage: 25,
|
||||||
|
|
@ -69,13 +69,10 @@ export function JigglerSetting({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{m.jiggler_examples_label()}</h4>
|
||||||
Examples
|
|
||||||
</h4>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{exampleConfigs.map((example, index) => (
|
{exampleConfigs.map((example, index) => (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -90,7 +87,7 @@ export function JigglerSetting({
|
||||||
to="https://crontab.guru/examples.html"
|
to="https://crontab.guru/examples.html"
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="More examples"
|
text={m.jiggler_more_examples()}
|
||||||
LeadingIcon={LuExternalLink}
|
LeadingIcon={LuExternalLink}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -100,8 +97,8 @@ export function JigglerSetting({
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
size="SM"
|
size="SM"
|
||||||
label="Cron Schedule"
|
label={m.jiggler_cron_schedule_label()}
|
||||||
description="Cron expression for scheduling"
|
description={m.jiggler_cron_schedule_description()}
|
||||||
placeholder="*/20 * * * * *"
|
placeholder="*/20 * * * * *"
|
||||||
value={jigglerConfigState.schedule_cron_tab}
|
value={jigglerConfigState.schedule_cron_tab}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
|
|
@ -114,8 +111,8 @@ export function JigglerSetting({
|
||||||
|
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
size="SM"
|
size="SM"
|
||||||
label="Inactivity Limit Seconds"
|
label={m.jiggler_inactivity_limit_label()}
|
||||||
description="Inactivity time before jiggle"
|
description={m.jiggler_inactivity_limit_description()}
|
||||||
value={jigglerConfigState.inactivity_limit_seconds}
|
value={jigglerConfigState.inactivity_limit_seconds}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
|
@ -131,8 +128,8 @@ export function JigglerSetting({
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
size="SM"
|
size="SM"
|
||||||
label="Random delay"
|
label={m.jiggler_random_delay_label()}
|
||||||
description="To avoid recognizable patterns"
|
description={m.jiggler_random_delay_description()}
|
||||||
placeholder="25"
|
placeholder="25"
|
||||||
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
|
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
|
||||||
value={jigglerConfigState.jitter_percentage}
|
value={jigglerConfigState.jitter_percentage}
|
||||||
|
|
@ -149,8 +146,8 @@ export function JigglerSetting({
|
||||||
|
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
label="Timezone"
|
label={m.jiggler_timezone_label()}
|
||||||
description="Timezone for cron schedule"
|
description={m.jiggler_timezone_description()}
|
||||||
value={jigglerConfigState.timezone || "UTC"}
|
value={jigglerConfigState.timezone || "UTC"}
|
||||||
disabled={timezones.length === 0}
|
disabled={timezones.length === 0}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
|
|
@ -167,7 +164,7 @@ export function JigglerSetting({
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Save Jiggler Config"
|
text={m.jiggler_save_jiggler_config()}
|
||||||
onClick={() => onSave(jigglerConfigState)}
|
onClick={() => onSave(jigglerConfigState)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { Link } from "react-router";
|
||||||
import { MdConnectWithoutContact } from "react-icons/md";
|
import { MdConnectWithoutContact } from "react-icons/md";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { Link } from "react-router";
|
|
||||||
import { LuEllipsisVertical } from "react-icons/lu";
|
import { LuEllipsisVertical } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
|
function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
|
||||||
// Allow dates or times to be passed
|
// Allow dates or times to be passed
|
||||||
|
|
@ -62,16 +63,16 @@ export default function KvmCard({
|
||||||
{online ? (
|
{online ? (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
<div className="h-2.5 w-2.5 rounded-full border border-green-600 bg-green-500" />
|
<div className="h-2.5 w-2.5 rounded-full border border-green-600 bg-green-500" />
|
||||||
<div className="text-sm text-black dark:text-white">Online</div>
|
<div className="text-sm text-black dark:text-white">{m.online()}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
<div className="h-2.5 w-2.5 rounded-full border border-slate-400/60 dark:border-slate-500 bg-slate-200 dark:bg-slate-600" />
|
<div className="h-2.5 w-2.5 rounded-full border border-slate-400/60 dark:border-slate-500 bg-slate-200 dark:bg-slate-600" />
|
||||||
<div className="text-sm text-black dark:text-white">
|
<div className="text-sm text-black dark:text-white">
|
||||||
{lastSeen ? (
|
{lastSeen ? (
|
||||||
<>Last online {getRelativeTimeString(lastSeen)}</>
|
<>{m.last_online({ time: getRelativeTimeString(lastSeen) })}</>
|
||||||
) : (
|
) : (
|
||||||
<>Never seen online</>
|
<>{m.never_seen_online()}</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -85,7 +86,7 @@ export default function KvmCard({
|
||||||
<LinkButton
|
<LinkButton
|
||||||
size="MD"
|
size="MD"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Connect to KVM"
|
text={m.connect_to_kvm()}
|
||||||
LeadingIcon={MdConnectWithoutContact}
|
LeadingIcon={MdConnectWithoutContact}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
to={`/devices/${id}`}
|
to={`/devices/${id}`}
|
||||||
|
|
@ -94,7 +95,7 @@ export default function KvmCard({
|
||||||
<Button
|
<Button
|
||||||
size="MD"
|
size="MD"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Troubleshoot Connection"
|
text={m.troubleshoot_connection()}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { LuCommand } from "react-icons/lu";
|
import { LuCommand } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { useMacrosStore } from "@hooks/stores";
|
||||||
|
import useKeyboard from "@hooks/useKeyboard";
|
||||||
|
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { useMacrosStore } from "@/hooks/stores";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
|
|
||||||
export default function MacroBar() {
|
export default function MacroBar() {
|
||||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { LuPlus } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { KeySequence } from "@hooks/stores";
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||||
import Fieldset from "@/components/Fieldset";
|
import { Button } from "@components/Button";
|
||||||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import { MacroStepCard } from "@/components/MacroStepCard";
|
import Fieldset from "@components/Fieldset";
|
||||||
|
import { InputFieldWithLabel, FieldError } from "@components/InputField";
|
||||||
|
import { MacroStepCard } from "@components/MacroStepCard";
|
||||||
import {
|
import {
|
||||||
DEFAULT_DELAY,
|
DEFAULT_DELAY,
|
||||||
MAX_STEPS_PER_MACRO,
|
MAX_STEPS_PER_MACRO,
|
||||||
MAX_KEYS_PER_STEP,
|
MAX_KEYS_PER_STEP,
|
||||||
} from "@/constants/macros";
|
} from "@/constants/macros";
|
||||||
import { KeySequence } from "@/hooks/stores";
|
import { m } from "@localizations/messages.js";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -31,7 +32,6 @@ interface MacroFormProps {
|
||||||
onSubmit: (macro: Partial<KeySequence>) => Promise<void>;
|
onSubmit: (macro: Partial<KeySequence>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
submitText?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MacroForm({
|
export function MacroForm({
|
||||||
|
|
@ -39,7 +39,6 @@ export function MacroForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
submitText = "Save Macro",
|
|
||||||
}: MacroFormProps) {
|
}: MacroFormProps) {
|
||||||
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
|
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
|
||||||
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
||||||
|
|
@ -57,13 +56,13 @@ export function MacroForm({
|
||||||
|
|
||||||
// Name validation
|
// Name validation
|
||||||
if (!macro.name?.trim()) {
|
if (!macro.name?.trim()) {
|
||||||
newErrors.name = "Name is required";
|
newErrors.name = m.macro_name_required();
|
||||||
} else if (macro.name.trim().length > 50) {
|
} else if (macro.name.trim().length > 50) {
|
||||||
newErrors.name = "Name must be less than 50 characters";
|
newErrors.name = m.macro_name_too_long();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!macro.steps?.length) {
|
if (!macro.steps?.length) {
|
||||||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
newErrors.steps = { 0: { keys: m.macro_at_least_one_step_required() } };
|
||||||
} else {
|
} else {
|
||||||
const hasKeyOrModifier = macro.steps.some(
|
const hasKeyOrModifier = macro.steps.some(
|
||||||
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||||
|
|
@ -71,7 +70,7 @@ export function MacroForm({
|
||||||
|
|
||||||
if (!hasKeyOrModifier) {
|
if (!hasKeyOrModifier) {
|
||||||
newErrors.steps = {
|
newErrors.steps = {
|
||||||
0: { keys: "At least one step must have keys or modifiers" },
|
0: { keys: m.macro_at_least_one_step_keys_or_modifiers() },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +81,7 @@ export function MacroForm({
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
showTemporaryError("Please fix the validation errors");
|
showTemporaryError(m.macro_please_fix_validation_errors());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +91,7 @@ export function MacroForm({
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
showTemporaryError(error.message);
|
showTemporaryError(error.message);
|
||||||
} else {
|
} else {
|
||||||
showTemporaryError("An error occurred while saving");
|
showTemporaryError(m.macro_save_error());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -114,7 +113,7 @@ export function MacroForm({
|
||||||
? newSteps[stepIndex].keys
|
? newSteps[stepIndex].keys
|
||||||
: [];
|
: [];
|
||||||
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
||||||
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
|
showTemporaryError(m.macro_max_steps_error({max: MAX_KEYS_PER_STEP}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newSteps[stepIndex].keys = [...keysArray, option.value];
|
newSteps[stepIndex].keys = [...keysArray, option.value];
|
||||||
|
|
@ -178,8 +177,8 @@ export function MacroForm({
|
||||||
<Fieldset>
|
<Fieldset>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
type="text"
|
type="text"
|
||||||
label="Macro Name"
|
label={m.macro_name_label()}
|
||||||
placeholder="Macro Name"
|
placeholder={m.macro_name_label()}
|
||||||
value={macro.name}
|
value={macro.name}
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
|
|
@ -197,12 +196,12 @@ export function MacroForm({
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
label="Steps"
|
label={m.macro_steps_label()}
|
||||||
description={`Keys/modifiers executed in sequence with a delay between each step.`}
|
description={m.macro_steps_description()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-slate-500 dark:text-slate-400">
|
<span className="text-slate-500 dark:text-slate-400">
|
||||||
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
|
{m.macro_step_count({steps: macro.steps?.length || 0, max: MAX_STEPS_PER_MACRO})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{errors.steps && errors.steps[0]?.keys && (
|
{errors.steps && errors.steps[0]?.keys && (
|
||||||
|
|
@ -248,12 +247,10 @@ export function MacroForm({
|
||||||
theme="light"
|
theme="light"
|
||||||
fullWidth
|
fullWidth
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`}
|
text={m.macro_add_step({ maxed_out: isMaxStepsReached ? m.macro_max_steps_reached({ max: MAX_STEPS_PER_MACRO} ) : ""})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMaxStepsReached) {
|
if (isMaxStepsReached) {
|
||||||
showTemporaryError(
|
showTemporaryError(m.macro_max_steps_error({max: MAX_STEPS_PER_MACRO}));
|
||||||
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,11 +277,11 @@ export function MacroForm({
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text={isSubmitting ? "Saving..." : submitText}
|
text={isSubmitting ? m.saving() : m.macro_save()}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
|
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { Combobox } from "@/components/Combobox";
|
import { Combobox } from "@components/Combobox";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import Card from "@components/Card";
|
||||||
import Card from "@/components/Card";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
|
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
|
||||||
import { KeyboardLayout } from "@/keyboardLayouts";
|
import { KeyboardLayout } from "@/keyboardLayouts";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
// Filter out modifier keys since they're handled in the modifiers section
|
// Filter out modifier keys since they're handled in the modifiers section
|
||||||
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
||||||
|
|
@ -25,6 +26,7 @@ const groupedModifiers: Record<string, typeof modifierOptions> = {
|
||||||
Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')),
|
Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// not going to localize these since they're short time intervals
|
||||||
const basePresetDelays = [
|
const basePresetDelays = [
|
||||||
{ value: "50", label: "50ms" },
|
{ value: "50", label: "50ms" },
|
||||||
{ value: "100", label: "100ms" },
|
{ value: "100", label: "100ms" },
|
||||||
|
|
@ -137,7 +139,7 @@ export function MacroStepCard({
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
className="text-red-500 dark:text-red-400"
|
className="text-red-500 dark:text-red-400"
|
||||||
text="Delete"
|
text={m.delete()}
|
||||||
LeadingIcon={LuTrash2}
|
LeadingIcon={LuTrash2}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
|
|
@ -147,7 +149,7 @@ export function MacroStepCard({
|
||||||
|
|
||||||
<div className="space-y-4 mt-2">
|
<div className="space-y-4 mt-2">
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<FieldLabel label="Modifiers" />
|
<FieldLabel label={m.macro_step_modifiers_label()} description={m.macro_step_modifiers_description()}/>
|
||||||
<div className="inline-flex flex-wrap gap-3">
|
<div className="inline-flex flex-wrap gap-3">
|
||||||
{Object.entries(groupedModifiers).map(([group, mods]) => (
|
{Object.entries(groupedModifiers).map(([group, mods]) => (
|
||||||
<div key={group} className="relative min-w-[120px] rounded-md border border-slate-200 dark:border-slate-700 p-2">
|
<div key={group} className="relative min-w-[120px] rounded-md border border-slate-200 dark:border-slate-700 p-2">
|
||||||
|
|
@ -179,7 +181,7 @@ export function MacroStepCard({
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-1">
|
<div className="w-full flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FieldLabel label="Keys" description={`Maximum ${MAX_KEYS_PER_STEP} keys per step.`} />
|
<FieldLabel label={m.macro_step_keys_label()} description={m.macro_step_keys_description({max: MAX_KEYS_PER_STEP})} />
|
||||||
</div>
|
</div>
|
||||||
{ensureArray(step.keys) && step.keys.length > 0 && (
|
{ensureArray(step.keys) && step.keys.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 pb-2">
|
<div className="flex flex-wrap gap-1 pb-2">
|
||||||
|
|
@ -214,19 +216,19 @@ export function MacroStepCard({
|
||||||
displayValue={() => keyQuery}
|
displayValue={() => keyQuery}
|
||||||
onInputChange={onKeyQueryChange}
|
onInputChange={onKeyQueryChange}
|
||||||
options={() => filteredKeys}
|
options={() => filteredKeys}
|
||||||
disabledMessage="Max keys reached"
|
disabledMessage={m.macro_step_max_keys_reached({max: MAX_KEYS_PER_STEP})}
|
||||||
size="SM"
|
size="SM"
|
||||||
immediate
|
immediate
|
||||||
disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP}
|
disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP}
|
||||||
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."}
|
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? m.macro_step_max_keys_reached() : m.macro_step_search_for_key()}
|
||||||
emptyMessage="No matching keys found"
|
emptyMessage={m.macro_step_no_matching_keys_found()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-1">
|
<div className="w-full flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FieldLabel label="Step Duration" description="Time to wait before executing the next step." />
|
<FieldLabel label={m.macro_step_duration_label()} description={m.macro_step_duration_description()} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
import { ComponentProps } from "react";
|
import { ComponentProps } from "react";
|
||||||
import { cva, cx } from "cva";
|
import { cva, cx } from "cva";
|
||||||
|
|
||||||
import { someIterable } from "../utils";
|
import { GridCard } from "@components/Card";
|
||||||
|
import MetricsChart from "@components/MetricsChart";
|
||||||
import { GridCard } from "./Card";
|
import { someIterable } from "@/utils";
|
||||||
import MetricsChart from "./MetricsChart";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
interface ChartPoint {
|
interface ChartPoint {
|
||||||
date: number;
|
date: number;
|
||||||
|
|
@ -159,7 +159,7 @@ export function Metric<T, K extends keyof T>({
|
||||||
>
|
>
|
||||||
{!ready ? (
|
{!ready ? (
|
||||||
<div className="flex flex-col items-center space-y-1">
|
<div className="flex flex-col items-center space-y-1">
|
||||||
<p className="text-slate-700">Waiting for data...</p>
|
<p className="text-slate-700">{m.metric_waiting_for_data()}</p>
|
||||||
</div>
|
</div>
|
||||||
) : supportedFinal ? (
|
) : supportedFinal ? (
|
||||||
<MetricsChart
|
<MetricsChart
|
||||||
|
|
@ -170,7 +170,7 @@ export function Metric<T, K extends keyof T>({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center space-y-1">
|
<div className="flex flex-col items-center space-y-1">
|
||||||
<p className="text-black">Metric not supported</p>
|
<p className="text-black">{m.metric_not_supported()}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export default function MetricsChart({
|
||||||
axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
||||||
tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
||||||
tickFormatter={date => {
|
tickFormatter={date => {
|
||||||
|
// TODO use locale from user settings for date formatting
|
||||||
return new Date(date * 1000).toLocaleString("en-US", {
|
return new Date(date * 1000).toLocaleString("en-US", {
|
||||||
hourCycle: "h23",
|
hourCycle: "h23",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import EmptyCard from "@/components/EmptyCard";
|
import EmptyCard from "@components/EmptyCard";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
export default function NotFoundPage() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -9,8 +10,8 @@ export default function NotFoundPage() {
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
IconElm={ExclamationTriangleIcon}
|
IconElm={ExclamationTriangleIcon}
|
||||||
headline="Not found"
|
headline={m.not_found()}
|
||||||
description="The page you were looking for does not exist."
|
description={m.page_not_found_description()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import StatusCard from "@components/StatusCards";
|
import StatusCard from "@components/StatusCards";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
const PeerConnectionStatusMap = {
|
const PeerConnectionStatusMap = {
|
||||||
connected: "Connected",
|
connected: m.peer_connection_connected(),
|
||||||
connecting: "Connecting",
|
connecting: m.peer_connection_connecting(),
|
||||||
disconnected: "Disconnected",
|
disconnected: m.peer_connection_disconnected(),
|
||||||
error: "Connection error",
|
error: m.peer_connection_error(),
|
||||||
closing: "Closing",
|
closing: m.peer_connection_closing(),
|
||||||
failed: "Connection failed",
|
failed: m.peer_connection_failed(),
|
||||||
closed: "Closed",
|
closed: m.peer_connection_closed(),
|
||||||
new: "Connecting",
|
new: m.peer_connection_new(),
|
||||||
} as Record<RTCPeerConnectionState | "error" | "closing", string>;
|
} as Record<RTCPeerConnectionState | "error" | "closing", string>;
|
||||||
|
|
||||||
export type PeerConnections = keyof typeof PeerConnectionStatusMap;
|
export type PeerConnections = keyof typeof PeerConnectionStatusMap;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import Card from "@components/Card";
|
||||||
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import { cva } from "@/cva.config";
|
import { cva } from "@/cva.config";
|
||||||
|
|
||||||
import Card from "./Card";
|
|
||||||
|
|
||||||
|
|
||||||
type SelectMenuProps = Pick<
|
type SelectMenuProps = Pick<
|
||||||
JSX.IntrinsicElements["select"],
|
JSX.IntrinsicElements["select"],
|
||||||
"disabled" | "onChange" | "name" | "value"
|
"disabled" | "onChange" | "name" | "value"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { AvailableSidebarViews } from "@hooks/stores";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { AvailableSidebarViews } from "@/hooks/stores";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function SidebarHeader({
|
export default function SidebarHeader({
|
||||||
title,
|
title,
|
||||||
|
|
@ -17,7 +18,7 @@ export default function SidebarHeader({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Hide"
|
text={m.hide()}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<svg
|
<svg
|
||||||
className={cx(className, "rotate-180")}
|
className={cx(className, "rotate-180")}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Link } from "react-router";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
import Container from "@/components/Container";
|
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import Container from "@components/Container";
|
||||||
|
|
||||||
interface Props { logoHref?: string; actionElement?: React.ReactNode }
|
interface Props { logoHref?: string; actionElement?: React.ReactNode }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { CheckIcon } from "@heroicons/react/16/solid";
|
import { CheckIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { cva, cx } from "@/cva.config";
|
import { cva, cx } from "@/cva.config";
|
||||||
import Card from "@/components/Card";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nSteps: number;
|
nSteps: number;
|
||||||
|
|
@ -49,7 +50,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
|
||||||
)}
|
)}
|
||||||
key={`${i}-${currStepIdx}`}
|
key={`${i}-${currStepIdx}`}
|
||||||
>
|
>
|
||||||
Step {i + 1}
|
{m.step_counter_step({ step: i + 1 })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { useXTerm } from "react-xtermjs";
|
import { useXTerm } from "react-xtermjs";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
import { WebglAddon } from "@xterm/addon-webgl";
|
import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
|
||||||
|
|
||||||
import { Button } from "./Button";
|
import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ function Terminal({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Hide"
|
text={m.hide()}
|
||||||
LeadingIcon={ChevronDownIcon}
|
LeadingIcon={ChevronDownIcon}
|
||||||
onClick={() => setTerminalType("none")}
|
onClick={() => setTerminalType("none")}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import { FieldError } from "@/components/InputField";
|
import { FieldError } from "@components/InputField";
|
||||||
import Card from "@/components/Card";
|
import Card from "@components/Card";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type TextAreaProps = JSX.IntrinsicElements["textarea"] & {
|
type TextAreaProps = JSX.IntrinsicElements["textarea"] & {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
import KeyboardAndMouseConnectedIcon from "@assets/keyboard-and-mouse-connected.png";
|
||||||
|
|
||||||
|
import { USBStates } from "@hooks/stores";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import StatusCard from "@components/StatusCards";
|
import StatusCard from "@components/StatusCards";
|
||||||
import { USBStates } from "@/hooks/stores";
|
|
||||||
|
|
||||||
type StatusProps = Record<
|
type StatusProps = Record<
|
||||||
USBStates,
|
USBStates,
|
||||||
|
|
@ -16,11 +18,11 @@ type StatusProps = Record<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const USBStateMap: Record<USBStates, string> = {
|
const USBStateMap: Record<USBStates, string> = {
|
||||||
configured: "Connected",
|
configured: m.usb_state_connected(),
|
||||||
attached: "Connecting",
|
attached: m.usb_state_connecting(),
|
||||||
addressed: "Connecting",
|
addressed: m.usb_state_connecting(),
|
||||||
"not attached": "Disconnected",
|
"not attached": m.usb_state_disconnected(),
|
||||||
suspended: "Low power mode",
|
suspended: m.usb_state_low_power_mode(),
|
||||||
};
|
};
|
||||||
const StatusCardProps: StatusProps = {
|
const StatusCardProps: StatusProps = {
|
||||||
configured: {
|
configured: {
|
||||||
|
|
@ -80,8 +82,8 @@ export default function USBStateStatus({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusCard
|
<StatusCard
|
||||||
title="USB"
|
title={m.usb()}
|
||||||
status="Disconnected"
|
status={m.usb_state_disconnected()}
|
||||||
icon={Icon}
|
icon={Icon}
|
||||||
iconClassName={iconClassName}
|
iconClassName={iconClassName}
|
||||||
statusIndicatorClassName={statusIndicatorClassName}
|
statusIndicatorClassName={statusIndicatorClassName}
|
||||||
|
|
@ -90,6 +92,6 @@ export default function USBStateStatus({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusCard title="USB" status={USBStateMap[state]} {...StatusCardProps[state]} />
|
<StatusCard title={m.usb()} status={USBStateMap[state]} {...StatusCardProps[state]} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
import { Button } from "./Button";
|
import { GridCard } from "@components/Card";
|
||||||
import { GridCard } from "./Card";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import LoadingSpinner from "./LoadingSpinner";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function UpdateInProgressStatusCard() {
|
export default function UpdateInProgressStatusCard() {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
@ -17,12 +17,12 @@ export default function UpdateInProgressStatusCard() {
|
||||||
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
|
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-ellipsis text-sm font-semibold leading-none transition">
|
<div className="text-ellipsis text-sm font-semibold leading-none transition">
|
||||||
Update in Progress
|
{m.update_in_progress()}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm leading-none">
|
<div className="text-sm leading-none">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<span className={cx("transition")}>
|
<span className={cx("transition")}>
|
||||||
Please don{"'"}t turn off your device...
|
{m.updating_leave_device_on()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,7 +32,7 @@ export default function UpdateInProgressStatusCard() {
|
||||||
size="SM"
|
size="SM"
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="View Details"
|
text={m.view_details()}
|
||||||
onClick={() => navigateTo("/settings/general/update")}
|
onClick={() => navigateTo("/settings/general/update")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
import { SettingsItem } from "@components/SettingsItem";
|
||||||
|
import Checkbox from "@components/Checkbox";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
|
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||||
|
import Fieldset from "@components/Fieldset";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
|
|
||||||
import Checkbox from "./Checkbox";
|
|
||||||
import { Button } from "./Button";
|
|
||||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
|
||||||
import { SettingsSectionHeader } from "./SettingsSectionHeader";
|
|
||||||
import Fieldset from "./Fieldset";
|
|
||||||
export interface USBConfig {
|
export interface USBConfig {
|
||||||
vendor_id: string;
|
vendor_id: string;
|
||||||
product_id: string;
|
product_id: string;
|
||||||
|
|
@ -34,7 +34,7 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
||||||
|
|
||||||
const usbPresets = [
|
const usbPresets = [
|
||||||
{
|
{
|
||||||
label: "Keyboard, Mouse and Mass Storage",
|
label: m.usb_device_keyboard_mouse_and_mass_storage(),
|
||||||
value: "default",
|
value: "default",
|
||||||
config: {
|
config: {
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
|
|
@ -44,7 +44,7 @@ const usbPresets = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Keyboard Only",
|
label: m.usb_device_keyboard_only(),
|
||||||
value: "keyboard_only",
|
value: "keyboard_only",
|
||||||
config: {
|
config: {
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
|
|
@ -54,7 +54,7 @@ const usbPresets = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Custom",
|
label: m.usb_device_custom(),
|
||||||
value: "custom",
|
value: "custom",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to load USB devices:", resp.error);
|
console.error("Failed to load USB devices:", resp.error);
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
m.usb_device_failed_load({ error: String(resp.error.data || "Unknown error") }),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||||
|
|
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
|
||||||
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
|
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,
|
m.usb_device_failed_set({ error: String(resp.error.data || "Unknown error") }),
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -111,7 +111,7 @@ export function UsbDeviceSetting() {
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
syncUsbDeviceConfig();
|
syncUsbDeviceConfig();
|
||||||
notifications.success(`USB Devices updated`);
|
notifications.success(m.usb_device_updated());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[send, syncUsbDeviceConfig],
|
[send, syncUsbDeviceConfig],
|
||||||
|
|
@ -154,14 +154,14 @@ export function UsbDeviceSetting() {
|
||||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="USB Device"
|
title={m.usb_device_title()}
|
||||||
description="USB devices to emulate on the target computer"
|
description={m.usb_device_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title="Classes"
|
title={m.usb_device_classes_title()}
|
||||||
description="USB device classes in the composite device"
|
description={m.usb_device_classes_description()}
|
||||||
>
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
@ -178,7 +178,7 @@ export function UsbDeviceSetting() {
|
||||||
<div className="ml-2 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 ">
|
<div className="ml-2 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 ">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem title="Enable Keyboard" description="Enable Keyboard">
|
<SettingsItem title={m.usb_device_enable_keyboard_title()} description={m.usb_device_enable_keyboard_description()}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={usbDeviceConfig.keyboard}
|
checked={usbDeviceConfig.keyboard}
|
||||||
onChange={onUsbConfigItemChange("keyboard")}
|
onChange={onUsbConfigItemChange("keyboard")}
|
||||||
|
|
@ -187,8 +187,8 @@ export function UsbDeviceSetting() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Enable Absolute Mouse (Pointer)"
|
title={m.usb_device_enable_absolute_mouse_title()}
|
||||||
description="Enable Absolute Mouse (Pointer)"
|
description={m.usb_device_enable_absolute_mouse_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={usbDeviceConfig.absolute_mouse}
|
checked={usbDeviceConfig.absolute_mouse}
|
||||||
|
|
@ -198,8 +198,8 @@ export function UsbDeviceSetting() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Enable Relative Mouse"
|
title={m.usb_device_enable_relative_mouse_title()}
|
||||||
description="Enable Relative Mouse"
|
description={m.usb_device_enable_relative_mouse_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={usbDeviceConfig.relative_mouse}
|
checked={usbDeviceConfig.relative_mouse}
|
||||||
|
|
@ -209,8 +209,8 @@ export function UsbDeviceSetting() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Enable USB Mass Storage"
|
title={m.usb_device_enable_mass_storage_title()}
|
||||||
description="Sometimes it might need to be disabled to prevent issues with certain devices"
|
description={m.usb_device_enable_mass_storage_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={usbDeviceConfig.mass_storage}
|
checked={usbDeviceConfig.mass_storage}
|
||||||
|
|
@ -224,13 +224,13 @@ export function UsbDeviceSetting() {
|
||||||
size="SM"
|
size="SM"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Update USB Classes"
|
text={m.usb_device_update_classes()}
|
||||||
onClick={() => handleUsbConfigChange(usbDeviceConfig)}
|
onClick={() => handleUsbConfigChange(usbDeviceConfig)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Restore to Default"
|
text={m.usb_device_restore_default()}
|
||||||
onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)}
|
onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { useMemo , useCallback , useEffect, useState } from "react";
|
import { useMemo , useCallback , useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { UsbConfigState } from "@hooks/stores";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import Fieldset from "@components/Fieldset";
|
||||||
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
import { SettingsItem } from "@components/SettingsItem";
|
||||||
|
import notifications from "@/notifications";
|
||||||
import { UsbConfigState } from "../hooks/stores";
|
import { m } from "@localizations/messages.js";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
|
|
||||||
import { InputFieldWithLabel } from "./InputField";
|
|
||||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
|
||||||
import Fieldset from "./Fieldset";
|
|
||||||
|
|
||||||
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
|
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
|
||||||
|
|
||||||
|
|
@ -31,21 +30,22 @@ export interface USBConfig {
|
||||||
product: string;
|
product: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const usbConfigs = [
|
const usbConfigs = [
|
||||||
{
|
{
|
||||||
label: "JetKVM Default",
|
label: m.usb_config_default(),
|
||||||
value: "USB Emulation Device",
|
value: "USB Emulation Device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Logitech Universal Adapter",
|
label: m.usb_config_logitech(),
|
||||||
value: "Logitech USB Input Device",
|
value: "Logitech USB Input Device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Microsoft Wireless MultiMedia Keyboard",
|
label: m.usb_config_microsoft(),
|
||||||
value: "Wireless MultiMedia Keyboard",
|
value: "Wireless MultiMedia Keyboard",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Dell Multimedia Pro Keyboard",
|
label: m.usb_config_dell(),
|
||||||
value: "Multimedia Pro Keyboard",
|
value: "Multimedia Pro Keyboard",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -97,7 +97,7 @@ export function UsbInfoSetting() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to load USB Config:", resp.error);
|
console.error("Failed to load USB Config:", resp.error);
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`,
|
m.usb_config_failed_load({ error: String(resp.error.data || "Unknown error") }),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const usbConfigState = resp.result as UsbConfigState;
|
const usbConfigState = resp.result as UsbConfigState;
|
||||||
|
|
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
|
||||||
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
|
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set usb config: ${resp.error.data || "Unknown error"}`,
|
m.usb_config_failed_set({ error: String(resp.error.data || "Unknown error") }),
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -126,7 +126,7 @@ export function UsbInfoSetting() {
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
notifications.success(
|
notifications.success(
|
||||||
`USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`,
|
m.usb_config_set_success({ manufacturer: usbConfig.manufacturer, product: usbConfig.product }),
|
||||||
);
|
);
|
||||||
|
|
||||||
syncUsbConfigProduct();
|
syncUsbConfigProduct();
|
||||||
|
|
@ -152,8 +152,8 @@ export function UsbInfoSetting() {
|
||||||
<Fieldset disabled={loading} className="space-y-4">
|
<Fieldset disabled={loading} className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title="Identifiers"
|
title={m.usb_config_identifiers_title()}
|
||||||
description="USB device identifiers exposed to the target computer"
|
description={m.usb_config_identifiers_description()}
|
||||||
>
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
@ -169,7 +169,7 @@ export function UsbInfoSetting() {
|
||||||
handleUsbConfigChange(usbConfig);
|
handleUsbConfigChange(usbConfig);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={[...usbConfigs, { value: "custom", label: "Custom" }]}
|
options={[...usbConfigs, { value: "custom", label: m.usb_config_custom() }]}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{usbConfigProduct === "custom" && (
|
{usbConfigProduct === "custom" && (
|
||||||
|
|
@ -246,38 +246,38 @@ function USBConfigDialog({
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
label="Vendor ID"
|
label={m.usb_config_vendor_id_label()}
|
||||||
placeholder="Enter Vendor ID"
|
placeholder={m.usb_config_vendor_id_placeholder()}
|
||||||
pattern="^0[xX][\da-fA-F]{4}$"
|
pattern="^0[xX][\da-fA-F]{4}$"
|
||||||
defaultValue={usbConfigState?.vendor_id}
|
defaultValue={usbConfigState?.vendor_id}
|
||||||
onChange={e => handleUsbVendorIdChange(e.target.value)}
|
onChange={e => handleUsbVendorIdChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
label="Product ID"
|
label={m.usb_config_product_id_label()}
|
||||||
placeholder="Enter Product ID"
|
placeholder={m.usb_config_product_id_placeholder()}
|
||||||
pattern="^0[xX][\da-fA-F]{4}$"
|
pattern="^0[xX][\da-fA-F]{4}$"
|
||||||
defaultValue={usbConfigState?.product_id}
|
defaultValue={usbConfigState?.product_id}
|
||||||
onChange={e => handleUsbProductIdChange(e.target.value)}
|
onChange={e => handleUsbProductIdChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
label="Serial Number"
|
label={m.usb_config_serial_number_label()}
|
||||||
placeholder="Enter Serial Number"
|
placeholder={m.usb_config_serial_number_placeholder()}
|
||||||
defaultValue={usbConfigState?.serial_number}
|
defaultValue={usbConfigState?.serial_number}
|
||||||
onChange={e => handleUsbSerialChange(e.target.value)}
|
onChange={e => handleUsbSerialChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
label="Manufacturer"
|
label={m.usb_config_manufacturer_label()}
|
||||||
placeholder="Enter Manufacturer"
|
placeholder={m.usb_config_manufacturer_placeholder()}
|
||||||
defaultValue={usbConfigState?.manufacturer}
|
defaultValue={usbConfigState?.manufacturer}
|
||||||
onChange={e => handleUsbManufacturer(e.target.value)}
|
onChange={e => handleUsbManufacturer(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
required
|
required
|
||||||
label="Product Name"
|
label={m.usb_config_product_name_label()}
|
||||||
placeholder="Enter Product Name"
|
placeholder={m.usb_config_product_name_placeholder()}
|
||||||
defaultValue={usbConfigState?.product}
|
defaultValue={usbConfigState?.product}
|
||||||
onChange={e => handleUsbProduct(e.target.value)}
|
onChange={e => handleUsbProduct(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -287,13 +287,13 @@ function USBConfigDialog({
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Update USB Identifiers"
|
text={m.usb_config_update_identifiers()}
|
||||||
onClick={() => onSetUsbConfig(usbConfigState)}
|
onClick={() => onSetUsbConfig(usbConfigState)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Restore to Default"
|
text={m.usb_config_restore_default()}
|
||||||
onClick={onRestoreToDefault}
|
onClick={onRestoreToDefault}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { LuPlay } from "react-icons/lu";
|
import { LuPlay } from "react-icons/lu";
|
||||||
import { BsMouseFill } from "react-icons/bs";
|
import { BsMouseFill } from "react-icons/bs";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
|
@ -46,7 +47,7 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
<LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" />
|
<LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center text-sm text-slate-700 dark:text-slate-300">
|
<p className="text-center text-sm text-slate-700 dark:text-slate-300">
|
||||||
Loading video stream...
|
{m.video_overlay_loading_stream()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</OverlayContent>
|
</OverlayContent>
|
||||||
|
|
@ -118,26 +119,26 @@ export function ConnectionFailedOverlay({
|
||||||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2 text-black dark:text-white">
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
<h2 className="text-xl font-bold">Connection Issue Detected</h2>
|
<h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
|
||||||
<ul className="list-disc space-y-2 pl-4 text-left">
|
<ul className="list-disc space-y-2 pl-4 text-left">
|
||||||
<li>Verify that the device is powered on and properly connected</li>
|
<li>{m.video_overlay_conn_verify_power()}</li>
|
||||||
<li>Check all cable connections for any loose or damaged wires</li>
|
<li>{m.video_overlay_conn_check_cables()}</li>
|
||||||
<li>Ensure your network connection is stable and active</li>
|
<li>{m.video_overlay_conn_ensure_network()}</li>
|
||||||
<li>Try restarting both the device and your computer</li>
|
<li>{m.video_overlay_conn_restart()}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Troubleshooting Guide"
|
text={m.video_overlay_troubleshooting_guide()}
|
||||||
TrailingIcon={ArrowRightIcon}
|
TrailingIcon={ArrowRightIcon}
|
||||||
size="SM"
|
size="SM"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setupPeerConnection()}
|
onClick={() => setupPeerConnection()}
|
||||||
LeadingIcon={ArrowPathIcon}
|
LeadingIcon={ArrowPathIcon}
|
||||||
text="Try again"
|
text={m.video_overlay_try_again()}
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
/>
|
/>
|
||||||
|
|
@ -178,12 +179,12 @@ export function PeerConnectionDisconnectedOverlay({
|
||||||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2 text-black dark:text-white">
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
<h2 className="text-xl font-bold">Connection Issue Detected</h2>
|
<h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
|
||||||
<ul className="list-disc space-y-2 pl-4 text-left">
|
<ul className="list-disc space-y-2 pl-4 text-left">
|
||||||
<li>Verify that the device is powered on and properly connected</li>
|
<li>{m.video_overlay_conn_verify_power()}</li>
|
||||||
<li>Check all cable connections for any loose or damaged wires</li>
|
<li>{m.video_overlay_conn_check_cables()}</li>
|
||||||
<li>Ensure your network connection is stable and active</li>
|
<li>{m.video_overlay_conn_ensure_network()}</li>
|
||||||
<li>Try restarting both the device and your computer</li>
|
<li>{m.video_overlay_conn_restart()}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
|
|
@ -191,7 +192,7 @@ export function PeerConnectionDisconnectedOverlay({
|
||||||
<div className="flex items-center gap-x-2 p-4">
|
<div className="flex items-center gap-x-2 p-4">
|
||||||
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
|
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
|
||||||
<p className="text-sm text-slate-700 dark:text-slate-300">
|
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||||
Retrying connection...
|
{m.video_overlay_retrying_connection()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -235,23 +236,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2 text-black dark:text-white">
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
<h2 className="text-xl font-bold">No HDMI signal detected.</h2>
|
<h2 className="text-xl font-bold">{m.video_overlay_no_hdmi_signal()}</h2>
|
||||||
<ul className="list-disc space-y-2 pl-4 text-left">
|
<ul className="list-disc space-y-2 pl-4 text-left">
|
||||||
<li>Ensure the HDMI cable securely connected at both ends</li>
|
<li>{m.video_overlay_no_hdmi_ensure_cable()}</li>
|
||||||
<li>
|
<li>{m.video_overlay_no_hdmi_ensure_power()}</li>
|
||||||
Ensure source device is powered on and outputting a signal
|
<li>{m.video_overlay_no_hdmi_adapter_compat()}</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
If using an adapter, ensure it's compatible and functioning
|
|
||||||
correctly
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Learn more"
|
text={m.video_overlay_learn_more()}
|
||||||
TrailingIcon={ArrowRightIcon}
|
TrailingIcon={ArrowRightIcon}
|
||||||
size="SM"
|
size="SM"
|
||||||
/>
|
/>
|
||||||
|
|
@ -282,18 +278,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2 text-black dark:text-white">
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
<h2 className="text-xl font-bold">HDMI signal error detected.</h2>
|
<h2 className="text-xl font-bold">{m.video_overlay_hdmi_error_title()}</h2>
|
||||||
<ul className="list-disc space-y-2 pl-4 text-left">
|
<ul className="list-disc space-y-2 pl-4 text-left">
|
||||||
<li>A loose or faulty HDMI connection</li>
|
<li>{m.video_overlay_hdmi_loose_faulty()}</li>
|
||||||
<li>Incompatible resolution or refresh rate settings</li>
|
<li>{m.video_overlay_hdmi_incompatible_resolution()}</li>
|
||||||
<li>Issues with the source device's HDMI output</li>
|
<li>{m.video_overlay_hdmi_source_issue()}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Learn more"
|
text={m.video_overlay_learn_more()}
|
||||||
TrailingIcon={ArrowRightIcon}
|
TrailingIcon={ArrowRightIcon}
|
||||||
size="SM"
|
size="SM"
|
||||||
/>
|
/>
|
||||||
|
|
@ -334,7 +330,7 @@ export function NoAutoplayPermissionsOverlay({
|
||||||
<OverlayContent>
|
<OverlayContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-2xl font-extrabold text-black dark:text-white">
|
<h2 className="text-2xl font-extrabold text-black dark:text-white">
|
||||||
Autoplay permissions required
|
{m.video_overlay_autoplay_permissions_required()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
|
|
@ -343,13 +339,13 @@ export function NoAutoplayPermissionsOverlay({
|
||||||
size="MD"
|
size="MD"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
LeadingIcon={LuPlay}
|
LeadingIcon={LuPlay}
|
||||||
text="Manually start stream"
|
text={m.video_overlay_manually_start_stream()}
|
||||||
onClick={onPlayClick}
|
onClick={onPlayClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-slate-600 dark:text-slate-400">
|
<div className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
Please adjust browser settings to enable autoplay
|
{m.video_overlay_enable_autoplay_settings()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -381,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
<span className="text-sm text-black dark:text-white">
|
<span className="text-sm text-black dark:text-white">
|
||||||
Click on the video to enable mouse control
|
{m.video_overlay_pointerlock_click_to_enable()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import Keyboard from "react-simple-keyboard";
|
import Keyboard from "react-simple-keyboard";
|
||||||
import { LuKeyboard } from "react-icons/lu";
|
import { LuKeyboard } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import { useHidStore, useUiStore } from "@hooks/stores";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboard from "@hooks/useKeyboard";
|
||||||
|
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||||
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
import Card from "@components/Card";
|
||||||
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
|
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
|
|
@ -244,20 +242,20 @@ function KeyboardWrapper() {
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Detach"
|
text={m.detach()}
|
||||||
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
|
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Attach"
|
text={m.attach()}
|
||||||
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
|
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
|
||||||
Virtual Keyboard
|
m.virtual_keyboard_header()
|
||||||
</h2>
|
</h2>
|
||||||
<div className="absolute right-2 flex items-center gap-x-2">
|
<div className="absolute right-2 flex items-center gap-x-2">
|
||||||
<div className="hidden md:flex gap-x-2 items-center">
|
<div className="hidden md:flex gap-x-2 items-center">
|
||||||
|
|
@ -274,7 +272,7 @@ function KeyboardWrapper() {
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Hide"
|
text={m.hide()}
|
||||||
LeadingIcon={ChevronDownIcon}
|
LeadingIcon={ChevronDownIcon}
|
||||||
onClick={() => setVirtualKeyboardEnabled(false)}
|
onClick={() => setVirtualKeyboardEnabled(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
|
||||||
import Actionbar from "@components/ActionBar";
|
|
||||||
import MacroBar from "@/components/MacroBar";
|
|
||||||
import InfoBar from "@components/InfoBar";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { keys } from "@/keyboardMappings";
|
|
||||||
|
import useKeyboard from "@hooks/useKeyboard";
|
||||||
|
import useMouse from "@hooks/useMouse";
|
||||||
import {
|
import {
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} from "@hooks/stores";
|
||||||
import useMouse from "@/hooks/useMouse";
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
|
import Actionbar from "@components/ActionBar";
|
||||||
|
import MacroBar from "@components/MacroBar";
|
||||||
|
import InfoBar from "@components/InfoBar";
|
||||||
import {
|
import {
|
||||||
HDMIErrorOverlay,
|
HDMIErrorOverlay,
|
||||||
LoadingVideoOverlay,
|
LoadingVideoOverlay,
|
||||||
NoAutoplayPermissionsOverlay,
|
NoAutoplayPermissionsOverlay,
|
||||||
PointerLockBar,
|
PointerLockBar,
|
||||||
} from "./VideoOverlay";
|
} from "@components/VideoOverlay";
|
||||||
|
import { keys } from "@/keyboardMappings";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export default function WebRTCVideo() {
|
export default function WebRTCVideo() {
|
||||||
// Video and stream related refs and states
|
// Video and stream related refs and states
|
||||||
|
|
@ -168,10 +168,10 @@ export default function WebRTCVideo() {
|
||||||
|
|
||||||
const handlePointerLockChange = () => {
|
const handlePointerLockChange = () => {
|
||||||
if (document.pointerLockElement) {
|
if (document.pointerLockElement) {
|
||||||
notifications.success("Pointer lock Enabled, press escape to unlock");
|
notifications.success(m.video_pointer_lock_enabled());
|
||||||
setIsPointerLockActive(true);
|
setIsPointerLockActive(true);
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Pointer lock Disabled");
|
notifications.success(m.video_pointer_lock_disabled());
|
||||||
setIsPointerLockActive(false);
|
setIsPointerLockActive(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
|
|
||||||
|
|
||||||
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
||||||
|
|
||||||
|
|
@ -33,9 +33,7 @@ export function ATXPowerControl() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getATXState", {}, (resp: JsonRpcResponse) => {
|
send("getATXState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAtxState(resp.result as ATXState);
|
setAtxState(resp.result as ATXState);
|
||||||
|
|
@ -56,9 +54,7 @@ export function ATXPowerControl() {
|
||||||
console.log("Sending long press ATX power action");
|
console.log("Sending long press ATX power action");
|
||||||
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
|
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_long_power_button(), error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
setIsPowerPressed(false);
|
setIsPowerPressed(false);
|
||||||
});
|
});
|
||||||
|
|
@ -77,9 +73,7 @@ export function ATXPowerControl() {
|
||||||
console.log("Sending short press ATX power action");
|
console.log("Sending short press ATX power action");
|
||||||
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
|
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_short_power_button(), error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -98,8 +92,8 @@ export function ATXPowerControl() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="ATX Power Control"
|
title={m.extensions_atx_power_control()}
|
||||||
description="Control your ATX power settings"
|
description={m.extensions_atx_power_control_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{atxState === null ? (
|
{atxState === null ? (
|
||||||
|
|
@ -115,7 +109,7 @@ export function ATXPowerControl() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
LeadingIcon={LuPower}
|
LeadingIcon={LuPower}
|
||||||
text="Power"
|
text={m.atx_power_control_power_button()}
|
||||||
onMouseDown={() => handlePowerPress(true)}
|
onMouseDown={() => handlePowerPress(true)}
|
||||||
onMouseUp={() => handlePowerPress(false)}
|
onMouseUp={() => handlePowerPress(false)}
|
||||||
onMouseLeave={() => handlePowerPress(false)}
|
onMouseLeave={() => handlePowerPress(false)}
|
||||||
|
|
@ -125,13 +119,11 @@ export function ATXPowerControl() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
LeadingIcon={LuRotateCcw}
|
LeadingIcon={LuRotateCcw}
|
||||||
text="Reset"
|
text={m.atx_power_control_reset_button()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
|
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_reset_button(), error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -150,7 +142,7 @@ export function ATXPowerControl() {
|
||||||
atxState?.power ? "text-green-600" : "text-slate-300"
|
atxState?.power ? "text-green-600" : "text-slate-300"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
Power LED
|
{m.atx_power_control_power_led()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -161,7 +153,7 @@ export function ATXPowerControl() {
|
||||||
atxState?.hdd ? "text-blue-400" : "text-slate-300"
|
atxState?.hdd ? "text-blue-400" : "text-slate-300"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
HDD LED
|
{m.atx_power_control_hdd_led()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { LuPower } from "react-icons/lu";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { LuPower } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
import FieldLabel from "@components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
interface DCPowerState {
|
interface DCPowerState {
|
||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
|
|
@ -25,9 +26,7 @@ export function DCPowerControl() {
|
||||||
const getDCPowerState = useCallback(() => {
|
const getDCPowerState = useCallback(() => {
|
||||||
send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
|
send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.dc_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPowerState(resp.result as DCPowerState);
|
setPowerState(resp.result as DCPowerState);
|
||||||
|
|
@ -37,9 +36,7 @@ export function DCPowerControl() {
|
||||||
const handlePowerToggle = (enabled: boolean) => {
|
const handlePowerToggle = (enabled: boolean) => {
|
||||||
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
|
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.dc_power_control_set_power_state_error({ enabled: enabled, error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getDCPowerState(); // Refresh state after change
|
getDCPowerState(); // Refresh state after change
|
||||||
|
|
@ -49,17 +46,13 @@ export function DCPowerControl() {
|
||||||
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||||
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
|
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.dc_power_control_set_restore_state_error({ state: state, error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getDCPowerState(); // Refresh state after change
|
getDCPowerState(); // Refresh state after change
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDCPowerState();
|
getDCPowerState();
|
||||||
// Set up polling interval to update status
|
// Set up polling interval to update status
|
||||||
|
|
@ -70,8 +63,8 @@ export function DCPowerControl() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="DC Power Control"
|
title={m.extensions_dc_power_control()}
|
||||||
description="Control your DC power settings"
|
description={m.extensions_dc_power_control_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{powerState === null ? (
|
{powerState === null ? (
|
||||||
|
|
@ -87,7 +80,7 @@ export function DCPowerControl() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
LeadingIcon={LuPower}
|
LeadingIcon={LuPower}
|
||||||
text="Power On"
|
text={m.dc_power_control_power_on_button()}
|
||||||
onClick={() => handlePowerToggle(true)}
|
onClick={() => handlePowerToggle(true)}
|
||||||
disabled={powerState.isOn}
|
disabled={powerState.isOn}
|
||||||
/>
|
/>
|
||||||
|
|
@ -95,7 +88,7 @@ export function DCPowerControl() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
LeadingIcon={LuPower}
|
LeadingIcon={LuPower}
|
||||||
text="Power Off"
|
text={m.dc_power_control_power_off_button()}
|
||||||
disabled={!powerState.isOn}
|
disabled={!powerState.isOn}
|
||||||
onClick={() => handlePowerToggle(false)}
|
onClick={() => handlePowerToggle(false)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -104,13 +97,13 @@ export function DCPowerControl() {
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
label="Restore Power Loss"
|
label={m.dc_power_control_restore_power_state()}
|
||||||
value={powerState.restoreState}
|
value={powerState.restoreState}
|
||||||
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||||
options={[
|
options={[
|
||||||
{ value: '0', label: "Power OFF" },
|
{ value: '0', label: m.dc_power_control_power_off_state()},
|
||||||
{ value: '1', label: "Power ON" },
|
{ value: '1', label: m.dc_power_control_power_on_state()},
|
||||||
{ value: '2', label: "Last State" },
|
{ value: '2', label: m.dc_power_control_restore_last_state()},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,21 +113,21 @@ export function DCPowerControl() {
|
||||||
{/* Status Display */}
|
{/* Status Display */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FieldLabel label="Voltage" />
|
<FieldLabel label={m.dc_power_control_voltage()} />
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
{powerState.voltage.toFixed(1)}V
|
{powerState.voltage.toFixed(1)} {m.dc_power_control_voltage_unit()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FieldLabel label="Current" />
|
<FieldLabel label={m.dc_power_control_current()} />
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
{powerState.current.toFixed(1)}A
|
{powerState.current.toFixed(1)} {m.dc_power_control_current_unit()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FieldLabel label="Power" />
|
<FieldLabel label={m.dc_power_control_power()}/>
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
{powerState.power.toFixed(1)}W
|
{powerState.power.toFixed(1)} {m.dc_power_control_power_unit()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { LuTerminal } from "react-icons/lu";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { LuTerminal } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
|
import { useUiStore } from "@hooks/stores";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
import { useUiStore } from "@/hooks/stores";
|
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
interface SerialSettings {
|
interface SerialSettings {
|
||||||
baudRate: string;
|
baudRate: string;
|
||||||
|
|
@ -28,9 +29,7 @@ export function SerialConsole() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
|
send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.serial_console_get_settings_error({ error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSettings(resp.result as SerialSettings);
|
setSettings(resp.result as SerialSettings);
|
||||||
|
|
@ -41,9 +40,7 @@ export function SerialConsole() {
|
||||||
const newSettings = { ...settings, [setting]: value };
|
const newSettings = { ...settings, [setting]: value };
|
||||||
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
|
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(m.serial_console_set_settings_error({ settings: setting, error: resp.error.data || m.unknown_error() }));
|
||||||
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
|
|
@ -54,8 +51,8 @@ export function SerialConsole() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Serial Console"
|
title={m.extension_serial_console()}
|
||||||
description="Configure your serial console settings"
|
description={m.serial_console_configure_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="animate-fadeIn opacity-0">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
|
|
@ -66,10 +63,10 @@ export function SerialConsole() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
LeadingIcon={LuTerminal}
|
LeadingIcon={LuTerminal}
|
||||||
text="Open Console"
|
text={m.serial_console_open_console()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTerminalType("serial");
|
|
||||||
console.log("Opening serial console with settings: ", settings);
|
console.log("Opening serial console with settings: ", settings);
|
||||||
|
setTerminalType("serial");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,7 +74,7 @@ export function SerialConsole() {
|
||||||
{/* Settings */}
|
{/* Settings */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
label="Baud Rate"
|
label={m.serial_console_baud_rate()}
|
||||||
options={[
|
options={[
|
||||||
{ label: "1200", value: "1200" },
|
{ label: "1200", value: "1200" },
|
||||||
{ label: "2400", value: "2400" },
|
{ label: "2400", value: "2400" },
|
||||||
|
|
@ -93,7 +90,7 @@ export function SerialConsole() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
label="Data Bits"
|
label={m.serial_console_data_bits()}
|
||||||
options={[
|
options={[
|
||||||
{ label: "8", value: "8" },
|
{ label: "8", value: "8" },
|
||||||
{ label: "7", value: "7" },
|
{ label: "7", value: "7" },
|
||||||
|
|
@ -103,7 +100,7 @@ export function SerialConsole() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
label="Stop Bits"
|
label={m.serial_console_stop_bits()}
|
||||||
options={[
|
options={[
|
||||||
{ label: "1", value: "1" },
|
{ label: "1", value: "1" },
|
||||||
{ label: "1.5", value: "1.5" },
|
{ label: "1.5", value: "1.5" },
|
||||||
|
|
@ -114,11 +111,13 @@ export function SerialConsole() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
label="Parity"
|
label={m.serial_console_parity()}
|
||||||
options={[
|
options={[
|
||||||
{ label: "None", value: "none" },
|
{ label: m.serial_console_parity_none(), value: "none" },
|
||||||
{ label: "Even", value: "even" },
|
{ label: m.serial_console_parity_even(), value: "even" },
|
||||||
{ label: "Odd", value: "odd" },
|
{ label: m.serial_console_parity_odd(), value: "odd" },
|
||||||
|
{ label: m.serial_console_parity_mark(), value: "mark" },
|
||||||
|
{ label: m.serial_console_parity_space(), value: "space" },
|
||||||
]}
|
]}
|
||||||
value={settings.parity}
|
value={settings.parity}
|
||||||
onChange={e => handleSettingChange("parity", e.target.value)}
|
onChange={e => handleSettingChange("parity", e.target.value)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
||||||
|
|
@ -20,20 +21,20 @@ interface Extension {
|
||||||
const AVAILABLE_EXTENSIONS: Extension[] = [
|
const AVAILABLE_EXTENSIONS: Extension[] = [
|
||||||
{
|
{
|
||||||
id: "atx-power",
|
id: "atx-power",
|
||||||
name: "ATX Power Control",
|
name: m.extensions_atx_power_control(),
|
||||||
description: "Control your ATX Power extension",
|
description: m.extensions_atx_power_control_description(),
|
||||||
icon: LuPower,
|
icon: LuPower,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "dc-power",
|
id: "dc-power",
|
||||||
name: "DC Power Control",
|
name: m.extensions_dc_power_control(),
|
||||||
description: "Control your DC Power extension",
|
description: m.extensions_dc_power_control(),
|
||||||
icon: LuPlugZap,
|
icon: LuPlugZap,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "serial-console",
|
id: "serial-console",
|
||||||
name: "Serial Console",
|
name: m.extension_serial_console(),
|
||||||
description: "Access your serial console extension",
|
description: m.extension_serial_console_description(),
|
||||||
icon: LuTerminal,
|
icon: LuTerminal,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -60,7 +61,7 @@ export default function ExtensionPopover() {
|
||||||
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
|
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set active extension: ${resp.error.data || "Unknown error"}`,
|
m.extension_popover_set_error_notification({ error: resp.error.data || m.unknown_error() }),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +102,7 @@ export default function ExtensionPopover() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Unload Extension"
|
text={m.extension_popover_unload_extension()}
|
||||||
onClick={() => handleSetActiveExtension(null)}
|
onClick={() => handleSetActiveExtension(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,8 +111,8 @@ export default function ExtensionPopover() {
|
||||||
// Extensions List View
|
// Extensions List View
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Extensions"
|
title={m.extensions_popover_extensions()}
|
||||||
description="Load and manage your extensions"
|
description={m.extension_popover_load_and_manage_extensions()}
|
||||||
/>
|
/>
|
||||||
<Card className="animate-fadeIn opacity-0" >
|
<Card className="animate-fadeIn opacity-0" >
|
||||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||||
|
|
@ -131,7 +132,7 @@ export default function ExtensionPopover() {
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Load"
|
text={m.load()}
|
||||||
onClick={() => handleSetActiveExtension(extension)}
|
onClick={() => handleSetActiveExtension(extension)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { forwardRef, useEffect, useCallback } from "react";
|
import { forwardRef, useEffect, useCallback } from "react";
|
||||||
import {
|
import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
|
||||||
LuLink,
|
|
||||||
LuPlus,
|
|
||||||
LuRadioReceiver,
|
|
||||||
} from "react-icons/lu";
|
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
|
|
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const syncRemoteVirtualMediaState = useCallback(() => {
|
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
notifications.error(
|
notifications.error(m.mount_get_state_error({ error: response.error.message }));
|
||||||
`Failed to get virtual media state: ${response.error.message}`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
|
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const handleUnmount = () => {
|
const handleUnmount = () => {
|
||||||
send("unmountImage", {}, (response: JsonRpcResponse) => {
|
send("unmountImage", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
notifications.error(`Failed to unmount image: ${response.error.message}`);
|
notifications.error(m.mount_unmount_error({ error: response.error.message }));
|
||||||
} else {
|
} else {
|
||||||
syncRemoteVirtualMediaState();
|
syncRemoteVirtualMediaState();
|
||||||
}
|
}
|
||||||
|
|
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||||
No mounted media
|
{m.mount_no_mounted_media()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
Add a file to get started
|
{m.mount_add_file_to_get_started()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||||
Streaming from URL
|
{m.mount_streaming_from_url()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
||||||
{formatters.truncateMiddle(url, 55)}
|
{formatters.truncateMiddle(url, 55)}
|
||||||
|
|
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||||
Mounted from JetKVM Storage
|
{m.mount_mounted_from_storage()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||||
{formatters.truncateMiddle(path, 50)}
|
{formatters.truncateMiddle(path, 50)}
|
||||||
|
|
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Virtual Media"
|
title={m.mount_virtual_media()}
|
||||||
description="Mount an image to boot from or install an operating system."
|
description={m.mount_virtual_media_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -163,9 +158,9 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
{remoteVirtualMediaState ? (
|
{remoteVirtualMediaState ? (
|
||||||
<div className="flex select-none items-center justify-between text-xs">
|
<div className="flex select-none items-center justify-between text-xs">
|
||||||
<div className="select-none text-white dark:text-slate-300">
|
<div className="select-none text-white dark:text-slate-300">
|
||||||
<span>Mounted as</span>{" "}
|
<span>{m.mount_mounted_as()}</span>{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
{remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Close"
|
text={m.close()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
|
|
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Unmount"
|
text={m.mount_unmount()}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<svg
|
<svg
|
||||||
className={`${className} h-2.5 w-2.5 shrink-0`}
|
className={`${className} h-2.5 w-2.5 shrink-0`}
|
||||||
|
|
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Close"
|
text={m.close()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
|
|
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Media"
|
text={m.mount_add_new_media()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalView("mode");
|
setModalView("mode");
|
||||||
navigateTo("/mount");
|
navigateTo("/mount");
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
import { m } from "@localizations/messages.js";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
|
||||||
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
|
||||||
|
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
|
|
@ -105,7 +106,7 @@ export default function PasteModal() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to paste text:", error);
|
console.error("Failed to paste text:", error);
|
||||||
notifications.error("Failed to paste text");
|
notifications.error(m.paste_modal_failed_paste({ error: String(error) }));
|
||||||
}
|
}
|
||||||
}, [selectedKeyboard, executeMacro, delay]);
|
}, [selectedKeyboard, executeMacro, delay]);
|
||||||
|
|
||||||
|
|
@ -122,8 +123,8 @@ export default function PasteModal() {
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Paste text"
|
title={m.paste_modal_paste_text()}
|
||||||
description="Paste text from your client to the remote host"
|
description={m.paste_modal_paste_text_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -143,7 +144,7 @@ export default function PasteModal() {
|
||||||
>
|
>
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
ref={TextAreaRef}
|
ref={TextAreaRef}
|
||||||
label="Paste from host"
|
label={m.paste_modal_paste_from_host()}
|
||||||
rows={4}
|
rows={4}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
maxLength={pasteMaxLength}
|
maxLength={pasteMaxLength}
|
||||||
|
|
@ -176,7 +177,7 @@ export default function PasteModal() {
|
||||||
<div className="mt-2 flex items-center gap-x-2">
|
<div className="mt-2 flex items-center gap-x-2">
|
||||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
The following characters won't be pasted:{" "}
|
{m.paste_modal_invalid_chars_intro()}{" "}
|
||||||
{invalidChars.join(", ")}
|
{invalidChars.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,8 +187,8 @@ export default function PasteModal() {
|
||||||
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
|
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
type="number"
|
type="number"
|
||||||
label="Delay between keys"
|
label={m.paste_modal_delay_between_keys()}
|
||||||
placeholder="Delay between keys"
|
placeholder={m.paste_modal_delay_between_keys()}
|
||||||
min={50}
|
min={50}
|
||||||
max={65534}
|
max={65534}
|
||||||
value={delayValue}
|
value={delayValue}
|
||||||
|
|
@ -199,15 +200,14 @@ export default function PasteModal() {
|
||||||
<div className="mt-2 flex items-center gap-x-2">
|
<div className="mt-2 flex items-center gap-x-2">
|
||||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
Delay must be between 50 and 65534
|
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
|
{m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
|
||||||
{selectedKeyboard.name}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -224,7 +224,7 @@ export default function PasteModal() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Cancel"
|
text={m.cancel()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCancelPasteMode();
|
onCancelPasteMode();
|
||||||
close();
|
close();
|
||||||
|
|
@ -233,7 +233,7 @@ export default function PasteModal() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Confirm Paste"
|
text={m.paste_modal_confirm_paste()}
|
||||||
disabled={isPasteInProgress}
|
disabled={isPasteInProgress}
|
||||||
onClick={onConfirmPaste}
|
onClick={onConfirmPaste}
|
||||||
LeadingIcon={LuCornerDownLeft}
|
LeadingIcon={LuCornerDownLeft}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { LuPlus, LuArrowLeft } from "react-icons/lu";
|
import { LuPlus, LuArrowLeft } from "react-icons/lu";
|
||||||
|
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@/components/Button";
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
interface AddDeviceFormProps {
|
interface AddDeviceFormProps {
|
||||||
onAddDevice: (name: string, macAddress: string) => void;
|
onAddDevice: (name: string, macAddress: string) => void;
|
||||||
|
|
@ -34,8 +35,8 @@ export default function AddDeviceForm({
|
||||||
>
|
>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
ref={nameInputRef}
|
ref={nameInputRef}
|
||||||
placeholder="Plex Media Server"
|
placeholder={m.wake_on_lan_add_device_example_device_name()}
|
||||||
label="Device Name"
|
label={m.wake_on_lan_add_device_device_name()}
|
||||||
required
|
required
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setIsDeviceNameValid(e.target.validity.valid);
|
setIsDeviceNameValid(e.target.validity.valid);
|
||||||
|
|
@ -46,7 +47,7 @@ export default function AddDeviceForm({
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
ref={macInputRef}
|
ref={macInputRef}
|
||||||
placeholder="00:b0:d0:63:c2:26"
|
placeholder="00:b0:d0:63:c2:26"
|
||||||
label="MAC Address"
|
label={m.wake_on_lan_add_device_mac_address()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
required
|
required
|
||||||
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
|
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
|
||||||
|
|
@ -82,14 +83,14 @@ export default function AddDeviceForm({
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Back"
|
text={m.wake_on_lan_add_device_back()}
|
||||||
LeadingIcon={LuArrowLeft}
|
LeadingIcon={LuArrowLeft}
|
||||||
onClick={() => setShowAddForm(false)}
|
onClick={() => setShowAddForm(false)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Save Device"
|
text={m.wake_on_lan_add_device_save_device()}
|
||||||
disabled={!isDeviceNameValid || !isMacAddressValid}
|
disabled={!isDeviceNameValid || !isMacAddressValid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const deviceName = nameInputRef.current?.value || "";
|
const deviceName = nameInputRef.current?.value || "";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
|
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { m } from "@localizations/messages.js";
|
||||||
import Card from "@/components/Card";
|
import { Button } from "@components/Button";
|
||||||
import { FieldError } from "@/components/InputField";
|
import Card from "@components/Card";
|
||||||
|
import { FieldError } from "@components/InputField";
|
||||||
|
|
||||||
export interface StoredDevice {
|
export interface StoredDevice {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -46,7 +47,7 @@ export default function DeviceList({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Wake"
|
text={m.wake_on_lan_device_list_wake()}
|
||||||
LeadingIcon={LuSend}
|
LeadingIcon={LuSend}
|
||||||
onClick={() => onSendMagicPacket(device.macAddress)}
|
onClick={() => onSendMagicPacket(device.macAddress)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -55,7 +56,7 @@ export default function DeviceList({
|
||||||
theme="danger"
|
theme="danger"
|
||||||
LeadingIcon={LuTrash2}
|
LeadingIcon={LuTrash2}
|
||||||
onClick={() => onDeleteDevice(index)}
|
onClick={() => onDeleteDevice(index)}
|
||||||
aria-label="Delete device"
|
aria-label={m.wake_on_lan_device_list_delete_device()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,11 +70,11 @@ export default function DeviceList({
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Device"
|
text={m.wake_on_lan_device_list_add_new_device()}
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { PlusCircleIcon } from "@heroicons/react/16/solid";
|
import { PlusCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { LuPlus } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@/components/Button";
|
import Card from "@components/Card";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
export default function EmptyStateCard({
|
export default function EmptyStateCard({
|
||||||
onCancelWakeOnLanModal,
|
onCancelWakeOnLanModal,
|
||||||
|
|
@ -25,10 +26,10 @@ export default function EmptyStateCard({
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||||
No devices added
|
{m.wake_on_lan_empty_no_devices_added()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
Add a device to start using Wake-on-LAN
|
{m.wake_on_lan_empty_add_device_to_start()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,11 +42,11 @@ export default function EmptyStateCard({
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Device"
|
text={m.wake_on_lan_empty_add_new_device()}
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
import { useRTCStore, useUiStore } from "@hooks/stores";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import EmptyStateCard from "./EmptyStateCard";
|
import EmptyStateCard from "./EmptyStateCard";
|
||||||
|
|
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
const isInvalid = resp.error.data?.includes("invalid MAC address");
|
const isInvalid = resp.error.data?.includes("invalid MAC address");
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
setErrorMessage("Invalid MAC address");
|
setErrorMessage(m.wake_on_lan_invalid_mac());
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Failed to send Magic Packet");
|
setErrorMessage(m.wake_on_lan_failed_send_magic());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Magic Packet sent successfully");
|
notifications.success(m.wake_on_lan_magic_sent_success());
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
|
||||||
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
|
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to add Wake-on-LAN device:", resp.error);
|
console.error("Failed to add Wake-on-LAN device:", resp.error);
|
||||||
setAddDeviceErrorMessage("Failed to add device");
|
setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
|
||||||
} else {
|
} else {
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
syncStoredDevices();
|
syncStoredDevices();
|
||||||
|
|
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Wake On LAN"
|
title={m.wake_on_lan()}
|
||||||
description="Send a Magic Packet to wake up a remote device."
|
description={m.wake_on_lan_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showAddForm ? (
|
{showAddForm ? (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
||||||
import SidebarHeader from "@/components/SidebarHeader";
|
import { m } from "@localizations/messages.js";
|
||||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
import { useRTCStore, useUiStore } from "@hooks/stores";
|
||||||
|
import { createChartArray, Metric } from "@components/Metric";
|
||||||
|
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||||
|
import SidebarHeader from "@components/SidebarHeader";
|
||||||
import { someIterable } from "@/utils";
|
import { someIterable } from "@/utils";
|
||||||
|
|
||||||
import { createChartArray, Metric } from "../Metric";
|
|
||||||
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
|
||||||
|
|
||||||
export default function ConnectionStatsSidebar() {
|
export default function ConnectionStatsSidebar() {
|
||||||
const { sidebarView, setSidebarView } = useUiStore();
|
const { sidebarView, setSidebarView } = useUiStore();
|
||||||
const {
|
const {
|
||||||
|
|
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
||||||
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
|
<SidebarHeader title={m.connection_stats_sidebar()} setSidebarView={setSidebarView} />
|
||||||
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{sidebarView === "connection-stats" && (
|
{sidebarView === "connection-stats" && (
|
||||||
|
|
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
|
||||||
{/* Connection Group */}
|
{/* Connection Group */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="Connection"
|
title={m.connection_stats_connection()}
|
||||||
description="The connection between the client and the JetKVM."
|
description={m.connection_stats_connection_description()}
|
||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
title="Round-Trip Time"
|
title={m.connection_stats_round_trip_time()}
|
||||||
description="Round-trip time for the active ICE candidate pair between peers."
|
description={m.connection_stats_round_trip_time_description()}
|
||||||
stream={iceCandidatePairStats}
|
stream={iceCandidatePairStats}
|
||||||
metric="currentRoundTripTime"
|
metric="currentRoundTripTime"
|
||||||
map={x => ({
|
map={x => ({
|
||||||
|
|
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
|
||||||
{/* Video Group */}
|
{/* Video Group */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="Video"
|
title={m.connection_stats_video()}
|
||||||
description="The video stream from the JetKVM to the client."
|
description={m.connection_stats_video_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* RTP Jitter */}
|
{/* RTP Jitter */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Network Stability"
|
title={m.connection_stats_network_stability()}
|
||||||
badge="Jitter"
|
badge={m.connection_stats_badge_jitter()}
|
||||||
badgeTheme="light"
|
badgeTheme="light"
|
||||||
description="How steady the flow of inbound video packets is across the network."
|
description={m.connection_stats_network_stability_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="jitter"
|
metric="jitter"
|
||||||
map={x => ({
|
map={x => ({
|
||||||
|
|
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Playback Delay */}
|
{/* Playback Delay */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Playback Delay"
|
title={m.connection_stats_playback_delay()}
|
||||||
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
|
description={m.connection_stats_playback_delay_description()}
|
||||||
badge="Jitter Buffer Avg. Delay"
|
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
|
||||||
badgeTheme="light"
|
badgeTheme="light"
|
||||||
data={jitterBufferAvgDelayData}
|
data={jitterBufferAvgDelayData}
|
||||||
gate={inboundVideoRtpStats}
|
gate={inboundVideoRtpStats}
|
||||||
|
|
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Packets Lost */}
|
{/* Packets Lost */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Packets Lost"
|
title={m.connection_stats_packets_lost()}
|
||||||
description="Count of lost inbound video RTP packets."
|
description={m.connection_stats_packets_lost_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="packetsLost"
|
metric="packetsLost"
|
||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
|
|
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Frames Per Second */}
|
{/* Frames Per Second */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Frames per second"
|
title={m.connection_stats_frames_per_second()}
|
||||||
description="Number of inbound video frames displayed per second."
|
description={m.connection_stats_frames_per_second_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="framesPerSecond"
|
metric="framesPerSecond"
|
||||||
domain={[0, 80]}
|
domain={[0, 80]}
|
||||||
|
|
|
||||||
|
|
@ -270,9 +270,8 @@ export class KeyboardMacroReportMessage extends RpcMessage {
|
||||||
...keys,
|
...keys,
|
||||||
...fromUint16toUint8(step.delay),
|
...fromUint16toUint8(step.delay),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const offset = 6 + i * 9;
|
const offset = 6 + i * 9;
|
||||||
|
|
||||||
|
|
||||||
data.set(macroBinary, offset);
|
data.set(macroBinary, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ export default function useKeyboard() {
|
||||||
// support the keyPressReport API. In that case, we need to handle the key presses locally
|
// support the keyPressReport API. In that case, we need to handle the key presses locally
|
||||||
// and send the full state to the device, so it can behave like a real USB HID keyboard.
|
// and send the full state to the device, so it can behave like a real USB HID keyboard.
|
||||||
// This flag indicates whether the keyPressReport API is available on the device which is
|
// This flag indicates whether the keyPressReport API is available on the device which is
|
||||||
// dynamically set when the device responds to the first key press event or reports its // keysDownState when queried since the keyPressReport was introduced together with the
|
// dynamically set when the device responds to the first key press event or reports its
|
||||||
|
// keysDownState when queried since the keyPressReport was introduced together with the
|
||||||
// getKeysDownState API.
|
// getKeysDownState API.
|
||||||
|
|
||||||
// HidRPC is a binary format for exchanging keyboard and mouse events
|
// HidRPC is a binary format for exchanging keyboard and mouse events
|
||||||
|
|
@ -277,7 +278,6 @@ export default function useKeyboard() {
|
||||||
cancelKeepAlive();
|
cancelKeepAlive();
|
||||||
}, [cancelKeepAlive]);
|
}, [cancelKeepAlive]);
|
||||||
|
|
||||||
|
|
||||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||||
// The keys and modifiers are pressed together and held for the delay duration.
|
// The keys and modifiers are pressed together and held for the delay duration.
|
||||||
|
|
@ -306,6 +306,7 @@ export default function useKeyboard() {
|
||||||
|
|
||||||
sendKeyboardMacroEventHidRpc(macro);
|
sendKeyboardMacroEventHidRpc(macro);
|
||||||
}, [sendKeyboardMacroEventHidRpc]);
|
}, [sendKeyboardMacroEventHidRpc]);
|
||||||
|
|
||||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||||
const promises: (() => Promise<void>)[] = [];
|
const promises: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
|
@ -355,6 +356,7 @@ export default function useKeyboard() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
|
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
|
||||||
|
|
||||||
const executeMacro = useCallback(async (steps: MacroSteps) => {
|
const executeMacro = useCallback(async (steps: MacroSteps) => {
|
||||||
if (rpcHidReady) {
|
if (rpcHidReady) {
|
||||||
return executeMacroRemote(steps);
|
return executeMacroRemote(steps);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useCallback } from "react";
|
||||||
import { useDeviceStore } from "@/hooks/stores";
|
import { useDeviceStore } from "@/hooks/stores";
|
||||||
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
|
|
@ -29,7 +30,7 @@ export function useVersion() {
|
||||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||||
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
|
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(`Failed to check for updates: ${resp.error}`);
|
notifications.error(m.updates_failed_check({ error: String(resp.error) }));
|
||||||
reject(new Error("Failed to check for updates"));
|
reject(new Error("Failed to check for updates"));
|
||||||
} else {
|
} else {
|
||||||
const result = resp.result as SystemVersionInfo;
|
const result = resp.result as SystemVersionInfo;
|
||||||
|
|
@ -37,7 +38,7 @@ export function useVersion() {
|
||||||
setSystemVersion(result.local.systemVersion);
|
setSystemVersion(result.local.systemVersion);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
notifications.error(`Failed to check for updates: ${result.error}`);
|
notifications.error(m.updates_failed_check({ error: String(result.error) }));
|
||||||
reject(new Error("Failed to check for updates"));
|
reject(new Error("Failed to check for updates"));
|
||||||
} else {
|
} else {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
|
@ -57,7 +58,7 @@ export function useVersion() {
|
||||||
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
|
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
|
||||||
}
|
}
|
||||||
console.error("Failed to get device version N", resp.error);
|
console.error("Failed to get device version N", resp.error);
|
||||||
notifications.error(`Failed to get device version: ${resp.error}`);
|
notifications.error(m.updates_failed_get_device_version({ error: String(resp.error) }));
|
||||||
reject(new Error("Failed to get device version"));
|
reject(new Error("Failed to get device version"));
|
||||||
} else {
|
} else {
|
||||||
const result = resp.result as VersionInfo;
|
const result = resp.result as VersionInfo;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "./index.css";
|
|
||||||
import {
|
import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
|
|
@ -8,11 +7,13 @@ import {
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
useRouteError,
|
useRouteError,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
|
import "./index.css";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import Root from "@/root";
|
import Root from "@/root";
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import EmptyCard from "@components/EmptyCard";
|
import EmptyCard from "@components/EmptyCard";
|
||||||
import NotFoundPage from "@components/NotFoundPage";
|
import NotFoundPage from "@components/NotFoundPage";
|
||||||
|
|
@ -33,7 +34,7 @@ const SignupRoute = lazy(() => import("@routes/signup"));
|
||||||
const LoginRoute = lazy(() => import("@routes/login"));
|
const LoginRoute = lazy(() => import("@routes/login"));
|
||||||
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
||||||
const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session"));
|
const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session"));
|
||||||
const MountRoute = lazy(() => import("./routes/devices.$id.mount"));
|
const MountRoute = lazy(() => import("@routes/devices.$id.mount"));
|
||||||
const SettingsRoute = lazy(() => import("@routes/devices.$id.settings"));
|
const SettingsRoute = lazy(() => import("@routes/devices.$id.settings"));
|
||||||
const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse"));
|
const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse"));
|
||||||
const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard"));
|
const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard"));
|
||||||
|
|
@ -404,8 +405,8 @@ function ErrorBoundary() {
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
IconElm={ExclamationTriangleIcon}
|
IconElm={ExclamationTriangleIcon}
|
||||||
headline="Oh no!"
|
headline={m.oh_no()}
|
||||||
description="Something went wrong. Please try again later or contact support"
|
description={m.something_went_wrong()}
|
||||||
BtnElm={
|
BtnElm={
|
||||||
errorMessage && (
|
errorMessage && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
|
|
||||||
|
|
||||||
interface NotificationOptions {
|
interface NotificationOptions {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
// Add other options as needed
|
// Add other options as needed
|
||||||
|
|
@ -34,7 +33,7 @@ const ToastContent = ({
|
||||||
const notifications = {
|
const notifications = {
|
||||||
success: (message: string, options?: NotificationOptions) => {
|
success: (message: string, options?: NotificationOptions) => {
|
||||||
return toast.custom(
|
return toast.custom(
|
||||||
t => (
|
(t: Toast) => (
|
||||||
<ToastContent
|
<ToastContent
|
||||||
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
|
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -47,7 +46,7 @@ const notifications = {
|
||||||
|
|
||||||
error: (message: string, options?: NotificationOptions) => {
|
error: (message: string, options?: NotificationOptions) => {
|
||||||
return toast.custom(
|
return toast.custom(
|
||||||
t => (
|
(t: Toast) => (
|
||||||
<ToastContent
|
<ToastContent
|
||||||
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
|
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -64,9 +63,9 @@ function useMaxToasts(max: number) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toasts
|
toasts
|
||||||
.filter(t => t.visible) // Only consider visible toasts
|
.filter((t: Toast) => t.visible) // Only consider visible toasts
|
||||||
.filter((_, i) => i >= max) // Is toast index over limit?
|
.filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
|
||||||
.forEach(t => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
.forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
||||||
}, [toasts, max]);
|
}, [toasts, max]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ export default function SettingsMacrosEditRoute() {
|
||||||
onSubmit={handleUpdateMacro}
|
onSubmit={handleUpdateMacro}
|
||||||
onCancel={() => navigate("../")}
|
onCancel={() => navigate("../")}
|
||||||
isSubmitting={isUpdating}
|
isSubmitting={isUpdating}
|
||||||
submitText="Save Changes"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useLocation, useSearchParams } from "react-router";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
export default function LoginRoute() {
|
export default function LoginRoute() {
|
||||||
|
|
@ -11,11 +12,11 @@ export default function LoginRoute() {
|
||||||
return (
|
return (
|
||||||
<AuthLayout
|
<AuthLayout
|
||||||
showCounter={true}
|
showCounter={true}
|
||||||
title="Connect your JetKVM to the cloud"
|
title={m.auth_connect_to_cloud()}
|
||||||
description="Unlock remote access and advanced features for your device"
|
description={m.auth_connect_to_cloud_description()}
|
||||||
action="Log in & Connect device"
|
action={m.auth_connect_to_cloud_action()}
|
||||||
// Header CTA
|
// Header CTA
|
||||||
cta="Don't have an account?"
|
cta={m.auth_header_cta_dont_have_account()}
|
||||||
ctaHref={`/signup?${sq.toString()}`}
|
ctaHref={`/signup?${sq.toString()}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -23,11 +24,11 @@ export default function LoginRoute() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout
|
<AuthLayout
|
||||||
title="Log in to your JetKVM account"
|
title={m.auth_login()}
|
||||||
description="Log in to access and manage your devices securely"
|
description={m.auth_login_description()}
|
||||||
action="Log in"
|
action={m.auth_login_action()}
|
||||||
// Header CTA
|
// Header CTA
|
||||||
cta="New to JetKVM?"
|
cta={m.auth_header_cta_new_to_jetkvm()}
|
||||||
ctaHref={`/signup?${sq.toString()}`}
|
ctaHref={`/signup?${sq.toString()}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useLocation, useSearchParams } from "react-router";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
export default function SignupRoute() {
|
export default function SignupRoute() {
|
||||||
|
|
@ -11,10 +12,10 @@ export default function SignupRoute() {
|
||||||
return (
|
return (
|
||||||
<AuthLayout
|
<AuthLayout
|
||||||
showCounter={true}
|
showCounter={true}
|
||||||
title="Connect your JetKVM to the cloud"
|
title={m.auth_connect_to_cloud()}
|
||||||
description="Unlock remote access and advanced features for your device."
|
description={m.auth_connect_to_cloud_description()}
|
||||||
action="Signup & Connect device"
|
action={m.auth_signup_connect_to_cloud_action()}
|
||||||
cta="Already have an account?"
|
cta={m.auth_header_cta_already_have_account()}
|
||||||
ctaHref={`/login?${sq.toString()}`}
|
ctaHref={`/login?${sq.toString()}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -22,11 +23,11 @@ export default function SignupRoute() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout
|
<AuthLayout
|
||||||
title="Create your JetKVM account"
|
title={m.auth_signup_create_account()}
|
||||||
description="Create your account and start managing your devices with ease."
|
description={m.auth_signup_create_account_description()}
|
||||||
action="Create Account"
|
action={m.auth_signup_create_account_action()}
|
||||||
// Header CTA
|
// Header CTA
|
||||||
cta="Already have an account?"
|
cta={m.auth_header_cta_already_have_account()}
|
||||||
ctaHref={`/login?${sq.toString()}`}
|
ctaHref={`/login?${sq.toString()}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { cx } from "cva";
|
|
||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
import type { LoaderFunction } from "react-router";
|
import type { LoaderFunction } from "react-router";
|
||||||
|
import { cx } from "cva";
|
||||||
|
|
||||||
|
import api from "@/api";
|
||||||
|
import { DEVICE_API } from "@/ui.config";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { LinkButton } from "@components/Button";
|
import { LinkButton } from "@components/Button";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||||
import DeviceImage from "@/assets/jetkvm-device-still.png";
|
import DeviceImage from "@assets/jetkvm-device-still.png";
|
||||||
import LogoMark from "@/assets/logo-mark.png";
|
import LogoMark from "@assets/logo-mark.png";
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
import api from "../api";
|
|
||||||
|
|
||||||
export interface DeviceStatus {
|
export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
|
|
@ -49,7 +49,7 @@ export default function WelcomeRoute() {
|
||||||
<div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0">
|
<div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0">
|
||||||
<img
|
<img
|
||||||
src={LogoWhiteIcon}
|
src={LogoWhiteIcon}
|
||||||
alt="JetKVM Logo"
|
alt={m.jetkvm_logo()}
|
||||||
className="hidden h-[32px] dark:block"
|
className="hidden h-[32px] dark:block"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
"lib": [
|
||||||
|
"ES2021",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"allowJs": true,
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
|
|
@ -18,16 +23,39 @@
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"types": ["vite/client"],
|
"erasableSyntaxOnly": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
/* Import Aliases */
|
/* Import Aliases */
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": ["./src/components/*"],
|
"@components/*": [
|
||||||
"@routes/*": ["./src/routes/*"],
|
"./src/components/*"
|
||||||
"@assets/*": ["./src/assets/*"],
|
],
|
||||||
"@/*": ["./src/*"]
|
"@routes/*": [
|
||||||
|
"./src/routes/*"
|
||||||
|
],
|
||||||
|
"@hooks/*": [
|
||||||
|
"./src/hooks/*"
|
||||||
|
],
|
||||||
|
"@providers/*": [
|
||||||
|
"./src/providers/*"
|
||||||
|
],
|
||||||
|
"@assets/*": [
|
||||||
|
"./src/assets/*"
|
||||||
|
],
|
||||||
|
"@localizations/*": [
|
||||||
|
"./localization/paraglide/*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
/* Bundler mode */
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import react from "@vitejs/plugin-react-swc";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import basicSsl from "@vitejs/plugin-basic-ssl";
|
import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||||
|
import { paraglideVitePlugin } from "@inlang/paraglide-js";
|
||||||
|
|
||||||
declare const process: {
|
declare const process: {
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -22,10 +23,20 @@ export default defineConfig(({ mode, command }) => {
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
react()
|
react()
|
||||||
];
|
];
|
||||||
|
|
||||||
if (useSSL) {
|
if (useSSL) {
|
||||||
plugins.push(basicSsl());
|
plugins.push(basicSsl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins.push(paraglideVitePlugin({
|
||||||
|
project: "./localization/jetKVM.UI.inlang",
|
||||||
|
outdir: "./localization/paraglide",
|
||||||
|
outputStructure: 'message-modules',
|
||||||
|
cookieName: 'JETKVM_LOCALE',
|
||||||
|
localStorageKey: 'JETKVM_LOCALE',
|
||||||
|
strategy: ['cookie', 'localStorage', 'preferredLanguage', 'baseLocale'],
|
||||||
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue