mirror of https://github.com/jetkvm/kvm.git
Merge 1647b80b8c into cc9ff74276
This commit is contained in:
commit
13407d2bd8
|
|
@ -8,7 +8,8 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"customizations": {
|
||||
|
|
@ -31,8 +32,11 @@
|
|||
// Frontend
|
||||
"esbenp.prettier-vscode",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
|
@ -97,38 +97,42 @@ tail -f /var/log/jetkvm.log
|
|||
|
||||
```
|
||||
/kvm/
|
||||
├── main.go # App entry point
|
||||
├── config.go # Settings & configuration
|
||||
├── display.go # Device UI control
|
||||
├── web.go # API endpoints
|
||||
├── cmd/ # Command line main
|
||||
├── internal/ # Internal Go packages
|
||||
│ ├── confparser/ # Configuration file implementation
|
||||
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
|
||||
│ ├── logging/ # Logging implementation
|
||||
│ ├── mdns/ # mDNS implementation
|
||||
│ ├── native/ # CGO / Native code glue layer (on-device hardware)
|
||||
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
|
||||
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
|
||||
│ ├── network/ # Network implementation
|
||||
│ ├── timesync/ # Time sync/NTP implementation
|
||||
│ ├── tzdata/ # Timezone data and generation
|
||||
│ ├── udhcpc/ # DHCP implementation
|
||||
│ ├── usbgadget/ # USB gadget
|
||||
│ ├── utils/ # SSH handling
|
||||
│ └── websecure/ # TLS certificate management
|
||||
├── resource/ # netboot iso and other resources
|
||||
├── scripts/ # Bash shell scripts for building and deploying
|
||||
└── static/ # (react client build output)
|
||||
└── ui/ # React frontend
|
||||
├── public/ # UI website static images and fonts
|
||||
└── src/ # Client React UI
|
||||
├── assets/ # UI in-page images
|
||||
├── components/ # UI components
|
||||
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
||||
├── keyboardLayouts/ # Keyboard layout definitions
|
||||
├── providers/ # Feature flags
|
||||
└── routes/ # Pages (login, settings, etc.)
|
||||
├── main.go # App entry point
|
||||
├── config.go # Settings & configuration
|
||||
├── display.go # Device UI control
|
||||
├── web.go # API endpoints
|
||||
├── cmd/ # Command line main
|
||||
├── internal/ # Internal Go packages
|
||||
│ ├── confparser/ # Configuration file implementation
|
||||
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
|
||||
│ ├── logging/ # Logging implementation
|
||||
│ ├── mdns/ # mDNS implementation
|
||||
│ ├── native/ # CGO / Native code glue layer (on-device hardware)
|
||||
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
|
||||
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
|
||||
│ ├── network/ # Network implementation
|
||||
│ ├── timesync/ # Time sync/NTP implementation
|
||||
│ ├── tzdata/ # Timezone data and generation
|
||||
│ ├── udhcpc/ # DHCP implementation
|
||||
│ ├── usbgadget/ # USB gadget
|
||||
│ ├── utils/ # SSH handling
|
||||
│ └── websecure/ # TLS certificate management
|
||||
├── resource/ # netboot iso and other resources
|
||||
├── scripts/ # Bash shell scripts for building and deploying
|
||||
└── static/ # (react client build output)
|
||||
└── ui/ # React frontend
|
||||
├── localization/ # Client UI localization (i18n)
|
||||
│ ├── jetKVM.UI.inlang/ # Settings for inlang
|
||||
│ └── messages/ # Messages localized
|
||||
├── public/ # UI website static images and fonts
|
||||
└── src/ # Client React UI
|
||||
├── assets/ # UI in-page images
|
||||
├── components/ # UI components
|
||||
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
||||
├── keyboardLayouts/ # Keyboard layout definitions
|
||||
│ ├── paraglide/ # (localization compiled messages output)
|
||||
├── providers/ # Feature flags
|
||||
└── routes/ # Pages (login, settings, etc.)
|
||||
```
|
||||
|
||||
**Key files for beginners:**
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ const {
|
|||
fixupConfigRules,
|
||||
} = require("@eslint/compat");
|
||||
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const {
|
||||
|
|
@ -23,6 +21,9 @@ const compat = new FlatCompat({
|
|||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||
|
||||
module.exports = defineConfig([{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
|
@ -66,7 +67,7 @@ module.exports = defineConfig([{
|
|||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
"newlines-between": "always",
|
||||
}],
|
||||
|
||||
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_", "varsIgnorePattern": "^_"
|
||||
}],
|
||||
|
|
@ -81,7 +82,10 @@ module.exports = defineConfig([{
|
|||
map: [
|
||||
["@components", "./src/components"],
|
||||
["@routes", "./src/routes"],
|
||||
["@hooks", "./src/hooks"],
|
||||
["@providers", "./src/providers"],
|
||||
["@assets", "./src/assets"],
|
||||
["@localizations", "./localization/paraglide"],
|
||||
["@", "./src"],
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -45,31 +45,39 @@
|
|||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<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="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>
|
||||
function applyThemeFromPreference() {
|
||||
// dark theme setup
|
||||
var darkDesired = localStorage.theme === "dark" ||
|
||||
var darkDesired =
|
||||
localStorage.theme === "dark" ||
|
||||
(!("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
|
||||
applyThemeFromPreference();
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyThemeFromPreference);
|
||||
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", applyThemeFromPreference);
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", applyThemeFromPreference);
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: light)")
|
||||
.addEventListener("change", applyThemeFromPreference);
|
||||
</script>
|
||||
</head>
|
||||
<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>
|
||||
</body>
|
||||
</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,726 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"access_adopt_kvm": "Adopt KVM to Cloud",
|
||||
"access_adopted_message": "Your device is adopted to the Cloud",
|
||||
"access_auth_mode_no_password": "Current mode: No password",
|
||||
"access_auth_mode_password": "Current mode: Password protected",
|
||||
"access_authentication_mode_title": "Authentication Mode",
|
||||
"access_certificate_label": "Certificate",
|
||||
"access_change_password_button": "Change Password",
|
||||
"access_change_password_description": "Update your device access password",
|
||||
"access_change_password_title": "Change Password",
|
||||
"access_cloud_api_url_label": "Cloud API URL",
|
||||
"access_cloud_app_url_label": "Cloud Application URL",
|
||||
"access_cloud_provider_description": "Select the cloud provider for your device",
|
||||
"access_cloud_provider_title": "Cloud Provider",
|
||||
"access_cloud_security_title": "Cloud Security",
|
||||
"access_confirm_deregister": "Are you sure you want to de-register this device?",
|
||||
"access_deregister": "De-register from Cloud",
|
||||
"access_description": "Manage the Access Control of the device",
|
||||
"access_disable_protection": "Disable Protection",
|
||||
"access_enable_password": "Enable Password",
|
||||
"access_failed_deregister": "Failed to de-register device: {error}",
|
||||
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
|
||||
"access_failed_update_tls": "Failed to update TLS settings: {error}",
|
||||
"access_github_link": "GitHub",
|
||||
"access_https_description": "Configure secure HTTPS access to your device",
|
||||
"access_https_mode_title": "HTTPS Mode",
|
||||
"access_learn_security": "Learn about our cloud security",
|
||||
"access_local_description": "Manage the mode of local access to the device",
|
||||
"access_local_title": "Local",
|
||||
"access_no_device_id": "No device ID available",
|
||||
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
|
||||
"access_private_key_label": "Private Key",
|
||||
"access_provider_custom": "Custom",
|
||||
"access_provider_jetkvm": "JetKVM Cloud",
|
||||
"access_remote_description": "Manage the mode of Remote access to the device",
|
||||
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
|
||||
"access_security_oidc": "OIDC (OpenID Connect) authentication",
|
||||
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
|
||||
"access_security_streams": "All streams encrypted in transit",
|
||||
"access_security_zero_trust": "Zero Trust security model",
|
||||
"access_title": "Access",
|
||||
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
|
||||
"access_tls_certificate_title": "TLS Certificate",
|
||||
"access_tls_custom": "Custom",
|
||||
"access_tls_disabled": "Disabled",
|
||||
"access_tls_self_signed": "Self-signed",
|
||||
"access_tls_updated": "TLS settings updated successfully",
|
||||
"access_update_tls_settings": "Update TLS Settings",
|
||||
"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",
|
||||
"advanced_description": "Access additional settings for troubleshooting and customization",
|
||||
"advanced_dev_channel_description": "Receive early updates from the development channel",
|
||||
"advanced_dev_channel_title": "Dev Channel Updates",
|
||||
"advanced_developer_mode_description": "Enable advanced features for developers",
|
||||
"advanced_developer_mode_enabled_title": "Developer Mode Enabled",
|
||||
"advanced_developer_mode_title": "Developer Mode",
|
||||
"advanced_developer_mode_warning_advanced": "For advanced users only. Not for production use.",
|
||||
"advanced_developer_mode_warning_risks": "Only use if you understand the risks",
|
||||
"advanced_developer_mode_warning_security": "Security is weakened while active",
|
||||
"advanced_disable_usb_emulation": "Disable USB Emulation",
|
||||
"advanced_enable_usb_emulation": "Enable USB Emulation",
|
||||
"advanced_error_loopback_disable": "Failed to disable loopback-only mode: {error}",
|
||||
"advanced_error_loopback_enable": "Failed to enable loopback-only mode: {error}",
|
||||
"advanced_error_reset_config": "Failed to reset configuration: {error}",
|
||||
"advanced_error_set_dev_channel": "Failed to set dev channel state: {error}",
|
||||
"advanced_error_set_dev_mode": "Failed to set dev mode: {error}",
|
||||
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
|
||||
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
|
||||
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
|
||||
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
|
||||
"advanced_loopback_only_title": "Loopback-Only Mode",
|
||||
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
|
||||
"advanced_loopback_warning_cloud": "Cloud access enabled and working",
|
||||
"advanced_loopback_warning_confirm": "I Understand, Enable Anyway",
|
||||
"advanced_loopback_warning_description": "WARNING: This will restrict web interface access to localhost (127.0.0.1) only.",
|
||||
"advanced_loopback_warning_ssh": "SSH access configured and tested",
|
||||
"advanced_loopback_warning_title": "Enable Loopback-Only Mode?",
|
||||
"advanced_reset_config_button": "Reset Config",
|
||||
"advanced_reset_config_description": "Reset configuration to default. This will log you out.",
|
||||
"advanced_reset_config_title": "Reset Configuration",
|
||||
"advanced_ssh_access_description": "Add your SSH public key to enable secure remote access to the device",
|
||||
"advanced_ssh_access_title": "SSH Access",
|
||||
"advanced_ssh_default_user": "The default SSH user is",
|
||||
"advanced_ssh_public_key_label": "SSH Public Key",
|
||||
"advanced_ssh_public_key_placeholder": "Enter your SSH public key",
|
||||
"advanced_success_loopback_disabled": "Loopback-only mode disabled. Restart your device to apply.",
|
||||
"advanced_success_loopback_enabled": "Loopback-only mode enabled. Restart your device to apply.",
|
||||
"advanced_success_reset_config": "Configuration reset to default successfully",
|
||||
"advanced_success_update_ssh_key": "SSH key updated successfully",
|
||||
"advanced_title": "Advanced",
|
||||
"advanced_troubleshooting_mode_description": "Diagnostic tools and additional controls for troubleshooting and development purposes",
|
||||
"advanced_troubleshooting_mode_title": "Troubleshooting Mode",
|
||||
"advanced_update_ssh_key_button": "Update SSH Key",
|
||||
"advanced_usb_emulation_description": "Control the USB emulation state",
|
||||
"advanced_usb_emulation_title": "USB Emulation",
|
||||
"already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.",
|
||||
"already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.",
|
||||
"already_adopted_return_to_dashboard": "Return to Dashboard",
|
||||
"already_adopted_title": "Device Already Registered",
|
||||
"appearance_description": "Choose your preferred color theme",
|
||||
"appearance_page_description": "Customize the look and feel of your JetKVM interface",
|
||||
"appearance_theme_dark": "Dark",
|
||||
"appearance_theme_light": "Light",
|
||||
"appearance_theme_system": "System",
|
||||
"appearance_theme": "Theme",
|
||||
"appearance_title": "Appearance",
|
||||
"attach": "Attach",
|
||||
"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",
|
||||
"auth_authentication_mode_error": "An error occurred while setting the authentication mode",
|
||||
"auth_authentication_mode_invalid": "Invalid authentication mode",
|
||||
"auth_authentication_mode": "Please select an authentication mode",
|
||||
"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_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_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_mode_local_change_later": "You can always change your authentication method later in the settings.",
|
||||
"auth_mode_local_description": "Select how you would like to secure your JetKVM device locally.",
|
||||
"auth_mode_local_no_password_description": "Quick access without password authentication.",
|
||||
"auth_mode_local_no_password": "No Password",
|
||||
"auth_mode_local_password_confirm_description": "Confirm your password",
|
||||
"auth_mode_local_password_confirm_label": "Confirm Password",
|
||||
"auth_mode_local_password_description": "Secure your device with a password for added protection.",
|
||||
"auth_mode_local_password_do_not_match": "Passwords do not match",
|
||||
"auth_mode_local_password_failed_set": "Failed to set password: {error}",
|
||||
"auth_mode_local_password_note_local": "All data remains on your local device.",
|
||||
"auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
|
||||
"auth_mode_local_password_set_button": "Set Password",
|
||||
"auth_mode_local_password_set_description": "Create a strong password to secure your JetKVM device locally.",
|
||||
"auth_mode_local_password_set_label": "Enter a password",
|
||||
"auth_mode_local_password_set": "Set a Password",
|
||||
"auth_mode_local_password": "Password",
|
||||
"auth_mode_local": "Local Authentication Method",
|
||||
"auth_signup_connect_to_cloud_action": "Signup & Connect device",
|
||||
"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",
|
||||
"back_to_devices": "Back to Devices",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.",
|
||||
"cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.",
|
||||
"cloud_kvms_no_devices": "No devices found",
|
||||
"cloud_kvms": "Cloud KVMs",
|
||||
"confirm": "Confirm",
|
||||
"connect_to_kvm": "Connect to KVM",
|
||||
"connecting_to_device": "Connecting to device…",
|
||||
"connection_established": "Connection established",
|
||||
"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",
|
||||
"continue": "Continue",
|
||||
"creating_peer_connection": "Creating peer connection…",
|
||||
"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",
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
"deregister_button": "Deregister from Cloud",
|
||||
"deregister_cloud_devices": "Cloud Devices",
|
||||
"deregister_description": "This will remove the device from your cloud account and revoke remote access to it. Please note that local access will still be possible",
|
||||
"deregister_error": "There was an error {status} deregistering your device. Please try again.",
|
||||
"deregister_from_cloud": "Deregister from cloud",
|
||||
"deregister_headline": "Deregister {device} from your cloud account",
|
||||
"detach": "Detach",
|
||||
"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",
|
||||
"dhcp_server": "DHCP Server",
|
||||
"dns_servers": "DNS Servers",
|
||||
"establishing_secure_connection": "Establishing secure connection…",
|
||||
"experimental": "Experimental",
|
||||
"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",
|
||||
"gathering_ice_candidates": "Gathering ICE candidates…",
|
||||
"general_app_version": "App: {version}",
|
||||
"general_auto_update_description": "Automatically update the device to the latest version",
|
||||
"general_auto_update_error": "Failed to set auto-update: {error}",
|
||||
"general_auto_update_title": "Auto Update",
|
||||
"general_check_for_updates": "Check for Updates",
|
||||
"general_page_description": "Configure device settings and update preferences",
|
||||
"general_reboot_description": "Do you want to proceed with rebooting the system?",
|
||||
"general_reboot_device_description": "Power cycle the JetKVM",
|
||||
"general_reboot_device": "Reboot Device",
|
||||
"general_reboot_no_button": "No",
|
||||
"general_reboot_title": "Reboot JetKVM",
|
||||
"general_reboot_yes_button": "Yes",
|
||||
"general_system_version": "System: {version}",
|
||||
"general_title": "General",
|
||||
"general_update_app_update_title": "App Update",
|
||||
"general_update_application_type": "App",
|
||||
"general_update_available_description": "A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.",
|
||||
"general_update_available_title": "Update available",
|
||||
"general_update_background_button": "Update in Background",
|
||||
"general_update_check_again_button": "Check Again",
|
||||
"general_update_checking_description": "We're ensuring your device has the latest features and improvements.",
|
||||
"general_update_checking_title": "Checking for updates…",
|
||||
"general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!",
|
||||
"general_update_completed_title": "Update Completed Successfully",
|
||||
"general_update_error_description": "An error occurred while updating your device. Please try again later.",
|
||||
"general_update_error_details": "Error details: {errorMessage}",
|
||||
"general_update_error_title": "Update Error",
|
||||
"general_update_later_button": "Do it later",
|
||||
"general_update_now_button": "Update Now",
|
||||
"general_update_rebooting": "Rebooting to complete the update…",
|
||||
"general_update_status_awaiting_reboot": "Awaiting reboot",
|
||||
"general_update_status_downloading": "Downloading {update_type} update…",
|
||||
"general_update_status_fetching": "Fetching update information…",
|
||||
"general_update_status_installing": "Installing {update_type} update…",
|
||||
"general_update_status_verifying": "Verifying {update_type} update…",
|
||||
"general_update_system_type": "System",
|
||||
"general_update_system_update_title": "Linux System Update",
|
||||
"general_update_up_to_date_description": "Your system is running the latest version. No updates are currently available.",
|
||||
"general_update_up_to_date_title": "System is up to date",
|
||||
"general_update_updating_description": "Please don't turn off your device. This process may take a few minutes.",
|
||||
"general_update_updating_title": "Updating your device",
|
||||
"getting_remote_session_description": "Getting remote session description attempt {attempt}",
|
||||
"hardware_backlight_settings_error": "Failed to set backlight settings: {error}",
|
||||
"hardware_backlight_settings_get_error": "Failed to get backlight settings: {error}",
|
||||
"hardware_backlight_settings_success": "Backlight settings updated successfully",
|
||||
"hardware_dim_display_after_description": "Set how long to wait before dimming the display",
|
||||
"hardware_dim_display_after_title": "Dim Display After",
|
||||
"hardware_display_brightness_description": "Set the brightness of the display",
|
||||
"hardware_display_brightness_high": "High",
|
||||
"hardware_display_brightness_low": "Low",
|
||||
"hardware_display_brightness_medium": "Medium",
|
||||
"hardware_display_brightness_off": "Off",
|
||||
"hardware_display_brightness_title": "Display Brightness",
|
||||
"hardware_display_orientation_description": "Set the orientation of the display",
|
||||
"hardware_display_orientation_error": "Failed to set display orientation: {error}",
|
||||
"hardware_display_orientation_inverted": "Inverted",
|
||||
"hardware_display_orientation_normal": "Normal",
|
||||
"hardware_display_orientation_success": "Display orientation updated successfully",
|
||||
"hardware_display_orientation_title": "Display Orientation",
|
||||
"hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.",
|
||||
"hardware_page_description": "Configure display settings and hardware options for your JetKVM device",
|
||||
"hardware_time_1_hour": "1 Hour",
|
||||
"hardware_time_1_minute": "1 Minute",
|
||||
"hardware_time_10_minutes": "10 Minutes",
|
||||
"hardware_time_30_minutes": "30 Minutes",
|
||||
"hardware_time_5_minutes": "5 Minutes",
|
||||
"hardware_time_never": "Never",
|
||||
"hardware_title": "Hardware",
|
||||
"hardware_turn_off_display_after_description": "Period of inactivity before display automatically turns off",
|
||||
"hardware_turn_off_display_after_title": "Turn off Display After",
|
||||
"hide": "Hide",
|
||||
"ice_gathering_completed": "ICE Gathering completed",
|
||||
"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",
|
||||
"invalid_password": "Invalid password",
|
||||
"ip_address": "IP Address",
|
||||
"ipv6_address_label": "Address",
|
||||
"ipv6_addresses": "IPv6 Addresses",
|
||||
"ipv6_information": "IPv6 Information",
|
||||
"ipv6_link_local": "Link-local",
|
||||
"ipv6_preferred_lifetime": "Preferred Lifetime",
|
||||
"ipv6_valid_lifetime": "Valid Lifetime",
|
||||
"jetkvm_description": "JetKVM combines powerful hardware with intuitive software to provide a seamless remote control experience.",
|
||||
"jetkvm_device": "JetKVM Device",
|
||||
"jetkvm_logo": "JetKVM Logo",
|
||||
"jetkvm_setup": "Set up your JetKVM",
|
||||
"jetkvm": "JetKVM",
|
||||
"jiggler_cron_schedule_description": "Cron expression for scheduling",
|
||||
"jiggler_cron_schedule_label": "Cron Schedule",
|
||||
"jiggler_example_business_hours_early": "Business Hours 8-17",
|
||||
"jiggler_example_business_hours_late": "Business Hours 9-17",
|
||||
"jiggler_examples_label": "Examples",
|
||||
"jiggler_inactivity_limit_description": "Inactivity time before jiggle",
|
||||
"jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
|
||||
"jiggler_more_examples": "More examples",
|
||||
"jiggler_random_delay_description": "To avoid recognizable patterns",
|
||||
"jiggler_random_delay_label": "Random delay",
|
||||
"jiggler_save_jiggler_config": "Save Jiggler Config",
|
||||
"jiggler_timezone_description": "Timezone for cron schedule",
|
||||
"jiggler_timezone_label": "Timezone",
|
||||
"keyboard_description": "Configure keyboard settings for your device",
|
||||
"keyboard_layout_description": "Keyboard layout of target operating system",
|
||||
"keyboard_layout_error": "Failed to set keyboard layout: {error}",
|
||||
"keyboard_layout_long_description": "The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.",
|
||||
"keyboard_layout_success": "Keyboard layout set successfully to {layout}",
|
||||
"keyboard_layout_title": "Keyboard Layout",
|
||||
"keyboard_show_pressed_keys_description": "Display currently pressed keys in the status bar",
|
||||
"keyboard_show_pressed_keys_title": "Show Pressed Keys",
|
||||
"keyboard_title": "Keyboard",
|
||||
"kvm_terminal": "KVM Terminal",
|
||||
"last_online": "Last online {time}",
|
||||
"learn_more": "Learn more",
|
||||
"load": "Load",
|
||||
"loading": "Loading…",
|
||||
"local_auth_change_local_device_password_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_change_local_device_password_title": "Change Local Device Password",
|
||||
"local_auth_confirm_new_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_placeholder": "Re-enter your password",
|
||||
"local_auth_create_description": "Create a password to protect your device from unauthorized local access.",
|
||||
"local_auth_create_new_password_label": "New Password",
|
||||
"local_auth_create_new_password_placeholder": "Enter a strong password",
|
||||
"local_auth_create_not_now_button": "Not Now",
|
||||
"local_auth_create_secure_button": "Secure Device",
|
||||
"local_auth_create_title": "Local Device Protection",
|
||||
"local_auth_current_password_label": "Current Password",
|
||||
"local_auth_disable_local_device_protection_description": "Enter your current password to disable local device protection.",
|
||||
"local_auth_disable_local_device_protection_title": "Disable Local Device Protection",
|
||||
"local_auth_disable_protection_button": "Disable Protection",
|
||||
"local_auth_enter_current_password_placeholder": "Enter your current password",
|
||||
"local_auth_enter_new_password_placeholder": "Enter a new strong password",
|
||||
"local_auth_error_changing_password": "An error occurred while changing the password",
|
||||
"local_auth_error_disabling_password": "An error occurred while disabling the password",
|
||||
"local_auth_error_enter_current_password": "Please enter your current password",
|
||||
"local_auth_error_enter_new_password": "Please enter a new password",
|
||||
"local_auth_error_enter_old_password": "Please enter your old password",
|
||||
"local_auth_error_enter_password": "Please enter a password",
|
||||
"local_auth_error_passwords_not_match": "Passwords do not match",
|
||||
"local_auth_error_setting_password": "An error occurred while setting the password",
|
||||
"local_auth_new_password_label": "New Password",
|
||||
"local_auth_reenter_new_password_placeholder": "Re-enter your new password",
|
||||
"local_auth_success_password_disabled_description": "You've successfully disabled the password protection for local access. Remember, your device is now less secure.",
|
||||
"local_auth_success_password_disabled_title": "Password Protection Disabled",
|
||||
"local_auth_success_password_set_description": "You've successfully set up local device protection. Your device is now secure against unauthorized local access.",
|
||||
"local_auth_success_password_set_title": "Password Set Successfully",
|
||||
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
|
||||
"local_auth_success_password_updated_title": "Password Updated Successfully",
|
||||
"local_auth_update_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_update_current_password_label": "Current Password",
|
||||
"local_auth_update_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_update_new_password_label": "New Password",
|
||||
"local_auth_update_password_button": "Update Password",
|
||||
"local_auth_update_title": "Change Local Device Password",
|
||||
"log_in": "Log In",
|
||||
"log_out": "Log out",
|
||||
"logged_in_as": "Logged in as",
|
||||
"login_enter_password_description": "Enter your password to access your JetKVM.",
|
||||
"login_enter_password": "Enter your password",
|
||||
"login_error": "An error occurred while logging in",
|
||||
"login_forgot_password": "Forgot password?",
|
||||
"login_password_label": "Password",
|
||||
"login_welcome_back": "Welcome back to JetKVM",
|
||||
"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_error": "An error occurred while saving.",
|
||||
"macro_save": "Save Macro",
|
||||
"macro_step_count": "{steps} / {max} 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…",
|
||||
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
|
||||
"macro_steps_label": "Steps",
|
||||
"macros_add_description": "Create a new keyboard macro",
|
||||
"macros_add_new": "Add New Macro",
|
||||
"macros_create_first": "Create your first macro to get started",
|
||||
"macros_created_success": "Macro \"{name}\" created successfully",
|
||||
"macros_delete_confirm": "Are you sure you want to delete this macro? This action cannot be undone.",
|
||||
"macros_delete_macro": "Delete Macro",
|
||||
"macros_deleted_success": "Macro \"{name}\" deleted successfully",
|
||||
"macros_deleting": "Deleting",
|
||||
"macros_duplicate": "Duplicate",
|
||||
"macros_duplicated_success": "Macro \"{name}\" duplicated successfully",
|
||||
"macros_edit_description": "Modify your keyboard macro",
|
||||
"macros_edit_title": "Edit Macro",
|
||||
"macros_edit": "Edit",
|
||||
"macros_failed_create": "Failed to create macro",
|
||||
"macros_failed_create_error": "Failed to create macro: {error}",
|
||||
"macros_failed_delete": "Failed to delete macro",
|
||||
"macros_failed_delete_error": "Failed to delete macro: {error}",
|
||||
"macros_failed_duplicate": "Failed to duplicate macro",
|
||||
"macros_failed_duplicate_error": "Failed to duplicate macro: {error}",
|
||||
"macros_failed_reorder": "Failed to reorder macros",
|
||||
"macros_failed_reorder_error": "Failed to reorder macros: {error}",
|
||||
"macros_failed_update": "Failed to update macro",
|
||||
"macros_failed_update_error": "Failed to update macro: {error}",
|
||||
"macros_invalid_data": "Invalid macro data",
|
||||
"macros_maximum_macros_reached": "You have reached the maximum number of {maximum} macros allowed.",
|
||||
"macros_move_down": "Move Down",
|
||||
"macros_move_up": "Move Up",
|
||||
"macros_no_macros_available": "No macros available",
|
||||
"macros_no_macros_found": "No macros found",
|
||||
"macros_order_updated": "Macro order updated successfully",
|
||||
"macros_title": "Keyboard Macros",
|
||||
"macros_updated_success": "Macro \"{name}\" updated successfully",
|
||||
"macros_aria_delete": "Delete macro {name}",
|
||||
"macros_aria_duplicate": "Duplicate macro {name}",
|
||||
"macros_aria_edit": "Edit macro {name}",
|
||||
"macros_aria_move_down": "Move {name} down",
|
||||
"macros_aria_move_up": "Move {name} up",
|
||||
"macros_confirm_delete_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||
"macros_confirm_delete_title": "Delete Macro",
|
||||
"macros_confirm_deleting": "Deleting…",
|
||||
"macros_add_new_macro": "Add New Macro",
|
||||
"macros_aria_add_new": "Add new macro",
|
||||
"macros_create_first_headline": "Create Your First Macro",
|
||||
"macros_create_first_description": "Combine keystrokes into a single action",
|
||||
"macros_delay_only": "Delay only",
|
||||
"macros_edit_button": "Edit",
|
||||
"macros_loading": "Loading macros…",
|
||||
"macros_max_reached": "Max Reached",
|
||||
"metric_not_supported": "Metric not supported",
|
||||
"metric_waiting_for_data": "Waiting for data…",
|
||||
"mount_add_file_to_get_started": "Add a file to get started",
|
||||
"mount_add_new_media": "Add New Media",
|
||||
"mount_available_storage": "Available Storage",
|
||||
"mount_button_back_to_overview": "Back to Overview",
|
||||
"mount_button_cancel_upload": "Cancel Upload",
|
||||
"mount_button_continue_upload": "Continue uploading",
|
||||
"mount_button_mount_file": "Mount File",
|
||||
"mount_button_mount_url": "Mount URL",
|
||||
"mount_button_next": "Next",
|
||||
"mount_button_previous": "Previous",
|
||||
"mount_button_select": "Select",
|
||||
"mount_button_showing_results": "Showing {from} to {to} of {total} results",
|
||||
"mount_button_upload_new_image": "Upload a new image",
|
||||
"mount_bytes_free": "{bytesFree} free",
|
||||
"mount_bytes_used": "{bytesUsed} used",
|
||||
"mount_calculating": "Calculating…",
|
||||
"mount_click_to_select_file": "Click to select a file",
|
||||
"mount_click_to_select_incomplete": "Click to select \"{name}\"",
|
||||
"mount_confirm_delete": "Are you sure you want to delete {name}?",
|
||||
"mount_continue_uploading_with_name": "Continue uploading \"{name}\"",
|
||||
"mount_description_mode": "Choose how you want to mount your virtual media",
|
||||
"mount_error_delete_file": "Error deleting file: {error}",
|
||||
"mount_error_description": "An error occurred while attempting to mount the media. Please try again.",
|
||||
"mount_error_get_storage_space": "Error getting storage space: {error}",
|
||||
"mount_error_list_storage": "Error listing storage files: {error}",
|
||||
"mount_error_title": "Mount Error",
|
||||
"mount_get_state_error": "Failed to get virtual media state: {error}",
|
||||
"mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage",
|
||||
"mount_jetkvm_storage": "JetKVM Storage Mount",
|
||||
"mount_label_mount_as": "Mount as",
|
||||
"mount_label_url_description": "Mount files from any public web address",
|
||||
"mount_label_url": "URL Mount",
|
||||
"mount_mode_cdrom": "CD/DVD",
|
||||
"mount_mode_disk": "Disk",
|
||||
"mount_mounted_as": "Mounted as",
|
||||
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
|
||||
"mount_no_images_description": "Upload an image to start virtual media mounting.",
|
||||
"mount_no_images_title": "No images available",
|
||||
"mount_no_mounted_media": "No mounted media",
|
||||
"mount_percentage_used": "{percentageUsed}% used",
|
||||
"mount_please_select_file_to_upload": "Please select the file to upload.",
|
||||
"mount_please_select_file": "Please select the file \"{name}\" to continue the upload.",
|
||||
"mount_popular_images": "Popular images",
|
||||
"mount_streaming_from_url": "Streaming from URL",
|
||||
"mount_supported_formats": "Supported formats: ISO, IMG",
|
||||
"mount_tag_experimental": "Experimental",
|
||||
"mount_title_mode": "Virtual Media Source",
|
||||
"mount_unmount_error": "Failed to unmount image: {error}",
|
||||
"mount_unmount": "Unmount",
|
||||
"mount_upload_description": "Select an image file to upload to JetKVM storage",
|
||||
"mount_upload_error": "Upload error: {error}",
|
||||
"mount_upload_failed_datachannel": "Failed to create data channel for file upload",
|
||||
"mount_upload_failed_rtc": "Upload failed: {error}",
|
||||
"mount_upload_successful": "Upload successful",
|
||||
"mount_upload_title": "Upload New Image",
|
||||
"mount_uploaded_has_been_uploaded": "{name} has been uploaded",
|
||||
"mount_uploading_with_name": "Uploading {name}",
|
||||
"mount_uploading": "Uploading…",
|
||||
"mount_url_description": "Mount files from any public web address",
|
||||
"mount_url_input_label": "Image URL",
|
||||
"mount_url_mount": "URL Mount",
|
||||
"mount_view_device_description": "Select an image to mount from the JetKVM storage",
|
||||
"mount_view_device_title": "Mount from JetKVM Storage",
|
||||
"mount_view_url_description": "Enter an URL to the image file to mount",
|
||||
"mount_view_url_title": "Mount from URL",
|
||||
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
|
||||
"mount_virtual_media_source_description": "Choose how you want to mount your virtual media",
|
||||
"mount_virtual_media_source": "Virtual Media Source",
|
||||
"mount_virtual_media": "Virtual Media",
|
||||
"never_seen_online": "Never seen online",
|
||||
"next": "Next",
|
||||
"no_results_found": "No results found",
|
||||
"not_available": "N/A",
|
||||
"not_found": "Not found",
|
||||
"ntp_servers": "NTP Servers",
|
||||
"oh_no": "Oh no!",
|
||||
"online": "Online",
|
||||
"other_session_detected": "Another Active Session Detected",
|
||||
"other_session_take_over": " Only one active session is supported at a time. Would you like to take over this session?",
|
||||
"other_session_use_here_button": "Use Here",
|
||||
"page_not_found_description": "The page you were looking for does not exist.",
|
||||
"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_failed_paste": "Failed to paste text: {error}",
|
||||
"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}",
|
||||
"peer_connection_closed": "Closed",
|
||||
"peer_connection_closing": "Closing",
|
||||
"peer_connection_connected": "Connected",
|
||||
"peer_connection_connecting": "Connecting",
|
||||
"peer_connection_disconnected": "Disconnected",
|
||||
"peer_connection_error": "Connection error",
|
||||
"peer_connection_failed": "Connection failed",
|
||||
"peer_connection_new": "Connecting",
|
||||
"previous": "Previous",
|
||||
"register_device_error": "There was an error {error} registering your device.",
|
||||
"register_device_finish_button": "Finish Setup",
|
||||
"register_device_name_description": "Name your device so you can easily identify it later. You can change this name at any time.",
|
||||
"register_device_name_label": "Device Name",
|
||||
"register_device_name_placeholder": "Plex Media Server",
|
||||
"register_device_no_name": "Please specify a name",
|
||||
"rename_device_button": "Rename Device",
|
||||
"rename_device_description": "Properly name your device to easily identify it.",
|
||||
"rename_device_error": "There was an error {error} renaming your device.",
|
||||
"rename_device_headline": "Rename {name}",
|
||||
"rename_device_new_name_label": "New device name",
|
||||
"rename_device_new_name_placeholder": "Plex Media Server",
|
||||
"rename_device_no_name": "Please specify a name",
|
||||
"rename": "Rename",
|
||||
"retry": "Retry",
|
||||
"saving": "Saving…",
|
||||
"search_placeholder": "Search…",
|
||||
"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",
|
||||
"serial_console": "Serial Console",
|
||||
"setting_remote_description": "Setting remote description",
|
||||
"setting_remote_session_description": "Setting remote session description...",
|
||||
"setting_up_connection_to_device": "Setting up connection to device...",
|
||||
"something_went_wrong": "Something went wrong. Please try again later or contact support",
|
||||
"step_counter_step": "Step {step}",
|
||||
"subnet_mask": "Subnet Mask",
|
||||
"troubleshoot_connection": "Troubleshoot Connection",
|
||||
"unknown_error": "Unknown error",
|
||||
"update_in_progress": "Update in Progress",
|
||||
"updates_failed_check": "Failed to check for updates: {error}",
|
||||
"updates_failed_get_device_version": "Failed to get device version: {error}",
|
||||
"updating_leave_device_on": "Please don't turn off your device…",
|
||||
"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_device_classes_description": "USB device classes in the composite device",
|
||||
"usb_device_classes_title": "Classes",
|
||||
"usb_device_custom": "Custom",
|
||||
"usb_device_description": "USB devices to emulate on the target computer",
|
||||
"usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
|
||||
"usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
|
||||
"usb_device_enable_keyboard_description": "Enable Keyboard",
|
||||
"usb_device_enable_keyboard_title": "Enable Keyboard",
|
||||
"usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
|
||||
"usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
|
||||
"usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
|
||||
"usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
|
||||
"usb_device_failed_load": "Failed to load USB devices: {error}",
|
||||
"usb_device_failed_set": "Failed to set USB devices: {error}",
|
||||
"usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
|
||||
"usb_device_keyboard_only": "Keyboard Only",
|
||||
"usb_device_restore_default": "Restore to Default",
|
||||
"usb_device_title": "USB Device",
|
||||
"usb_device_update_classes": "Update USB Classes",
|
||||
"usb_device_updated": "USB Devices updated",
|
||||
"usb_state_connected": "Connected",
|
||||
"usb_state_connecting": "Connecting",
|
||||
"usb_state_disconnected": "Disconnected",
|
||||
"usb_state_low_power_mode": "Low power mode",
|
||||
"usb": "USB",
|
||||
"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_disabled": "Pointer lock disabled",
|
||||
"video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
|
||||
"view_details": "View Details",
|
||||
"virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer.",
|
||||
"virtual_keyboard_header": "Virtual Keyboard",
|
||||
"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",
|
||||
"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",
|
||||
"welcome_to_jetkvm_description": "Control any computer remotely",
|
||||
"welcome_to_jetkvm": "Welcome to JetKVM"
|
||||
}
|
||||
|
|
@ -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",
|
||||
"private": true,
|
||||
"version": "2025.10.01.1900",
|
||||
"version": "2025.10.13.2055",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0"
|
||||
|
|
@ -11,12 +11,14 @@
|
|||
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
||||
"dev:cloud": "vite dev --mode=cloud-development",
|
||||
"build": "npm run build:prod",
|
||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||
"build:staging": "tsc && vite build --mode=cloud-staging",
|
||||
"build:prod": "tsc && vite build --mode=cloud-production",
|
||||
"lint": "eslint './src/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix",
|
||||
"preview": "vite preview"
|
||||
"build:device": "npm run paraglide && tsc && vite build --mode=device --emptyOutDir",
|
||||
"build:staging": "npm run paraglide && tsc && vite build --mode=cloud-staging",
|
||||
"build:prod": "npm run paraglide && tsc && vite build --mode=cloud-production",
|
||||
"lint": "npm run paraglide && eslint './src/**/*.{ts,tsx}'",
|
||||
"lint:fix": "npm run paraglide && eslint './src/**/*.{ts,tsx}' --fix",
|
||||
"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": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
|
|
@ -33,16 +35,16 @@
|
|||
"dayjs": "^1.11.18",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"framer-motion": "^12.23.22",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^19.1.1",
|
||||
"react": "^19.2.0",
|
||||
"react-animate-height": "^3.2.3",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"react-simple-keyboard": "^3.8.125",
|
||||
"react-router": "^7.9.4",
|
||||
"react-simple-keyboard": "^3.8.130",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -54,32 +56,37 @@
|
|||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@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/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.1.17",
|
||||
"@types/react-dom": "^19.1.10",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/validator": "^13.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.1.9",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import { Fragment, useCallback, useRef } from "react";
|
||||
import { MdOutlineContentPasteGo } from "react-icons/md";
|
||||
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||
import { FaKeyboard } from "react-icons/fa6";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Fragment, useCallback, useRef } from "react";
|
||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
useSettingsStore,
|
||||
useUiStore,
|
||||
} from "@/hooks/stores";
|
||||
} from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { Button } from "@components/Button";
|
||||
import Container from "@components/Container";
|
||||
import { cx } from "@/cva.config";
|
||||
import PasteModal from "@/components/popovers/PasteModal";
|
||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||
import MountPopopover from "@/components/popovers/MountPopover";
|
||||
import ExtensionPopover from "@/components/popovers/ExtensionPopover";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import PasteModal from "@components/popovers/PasteModal";
|
||||
import WakeOnLanModal from "@components/popovers/WakeOnLan/Index";
|
||||
import MountPopopover from "@components/popovers/MountPopover";
|
||||
import ExtensionPopover from "@components/popovers/ExtensionPopover";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function Actionbar({
|
||||
requestFullscreen,
|
||||
|
|
@ -28,10 +29,7 @@ export default function Actionbar({
|
|||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
|
||||
|
||||
const remoteVirtualMediaState = useMountMediaStore(
|
||||
state => state.remoteVirtualMediaState,
|
||||
);
|
||||
const { remoteVirtualMediaState } = useMountMediaStore();
|
||||
const { developerMode } = useSettingsStore();
|
||||
|
||||
// This is the only way to get a reliable state change for the popover
|
||||
|
|
@ -64,7 +62,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Web Terminal"
|
||||
text={m.action_bar_web_terminal()}
|
||||
LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
|
||||
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
|
||||
/>
|
||||
|
|
@ -74,7 +72,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Paste text"
|
||||
text={m.action_bar_paste_text()}
|
||||
LeadingIcon={MdOutlineContentPasteGo}
|
||||
onClick={() => {
|
||||
setDisableVideoFocusTrap(true);
|
||||
|
|
@ -105,7 +103,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Virtual Media"
|
||||
text={m.action_bar_virtual_media()}
|
||||
LeadingIcon={({ className }) => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -148,7 +146,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Wake on LAN"
|
||||
text={m.action_bar_wake_on_lan()}
|
||||
onClick={() => {
|
||||
setDisableVideoFocusTrap(true);
|
||||
}}
|
||||
|
|
@ -198,7 +196,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Virtual Keyboard"
|
||||
text={m.action_bar_virtual_keyboard()}
|
||||
LeadingIcon={FaKeyboard}
|
||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||
/>
|
||||
|
|
@ -211,7 +209,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Extension"
|
||||
text={m.action_bar_extension()}
|
||||
LeadingIcon={LuCable}
|
||||
onClick={() => {
|
||||
setDisableVideoFocusTrap(true);
|
||||
|
|
@ -237,7 +235,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Virtual Keyboard"
|
||||
text={m.action_bar_virtual_keyboard()}
|
||||
LeadingIcon={FaKeyboard}
|
||||
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||
/>
|
||||
|
|
@ -246,7 +244,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Connection Stats"
|
||||
text={m.action_bar_connection_stats()}
|
||||
LeadingIcon={({ className }) => (
|
||||
<LuSignal
|
||||
className={cx(className, "mb-0.5 text-green-500")}
|
||||
|
|
@ -262,7 +260,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Settings"
|
||||
text={m.action_bar_settings()}
|
||||
LeadingIcon={LuSettings}
|
||||
onClick={() => {
|
||||
setDisableVideoFocusTrap(true);
|
||||
|
|
@ -276,7 +274,7 @@ export default function Actionbar({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Fullscreen"
|
||||
text={m.action_bar_fullscreen()}
|
||||
LeadingIcon={LuMaximize}
|
||||
onClick={() => requestFullscreen()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React, { JSX } from "react";
|
||||
import { Link, useNavigation } from "react-router";
|
||||
import type { FetcherWithComponents, LinkProps } from "react-router";
|
||||
import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
|
||||
|
||||
import ExtLink from "@/components/ExtLink";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { cva, cx } from "@/cva.config";
|
||||
import ExtLink from "@components/ExtLink";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
|
||||
const sizes = {
|
||||
XS: "h-[28px] px-2 text-xs",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { Ref } from "react";
|
|||
import React, { forwardRef, JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import { cva, cx } from "@/cva.config";
|
||||
|
||||
const sizes = {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import {
|
|||
ComboboxOptions,
|
||||
} from "@headlessui/react";
|
||||
|
||||
import { m } from "@localizations/messages.js";
|
||||
import Card from "@components/Card";
|
||||
import { cva } from "@/cva.config";
|
||||
|
||||
import Card from "./Card";
|
||||
|
||||
export interface ComboboxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
|
@ -44,11 +44,11 @@ export function Combobox({
|
|||
displayValue,
|
||||
options,
|
||||
disabled = false,
|
||||
placeholder = "Search...",
|
||||
emptyMessage = "No results found",
|
||||
placeholder = m.search_placeholder(),
|
||||
emptyMessage = m.no_results_found(),
|
||||
size = "MD",
|
||||
onChange,
|
||||
disabledMessage = "Input disabled",
|
||||
disabledMessage = m.input_disabled(),
|
||||
...otherProps
|
||||
}: ComboboxProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import {
|
|||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { Button } from "@components/Button";
|
||||
import Modal from "@components/Modal";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
type Variant = "danger" | "success" | "warning" | "info";
|
||||
|
|
@ -63,8 +64,8 @@ export function ConfirmDialog({
|
|||
title,
|
||||
description,
|
||||
variant = "info",
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
confirmText = m.confirm(),
|
||||
cancelText = m.cancel(),
|
||||
onConfirm,
|
||||
isConfirming = false,
|
||||
}: ConfirmDialogProps) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { LuRefreshCcw } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||
import { NetworkState } from "@/hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
|
||||
import { NetworkState } from "@hooks/stores";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function DhcpLeaseCard({
|
||||
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="space-y-3">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
DHCP Lease Information
|
||||
{m.dhcp_lease_header()}
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-x-6 gap-y-2">
|
||||
|
|
@ -25,7 +26,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.ip && (
|
||||
<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">
|
||||
IP Address
|
||||
{m.ip_address()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ip}
|
||||
|
|
@ -36,7 +37,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.netmask && (
|
||||
<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">
|
||||
Subnet Mask
|
||||
{m.subnet_mask()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.netmask}
|
||||
|
|
@ -47,7 +48,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.dns && (
|
||||
<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">
|
||||
DNS Servers
|
||||
{m.dns_servers()}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
||||
|
|
@ -58,7 +59,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.broadcast && (
|
||||
<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">
|
||||
Broadcast
|
||||
{m.dhcp_lease_broadcast()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.broadcast}
|
||||
|
|
@ -69,7 +70,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.domain && (
|
||||
<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">
|
||||
Domain
|
||||
{m.dhcp_lease_domain()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.domain}
|
||||
|
|
@ -81,7 +82,7 @@ export default function DhcpLeaseCard({
|
|||
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="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||
NTP Servers
|
||||
{m.ntp_servers()}
|
||||
</div>
|
||||
<div className="shrink text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||
|
|
@ -94,7 +95,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.hostname && (
|
||||
<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">
|
||||
Hostname
|
||||
{m.dhcp_lease_hostname()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.hostname}
|
||||
|
|
@ -108,7 +109,7 @@ export default function DhcpLeaseCard({
|
|||
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||
<div className="flex justify-between pt-2">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Gateway
|
||||
{m.dhcp_lease_gateway()}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.routers.map(router => (
|
||||
|
|
@ -121,7 +122,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.server_id && (
|
||||
<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">
|
||||
DHCP Server
|
||||
{m.dhcp_server()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.server_id}
|
||||
|
|
@ -132,7 +133,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.lease_expiry && (
|
||||
<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">
|
||||
Lease Expires
|
||||
{m.dhcp_lease_lease_expires()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
<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">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.mtu}
|
||||
{m.dhcp_lease_maximum_transfer_unit()}
|
||||
</span>
|
||||
</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">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ttl}
|
||||
{m.dhcp_lease_time_to_live()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -163,7 +164,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||
<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">
|
||||
Boot Next Server
|
||||
{m.dhcp_lease_boot_next_server()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_next_server}
|
||||
|
|
@ -174,7 +175,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||
<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">
|
||||
Boot Server Name
|
||||
{m.dhcp_lease_boot_server_name()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_server_name}
|
||||
|
|
@ -185,7 +186,7 @@ export default function DhcpLeaseCard({
|
|||
{networkState?.dhcp_lease?.bootp_file && (
|
||||
<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">
|
||||
Boot File
|
||||
{m.dhcp_lease_boot_file()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_file}
|
||||
|
|
@ -194,13 +195,12 @@ export default function DhcpLeaseCard({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
className="text-red-500"
|
||||
text="Renew DHCP Lease"
|
||||
text={m.dhcp_lease_renew()}
|
||||
LeadingIcon={LuRefreshCcw}
|
||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
import { GridCard } from "@/components/Card";
|
||||
|
||||
import { cx } from "../cva.config";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
interface Props {
|
||||
IconElm?: React.FC<{ className: string | undefined }>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||
import { useFeatureFlag } from "@hooks/useFeatureFlag";
|
||||
|
||||
export function FeatureFlag({
|
||||
minAppVersion,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { useNavigation } from "react-router";
|
||||
import type { FetcherWithComponents } from "react-router";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function Fieldset({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -4,19 +4,18 @@ import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/1
|
|||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||
|
||||
import Container from "@/components/Container";
|
||||
import Card from "@/components/Card";
|
||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import USBStateStatus from "@components/USBStateStatus";
|
||||
import LogoBlueIcon from "@assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
|
||||
import Card from "@components/Card";
|
||||
import Container from "@components/Container";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||
import USBStateStatus from "@components/USBStateStatus";
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import { isOnDevice } from "../main";
|
||||
|
||||
import { LinkButton } from "./Button";
|
||||
import api from "@/api";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface NavbarProps {
|
||||
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="p-2">
|
||||
<div className="font-display text-xs">
|
||||
Logged in as
|
||||
{m.logged_in_as()}
|
||||
</div>
|
||||
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
|
||||
{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">
|
||||
<ArrowLeftEndOnRectangleIcon className="size-4" />
|
||||
<div className="font-display">Log out</div>
|
||||
<div className="font-display">{m.log_out()}</div>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
useHidStore,
|
||||
useMouseStore,
|
||||
|
|
@ -8,9 +7,11 @@ import {
|
|||
useSettingsStore,
|
||||
useVideoStore,
|
||||
VideoState
|
||||
} from "@/hooks/stores";
|
||||
} from "@hooks/stores";
|
||||
import { useHidRpc } from "@hooks/useHidRpc";
|
||||
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() {
|
||||
const { keysDownState } = useHidStore();
|
||||
|
|
@ -25,29 +26,23 @@ export default function InfoBar() {
|
|||
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
|
||||
);
|
||||
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
|
||||
const { isPasteInProgress } = useHidStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcDataChannel) return;
|
||||
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
|
||||
rpcDataChannel.onerror = (e: Event) =>
|
||||
console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||
}, [rpcDataChannel]);
|
||||
|
||||
const { keyboardLedState, usbState } = useHidStore();
|
||||
const { isTurnServerInUse } = useRTCStore();
|
||||
const { hdmiState } = useVideoStore();
|
||||
|
||||
const displayKeys = useMemo(() => {
|
||||
if (!showPressedKeys)
|
||||
return "";
|
||||
if (!showPressedKeys) return "";
|
||||
|
||||
const activeModifierMask = keysDownState.modifier || 0;
|
||||
const keysDown = keysDownState.keys || [];
|
||||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||
const modifierNames = Object.entries(modifiers)
|
||||
.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(", ");
|
||||
}, [keysDownState, showPressedKeys]);
|
||||
|
|
@ -59,76 +54,75 @@ export default function InfoBar() {
|
|||
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
||||
{debugMode ? (
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{debugMode ? (
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(debugMode && mouseMode == "absolute") ? (
|
||||
<div className="flex w-[118px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Pointer:</span>
|
||||
<span className="text-xs">
|
||||
{mouseX},{mouseY}
|
||||
</span>
|
||||
<span className="text-xs font-semibold">{m.info_pointer()}</span>
|
||||
<span className="text-xs">{mouseX},{mouseY}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(debugMode && mouseMode == "relative") ? (
|
||||
<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">
|
||||
{mouseMove ?
|
||||
`${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` :
|
||||
"N/A"}
|
||||
{mouseMove ? `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{debugMode && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{debugMode && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{debugMode && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPasteInProgress && (
|
||||
<div className="flex w-[156px] items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Paste Mode:</span>
|
||||
<span className="text-xs">Enabled</span>
|
||||
<span className="text-xs font-semibold">{m.info_paste_mode()}</span>
|
||||
<span className="text-xs">{m.info_paste_enabled()}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPressedKeys && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Keys:</span>
|
||||
<h2 className="text-xs">
|
||||
{displayKeys}
|
||||
</h2>
|
||||
<span className="text-xs font-semibold">{m.info_keys()}</span>
|
||||
<h2 className="text-xs">{displayKeys}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
||||
{isTurnServerInUse && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
|
@ -140,8 +134,9 @@ export default function InfoBar() {
|
|||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
>
|
||||
Caps Lock
|
||||
{m.info_caps_lock()}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
|
|
@ -150,8 +145,9 @@ export default function InfoBar() {
|
|||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
>
|
||||
Num Lock
|
||||
{m.info_num_lock()}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
|
|
@ -160,22 +156,19 @@ export default function InfoBar() {
|
|||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
>
|
||||
Scroll Lock
|
||||
{m.info_scroll_lock()}
|
||||
</div>
|
||||
|
||||
{keyboardLedState.compose ? (
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||
Compose
|
||||
</div>
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_compose()}</div>
|
||||
) : null}
|
||||
|
||||
{keyboardLedState.kana ? (
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||
Kana
|
||||
</div>
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
|
||||
) : null}
|
||||
|
||||
{keyboardLedState.shift ? (
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||
Shift
|
||||
</div>
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_shift()}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NetworkState } from "../hooks/stores";
|
||||
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
||||
|
||||
import { GridCard } from "./Card";
|
||||
import { NetworkState } from "@hooks/stores";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function Ipv6NetworkCard({
|
||||
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="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Information
|
||||
{m.ipv6_information()}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||
{networkState?.ipv6_link_local && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Link-local
|
||||
{m.ipv6_link_local()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{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="col-span-2 flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Address
|
||||
{m.ipv6_address_label()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{addr.address}</span>
|
||||
</div>
|
||||
|
|
@ -50,12 +50,12 @@ export default function Ipv6NetworkCard({
|
|||
{addr.valid_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Valid Lifetime
|
||||
{m.ipv6_valid_lifetime()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.valid_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
{m.not_available()}
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||
|
|
@ -66,12 +66,12 @@ export default function Ipv6NetworkCard({
|
|||
{addr.preferred_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Preferred Lifetime
|
||||
{m.ipv6_preferred_lifetime()}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.preferred_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
{m.not_available()}
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
import { InputFieldWithLabel } from "./InputField";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export interface JigglerConfig {
|
||||
inactivity_limit_seconds: number;
|
||||
|
|
@ -51,7 +51,7 @@ export function JigglerSetting({
|
|||
|
||||
const exampleConfigs = [
|
||||
{
|
||||
name: "Business Hours 9-17",
|
||||
name: m.jiggler_example_business_hours_late(),
|
||||
config: {
|
||||
inactivity_limit_seconds: 60,
|
||||
jitter_percentage: 25,
|
||||
|
|
@ -60,7 +60,7 @@ export function JigglerSetting({
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Business Hours 8-17",
|
||||
name: m.jiggler_example_business_hours_early(),
|
||||
config: {
|
||||
inactivity_limit_seconds: 60,
|
||||
jitter_percentage: 25,
|
||||
|
|
@ -69,13 +69,10 @@ export function JigglerSetting({
|
|||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Examples
|
||||
</h4>
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{m.jiggler_examples_label()}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exampleConfigs.map((example, index) => (
|
||||
<Button
|
||||
|
|
@ -90,7 +87,7 @@ export function JigglerSetting({
|
|||
to="https://crontab.guru/examples.html"
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="More examples"
|
||||
text={m.jiggler_more_examples()}
|
||||
LeadingIcon={LuExternalLink}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -100,8 +97,8 @@ export function JigglerSetting({
|
|||
<InputFieldWithLabel
|
||||
required
|
||||
size="SM"
|
||||
label="Cron Schedule"
|
||||
description="Cron expression for scheduling"
|
||||
label={m.jiggler_cron_schedule_label()}
|
||||
description={m.jiggler_cron_schedule_description()}
|
||||
placeholder="*/20 * * * * *"
|
||||
value={jigglerConfigState.schedule_cron_tab}
|
||||
onChange={e =>
|
||||
|
|
@ -114,8 +111,8 @@ export function JigglerSetting({
|
|||
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Inactivity Limit Seconds"
|
||||
description="Inactivity time before jiggle"
|
||||
label={m.jiggler_inactivity_limit_label()}
|
||||
description={m.jiggler_inactivity_limit_description()}
|
||||
value={jigglerConfigState.inactivity_limit_seconds}
|
||||
type="number"
|
||||
min="1"
|
||||
|
|
@ -131,8 +128,8 @@ export function JigglerSetting({
|
|||
<InputFieldWithLabel
|
||||
required
|
||||
size="SM"
|
||||
label="Random delay"
|
||||
description="To avoid recognizable patterns"
|
||||
label={m.jiggler_random_delay_label()}
|
||||
description={m.jiggler_random_delay_description()}
|
||||
placeholder="25"
|
||||
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
|
||||
value={jigglerConfigState.jitter_percentage}
|
||||
|
|
@ -149,8 +146,8 @@ export function JigglerSetting({
|
|||
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label="Timezone"
|
||||
description="Timezone for cron schedule"
|
||||
label={m.jiggler_timezone_label()}
|
||||
description={m.jiggler_timezone_description()}
|
||||
value={jigglerConfigState.timezone || "UTC"}
|
||||
disabled={timezones.length === 0}
|
||||
onChange={e =>
|
||||
|
|
@ -167,7 +164,7 @@ export function JigglerSetting({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Save Jiggler Config"
|
||||
text={m.jiggler_save_jiggler_config()}
|
||||
onClick={() => onSave(jigglerConfigState)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Link } from "react-router";
|
||||
import { MdConnectWithoutContact } from "react-icons/md";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { Link } from "react-router";
|
||||
import { LuEllipsisVertical } from "react-icons/lu";
|
||||
|
||||
import Card from "@components/Card";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
|
||||
// Allow dates or times to be passed
|
||||
|
|
@ -62,16 +63,16 @@ export default function KvmCard({
|
|||
{online ? (
|
||||
<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="text-sm text-black dark:text-white">Online</div>
|
||||
<div className="text-sm text-black dark:text-white">{m.online()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<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="text-sm text-black dark:text-white">
|
||||
{lastSeen ? (
|
||||
<>Last online {getRelativeTimeString(lastSeen)}</>
|
||||
<>{m.last_online({ time: getRelativeTimeString(lastSeen) })}</>
|
||||
) : (
|
||||
<>Never seen online</>
|
||||
<>{m.never_seen_online()}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,7 +86,7 @@ export default function KvmCard({
|
|||
<LinkButton
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Connect to KVM"
|
||||
text={m.connect_to_kvm()}
|
||||
LeadingIcon={MdConnectWithoutContact}
|
||||
textAlign="center"
|
||||
to={`/devices/${id}`}
|
||||
|
|
@ -94,7 +95,7 @@ export default function KvmCard({
|
|||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Troubleshoot Connection"
|
||||
text={m.troubleshoot_connection()}
|
||||
textAlign="center"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
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 Container from "@components/Container";
|
||||
import { useMacrosStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
export default function MacroBar() {
|
||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { useState } from "react";
|
||||
import { LuPlus } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
||||
import { MacroStepCard } from "@/components/MacroStepCard";
|
||||
import { KeySequence } from "@hooks/stores";
|
||||
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||
import { Button } from "@components/Button";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel, FieldError } from "@components/InputField";
|
||||
import { MacroStepCard } from "@components/MacroStepCard";
|
||||
import {
|
||||
DEFAULT_DELAY,
|
||||
MAX_STEPS_PER_MACRO,
|
||||
MAX_KEYS_PER_STEP,
|
||||
} from "@/constants/macros";
|
||||
import { KeySequence } from "@/hooks/stores";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface ValidationErrors {
|
||||
name?: string;
|
||||
|
|
@ -31,7 +32,6 @@ interface MacroFormProps {
|
|||
onSubmit: (macro: Partial<KeySequence>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
isSubmitting?: boolean;
|
||||
submitText?: string;
|
||||
}
|
||||
|
||||
export function MacroForm({
|
||||
|
|
@ -39,7 +39,6 @@ export function MacroForm({
|
|||
onSubmit,
|
||||
onCancel,
|
||||
isSubmitting = false,
|
||||
submitText = "Save Macro",
|
||||
}: MacroFormProps) {
|
||||
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
|
||||
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
||||
|
|
@ -57,13 +56,13 @@ export function MacroForm({
|
|||
|
||||
// Name validation
|
||||
if (!macro.name?.trim()) {
|
||||
newErrors.name = "Name is required";
|
||||
newErrors.name = m.macro_name_required();
|
||||
} 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) {
|
||||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
||||
newErrors.steps = { 0: { keys: m.macro_at_least_one_step_required() } };
|
||||
} else {
|
||||
const hasKeyOrModifier = macro.steps.some(
|
||||
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||
|
|
@ -71,7 +70,7 @@ export function MacroForm({
|
|||
|
||||
if (!hasKeyOrModifier) {
|
||||
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 () => {
|
||||
if (!validateForm()) {
|
||||
showTemporaryError("Please fix the validation errors");
|
||||
showTemporaryError(m.macro_please_fix_validation_errors());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +91,7 @@ export function MacroForm({
|
|||
if (error instanceof Error) {
|
||||
showTemporaryError(error.message);
|
||||
} else {
|
||||
showTemporaryError("An error occurred while saving");
|
||||
showTemporaryError(m.macro_save_error());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -114,7 +113,7 @@ export function MacroForm({
|
|||
? newSteps[stepIndex].keys
|
||||
: [];
|
||||
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;
|
||||
}
|
||||
newSteps[stepIndex].keys = [...keysArray, option.value];
|
||||
|
|
@ -178,8 +177,8 @@ export function MacroForm({
|
|||
<Fieldset>
|
||||
<InputFieldWithLabel
|
||||
type="text"
|
||||
label="Macro Name"
|
||||
placeholder="Macro Name"
|
||||
label={m.macro_name_label()}
|
||||
placeholder={m.macro_name_label()}
|
||||
value={macro.name}
|
||||
error={errors.name}
|
||||
onChange={e => {
|
||||
|
|
@ -197,12 +196,12 @@ export function MacroForm({
|
|||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<FieldLabel
|
||||
label="Steps"
|
||||
description={`Keys/modifiers executed in sequence with a delay between each step.`}
|
||||
label={m.macro_steps_label()}
|
||||
description={m.macro_steps_description()}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
{errors.steps && errors.steps[0]?.keys && (
|
||||
|
|
@ -220,10 +219,10 @@ export function MacroForm({
|
|||
onDelete={
|
||||
macro.steps && macro.steps.length > 1
|
||||
? () => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(stepIndex, 1);
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
}
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(stepIndex, 1);
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onMoveUp={() => handleStepMove(stepIndex, "up")}
|
||||
|
|
@ -248,12 +247,10 @@ export function MacroForm({
|
|||
theme="light"
|
||||
fullWidth
|
||||
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={() => {
|
||||
if (isMaxStepsReached) {
|
||||
showTemporaryError(
|
||||
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
||||
);
|
||||
showTemporaryError(m.macro_max_steps_error({max: MAX_STEPS_PER_MACRO}));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -280,11 +277,11 @@ export function MacroForm({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={isSubmitting ? "Saving..." : submitText}
|
||||
text={isSubmitting ? m.saving() : m.macro_save()}
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useMemo } from "react";
|
||||
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Combobox } from "@/components/Combobox";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import Card from "@/components/Card";
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import { Button } from "@components/Button";
|
||||
import { Combobox } from "@components/Combobox";
|
||||
import Card from "@components/Card";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
|
||||
import { KeyboardLayout } from "@/keyboardLayouts";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
// Filter out modifier keys since they're handled in the modifiers section
|
||||
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
||||
|
|
@ -25,6 +26,7 @@ const groupedModifiers: Record<string, typeof modifierOptions> = {
|
|||
Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')),
|
||||
};
|
||||
|
||||
// not going to localize these since they're short time intervals
|
||||
const basePresetDelays = [
|
||||
{ value: "50", label: "50ms" },
|
||||
{ value: "100", label: "100ms" },
|
||||
|
|
@ -132,12 +134,12 @@ export function MacroStepCard({
|
|||
LeadingIcon={LuArrowDown}
|
||||
/>
|
||||
</div>
|
||||
{onDelete && (
|
||||
{onDelete && (
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
text="Delete"
|
||||
text={m.delete()}
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
|
|
@ -147,7 +149,7 @@ export function MacroStepCard({
|
|||
|
||||
<div className="space-y-4 mt-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">
|
||||
{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">
|
||||
|
|
@ -179,7 +181,7 @@ export function MacroStepCard({
|
|||
|
||||
<div className="w-full flex flex-col 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>
|
||||
{ensureArray(step.keys) && step.keys.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 pb-2">
|
||||
|
|
@ -214,19 +216,19 @@ export function MacroStepCard({
|
|||
displayValue={() => keyQuery}
|
||||
onInputChange={onKeyQueryChange}
|
||||
options={() => filteredKeys}
|
||||
disabledMessage="Max keys reached"
|
||||
disabledMessage={m.macro_step_max_keys_reached({max: MAX_KEYS_PER_STEP})}
|
||||
size="SM"
|
||||
immediate
|
||||
disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP}
|
||||
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."}
|
||||
emptyMessage="No matching keys found"
|
||||
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? m.macro_step_max_keys_reached() : m.macro_step_search_for_key()}
|
||||
emptyMessage={m.macro_step_no_matching_keys_found()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col 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 className="flex items-center gap-3">
|
||||
<SelectMenuBasic
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import { ComponentProps } from "react";
|
||||
import { cva, cx } from "cva";
|
||||
|
||||
import { someIterable } from "../utils";
|
||||
|
||||
import { GridCard } from "./Card";
|
||||
import MetricsChart from "./MetricsChart";
|
||||
import { GridCard } from "@components/Card";
|
||||
import MetricsChart from "@components/MetricsChart";
|
||||
import { someIterable } from "@/utils";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface ChartPoint {
|
||||
date: number;
|
||||
|
|
@ -159,7 +159,7 @@ export function Metric<T, K extends keyof T>({
|
|||
>
|
||||
{!ready ? (
|
||||
<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>
|
||||
) : supportedFinal ? (
|
||||
<MetricsChart
|
||||
|
|
@ -170,7 +170,7 @@ export function Metric<T, K extends keyof T>({
|
|||
/>
|
||||
) : (
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export default function MetricsChart({
|
|||
axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
||||
tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
|
||||
tickFormatter={date => {
|
||||
// TODO use locale from user settings for date formatting
|
||||
return new Date(date * 1000).toLocaleString("en-US", {
|
||||
hourCycle: "h23",
|
||||
hour: "numeric",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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() {
|
||||
return (
|
||||
|
|
@ -9,8 +10,8 @@ export default function NotFoundPage() {
|
|||
<div className="w-full max-w-2xl">
|
||||
<EmptyCard
|
||||
IconElm={ExclamationTriangleIcon}
|
||||
headline="Not found"
|
||||
description="The page you were looking for does not exist."
|
||||
headline={m.not_found()}
|
||||
description={m.page_not_found_description()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import StatusCard from "@components/StatusCards";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
const PeerConnectionStatusMap = {
|
||||
connected: "Connected",
|
||||
connecting: "Connecting",
|
||||
disconnected: "Disconnected",
|
||||
error: "Connection error",
|
||||
closing: "Closing",
|
||||
failed: "Connection failed",
|
||||
closed: "Closed",
|
||||
new: "Connecting",
|
||||
connected: m.peer_connection_connected(),
|
||||
connecting: m.peer_connection_connecting(),
|
||||
disconnected: m.peer_connection_disconnected(),
|
||||
error: m.peer_connection_error(),
|
||||
closing: m.peer_connection_closing(),
|
||||
failed: m.peer_connection_failed(),
|
||||
closed: m.peer_connection_closed(),
|
||||
new: m.peer_connection_new(),
|
||||
} as Record<RTCPeerConnectionState | "error" | "closing", string>;
|
||||
|
||||
export type PeerConnections = keyof typeof PeerConnectionStatusMap;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React, { JSX } from "react";
|
||||
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 Card from "./Card";
|
||||
|
||||
|
||||
type SelectMenuProps = Pick<
|
||||
JSX.IntrinsicElements["select"],
|
||||
"disabled" | "onChange" | "name" | "value"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AvailableSidebarViews } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import { cx } from "@/cva.config";
|
||||
import { AvailableSidebarViews } from "@/hooks/stores";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SidebarHeader({
|
||||
title,
|
||||
|
|
@ -17,7 +18,7 @@ export default function SidebarHeader({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="blank"
|
||||
text="Hide"
|
||||
text={m.hide()}
|
||||
LeadingIcon={({ className }) => (
|
||||
<svg
|
||||
className={cx(className, "rotate-180")}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Link } from "react-router";
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import Container from "@/components/Container";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import Container from "@components/Container";
|
||||
|
||||
interface Props { logoHref?: string; actionElement?: React.ReactNode }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
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 Card from "@/components/Card";
|
||||
|
||||
interface Props {
|
||||
nSteps: number;
|
||||
|
|
@ -49,7 +50,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
|
|||
)}
|
||||
key={`${i}-${currStepIdx}`}
|
||||
>
|
||||
Step {i + 1}
|
||||
{m.step_counter_step({ step: i + 1 })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import "react-simple-keyboard/build/css/index.css";
|
||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useXTerm } from "react-xtermjs";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
|
|
@ -9,9 +9,9 @@ import { Unicode11Addon } from "@xterm/addon-unicode11";
|
|||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
||||
|
||||
import { Button } from "./Button";
|
||||
import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ const TERMINAL_CONFIG = {
|
|||
// Add these configurations:
|
||||
cursorStyle: "block",
|
||||
rendererType: "canvas", // Ensure we're using the canvas renderer
|
||||
unicode: { activeVersion: "11" }
|
||||
} as const;
|
||||
|
||||
function Terminal({
|
||||
|
|
@ -144,7 +145,6 @@ function Terminal({
|
|||
instance.loadAddon(new ClipboardAddon());
|
||||
instance.loadAddon(new Unicode11Addon());
|
||||
instance.loadAddon(new WebLinksAddon());
|
||||
instance.unicode.activeVersion = "11";
|
||||
|
||||
if (isWebGl2Supported) {
|
||||
const webGl2Addon = new WebglAddon();
|
||||
|
|
@ -191,7 +191,7 @@ function Terminal({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Hide"
|
||||
text={m.hide()}
|
||||
LeadingIcon={ChevronDownIcon}
|
||||
onClick={() => setTerminalType("none")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import { FieldError } from "@/components/InputField";
|
||||
import Card from "@/components/Card";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import { FieldError } from "@components/InputField";
|
||||
import Card from "@components/Card";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
type TextAreaProps = JSX.IntrinsicElements["textarea"] & {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
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 StatusCard from "@components/StatusCards";
|
||||
import { USBStates } from "@/hooks/stores";
|
||||
|
||||
type StatusProps = Record<
|
||||
USBStates,
|
||||
|
|
@ -16,11 +17,11 @@ type StatusProps = Record<
|
|||
>;
|
||||
|
||||
const USBStateMap: Record<USBStates, string> = {
|
||||
configured: "Connected",
|
||||
attached: "Connecting",
|
||||
addressed: "Connecting",
|
||||
"not attached": "Disconnected",
|
||||
suspended: "Low power mode",
|
||||
configured: m.usb_state_connected(),
|
||||
attached: m.usb_state_connecting(),
|
||||
addressed: m.usb_state_connecting(),
|
||||
"not attached": m.usb_state_disconnected(),
|
||||
suspended: m.usb_state_low_power_mode(),
|
||||
};
|
||||
const StatusCardProps: StatusProps = {
|
||||
configured: {
|
||||
|
|
@ -80,8 +81,8 @@ export default function USBStateStatus({
|
|||
|
||||
return (
|
||||
<StatusCard
|
||||
title="USB"
|
||||
status="Disconnected"
|
||||
title={m.usb()}
|
||||
status={m.usb_state_disconnected()}
|
||||
icon={Icon}
|
||||
iconClassName={iconClassName}
|
||||
statusIndicatorClassName={statusIndicatorClassName}
|
||||
|
|
@ -90,6 +91,6 @@ export default function USBStateStatus({
|
|||
}
|
||||
|
||||
return (
|
||||
<StatusCard title="USB" status={USBStateMap[state]} {...StatusCardProps[state]} />
|
||||
<StatusCard title={m.usb()} status={USBStateMap[state]} {...StatusCardProps[state]} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { cx } from "@/cva.config";
|
||||
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
import { Button } from "./Button";
|
||||
import { GridCard } from "./Card";
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function UpdateInProgressStatusCard() {
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
|
@ -17,12 +16,12 @@ export default function UpdateInProgressStatusCard() {
|
|||
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
|
||||
<div className="space-y-1">
|
||||
<div className="text-ellipsis text-sm font-semibold leading-none transition">
|
||||
Update in Progress
|
||||
{m.update_in_progress()}
|
||||
</div>
|
||||
<div className="text-sm leading-none">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className={cx("transition")}>
|
||||
Please don{"'"}t turn off your device...
|
||||
{m.updating_leave_device_on()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,7 +31,7 @@ export default function UpdateInProgressStatusCard() {
|
|||
size="SM"
|
||||
className="pointer-events-auto"
|
||||
theme="light"
|
||||
text="View Details"
|
||||
text={m.view_details()}
|
||||
onClick={() => navigateTo("/settings/general/update")}
|
||||
/>
|
||||
</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 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 {
|
||||
vendor_id: string;
|
||||
product_id: string;
|
||||
|
|
@ -34,7 +34,7 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
|||
|
||||
const usbPresets = [
|
||||
{
|
||||
label: "Keyboard, Mouse and Mass Storage",
|
||||
label: m.usb_device_keyboard_mouse_and_mass_storage(),
|
||||
value: "default",
|
||||
config: {
|
||||
keyboard: true,
|
||||
|
|
@ -44,7 +44,7 @@ const usbPresets = [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: "Keyboard Only",
|
||||
label: m.usb_device_keyboard_only(),
|
||||
value: "keyboard_only",
|
||||
config: {
|
||||
keyboard: true,
|
||||
|
|
@ -54,7 +54,7 @@ const usbPresets = [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: "Custom",
|
||||
label: m.usb_device_custom(),
|
||||
value: "custom",
|
||||
},
|
||||
];
|
||||
|
|
@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
|
|||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.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 {
|
||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||
|
|
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
|
|||
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
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);
|
||||
return;
|
||||
|
|
@ -111,7 +111,7 @@ export function UsbDeviceSetting() {
|
|||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncUsbDeviceConfig();
|
||||
notifications.success(`USB Devices updated`);
|
||||
notifications.success(m.usb_device_updated());
|
||||
});
|
||||
},
|
||||
[send, syncUsbDeviceConfig],
|
||||
|
|
@ -154,14 +154,14 @@ export function UsbDeviceSetting() {
|
|||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
|
||||
<SettingsSectionHeader
|
||||
title="USB Device"
|
||||
description="USB devices to emulate on the target computer"
|
||||
title={m.usb_device_title()}
|
||||
description={m.usb_device_description()}
|
||||
/>
|
||||
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="Classes"
|
||||
description="USB device classes in the composite device"
|
||||
title={m.usb_device_classes_title()}
|
||||
description={m.usb_device_classes_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
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="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
|
||||
checked={usbDeviceConfig.keyboard}
|
||||
onChange={onUsbConfigItemChange("keyboard")}
|
||||
|
|
@ -187,8 +187,8 @@ export function UsbDeviceSetting() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Enable Absolute Mouse (Pointer)"
|
||||
description="Enable Absolute Mouse (Pointer)"
|
||||
title={m.usb_device_enable_absolute_mouse_title()}
|
||||
description={m.usb_device_enable_absolute_mouse_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={usbDeviceConfig.absolute_mouse}
|
||||
|
|
@ -198,8 +198,8 @@ export function UsbDeviceSetting() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Enable Relative Mouse"
|
||||
description="Enable Relative Mouse"
|
||||
title={m.usb_device_enable_relative_mouse_title()}
|
||||
description={m.usb_device_enable_relative_mouse_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={usbDeviceConfig.relative_mouse}
|
||||
|
|
@ -209,8 +209,8 @@ export function UsbDeviceSetting() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Enable USB Mass Storage"
|
||||
description="Sometimes it might need to be disabled to prevent issues with certain devices"
|
||||
title={m.usb_device_enable_mass_storage_title()}
|
||||
description={m.usb_device_enable_mass_storage_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={usbDeviceConfig.mass_storage}
|
||||
|
|
@ -224,13 +224,13 @@ export function UsbDeviceSetting() {
|
|||
size="SM"
|
||||
loading={loading}
|
||||
theme="primary"
|
||||
text="Update USB Classes"
|
||||
text={m.usb_device_update_classes()}
|
||||
onClick={() => handleUsbConfigChange(usbDeviceConfig)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Restore to Default"
|
||||
text={m.usb_device_restore_default()}
|
||||
onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { useMemo , useCallback , useEffect, useState } from "react";
|
||||
|
||||
import { UsbConfigState } from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
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 { UsbConfigState } from "../hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
|
||||
import { InputFieldWithLabel } from "./InputField";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
import Fieldset from "./Fieldset";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
|
||||
|
||||
|
|
@ -31,21 +30,22 @@ export interface USBConfig {
|
|||
product: string;
|
||||
}
|
||||
|
||||
|
||||
const usbConfigs = [
|
||||
{
|
||||
label: "JetKVM Default",
|
||||
label: m.usb_config_default(),
|
||||
value: "USB Emulation Device",
|
||||
},
|
||||
{
|
||||
label: "Logitech Universal Adapter",
|
||||
label: m.usb_config_logitech(),
|
||||
value: "Logitech USB Input Device",
|
||||
},
|
||||
{
|
||||
label: "Microsoft Wireless MultiMedia Keyboard",
|
||||
label: m.usb_config_microsoft(),
|
||||
value: "Wireless MultiMedia Keyboard",
|
||||
},
|
||||
{
|
||||
label: "Dell Multimedia Pro Keyboard",
|
||||
label: m.usb_config_dell(),
|
||||
value: "Multimedia Pro Keyboard",
|
||||
},
|
||||
];
|
||||
|
|
@ -94,10 +94,10 @@ export function UsbInfoSetting() {
|
|||
|
||||
const syncUsbConfigProduct = useCallback(() => {
|
||||
send("getUsbConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB Config:", resp.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 {
|
||||
const usbConfigState = resp.result as UsbConfigState;
|
||||
|
|
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
|
|||
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
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);
|
||||
return;
|
||||
|
|
@ -126,7 +126,7 @@ export function UsbInfoSetting() {
|
|||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
notifications.success(
|
||||
`USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`,
|
||||
m.usb_config_set_success({ manufacturer: usbConfig.manufacturer, product: usbConfig.product }),
|
||||
);
|
||||
|
||||
syncUsbConfigProduct();
|
||||
|
|
@ -152,8 +152,8 @@ export function UsbInfoSetting() {
|
|||
<Fieldset disabled={loading} className="space-y-4">
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="Identifiers"
|
||||
description="USB device identifiers exposed to the target computer"
|
||||
title={m.usb_config_identifiers_title()}
|
||||
description={m.usb_config_identifiers_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -169,7 +169,7 @@ export function UsbInfoSetting() {
|
|||
handleUsbConfigChange(usbConfig);
|
||||
}
|
||||
}}
|
||||
options={[...usbConfigs, { value: "custom", label: "Custom" }]}
|
||||
options={[...usbConfigs, { value: "custom", label: m.usb_config_custom() }]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{usbConfigProduct === "custom" && (
|
||||
|
|
@ -246,38 +246,38 @@ function USBConfigDialog({
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Vendor ID"
|
||||
placeholder="Enter Vendor ID"
|
||||
label={m.usb_config_vendor_id_label()}
|
||||
placeholder={m.usb_config_vendor_id_placeholder()}
|
||||
pattern="^0[xX][\da-fA-F]{4}$"
|
||||
defaultValue={usbConfigState?.vendor_id}
|
||||
onChange={e => handleUsbVendorIdChange(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Product ID"
|
||||
placeholder="Enter Product ID"
|
||||
label={m.usb_config_product_id_label()}
|
||||
placeholder={m.usb_config_product_id_placeholder()}
|
||||
pattern="^0[xX][\da-fA-F]{4}$"
|
||||
defaultValue={usbConfigState?.product_id}
|
||||
onChange={e => handleUsbProductIdChange(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Serial Number"
|
||||
placeholder="Enter Serial Number"
|
||||
label={m.usb_config_serial_number_label()}
|
||||
placeholder={m.usb_config_serial_number_placeholder()}
|
||||
defaultValue={usbConfigState?.serial_number}
|
||||
onChange={e => handleUsbSerialChange(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Manufacturer"
|
||||
placeholder="Enter Manufacturer"
|
||||
label={m.usb_config_manufacturer_label()}
|
||||
placeholder={m.usb_config_manufacturer_placeholder()}
|
||||
defaultValue={usbConfigState?.manufacturer}
|
||||
onChange={e => handleUsbManufacturer(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Product Name"
|
||||
placeholder="Enter Product Name"
|
||||
label={m.usb_config_product_name_label()}
|
||||
placeholder={m.usb_config_product_name_placeholder()}
|
||||
defaultValue={usbConfigState?.product}
|
||||
onChange={e => handleUsbProduct(e.target.value)}
|
||||
/>
|
||||
|
|
@ -287,13 +287,13 @@ function USBConfigDialog({
|
|||
loading={loading}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update USB Identifiers"
|
||||
text={m.usb_config_update_identifiers()}
|
||||
onClick={() => onSetUsbConfig(usbConfigState)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Restore to Default"
|
||||
text={m.usb_config_restore_default()}
|
||||
onClick={onRestoreToDefault}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||
import { LuPlay } from "react-icons/lu";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
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" />
|
||||
</div>
|
||||
<p className="text-center text-sm text-slate-700 dark:text-slate-300">
|
||||
Loading video stream...
|
||||
{m.video_overlay_loading_stream()}
|
||||
</p>
|
||||
</div>
|
||||
</OverlayContent>
|
||||
|
|
@ -118,26 +119,26 @@ export function ConnectionFailedOverlay({
|
|||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<li>Verify that the device is powered on and properly connected</li>
|
||||
<li>Check all cable connections for any loose or damaged wires</li>
|
||||
<li>Ensure your network connection is stable and active</li>
|
||||
<li>Try restarting both the device and your computer</li>
|
||||
<li>{m.video_overlay_conn_verify_power()}</li>
|
||||
<li>{m.video_overlay_conn_check_cables()}</li>
|
||||
<li>{m.video_overlay_conn_ensure_network()}</li>
|
||||
<li>{m.video_overlay_conn_restart()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<LinkButton
|
||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||
theme="primary"
|
||||
text="Troubleshooting Guide"
|
||||
text={m.video_overlay_troubleshooting_guide()}
|
||||
TrailingIcon={ArrowRightIcon}
|
||||
size="SM"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setupPeerConnection()}
|
||||
LeadingIcon={ArrowPathIcon}
|
||||
text="Try again"
|
||||
text={m.video_overlay_try_again()}
|
||||
size="SM"
|
||||
theme="light"
|
||||
/>
|
||||
|
|
@ -178,12 +179,12 @@ export function PeerConnectionDisconnectedOverlay({
|
|||
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<li>Verify that the device is powered on and properly connected</li>
|
||||
<li>Check all cable connections for any loose or damaged wires</li>
|
||||
<li>Ensure your network connection is stable and active</li>
|
||||
<li>Try restarting both the device and your computer</li>
|
||||
<li>{m.video_overlay_conn_verify_power()}</li>
|
||||
<li>{m.video_overlay_conn_check_cables()}</li>
|
||||
<li>{m.video_overlay_conn_ensure_network()}</li>
|
||||
<li>{m.video_overlay_conn_restart()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<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">
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||
Retrying connection...
|
||||
{m.video_overlay_retrying_connection()}
|
||||
</p>
|
||||
</div>
|
||||
</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="space-y-4">
|
||||
<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">
|
||||
<li>Ensure the HDMI cable securely connected at both ends</li>
|
||||
<li>
|
||||
Ensure source device is powered on and outputting a signal
|
||||
</li>
|
||||
<li>
|
||||
If using an adapter, ensure it's compatible and functioning
|
||||
correctly
|
||||
</li>
|
||||
<li>{m.video_overlay_no_hdmi_ensure_cable()}</li>
|
||||
<li>{m.video_overlay_no_hdmi_ensure_power()}</li>
|
||||
<li>{m.video_overlay_no_hdmi_adapter_compat()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||
theme="light"
|
||||
text="Learn more"
|
||||
text={m.video_overlay_learn_more()}
|
||||
TrailingIcon={ArrowRightIcon}
|
||||
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="space-y-4">
|
||||
<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">
|
||||
<li>A loose or faulty HDMI connection</li>
|
||||
<li>Incompatible resolution or refresh rate settings</li>
|
||||
<li>Issues with the source device's HDMI output</li>
|
||||
<li>{m.video_overlay_hdmi_loose_faulty()}</li>
|
||||
<li>{m.video_overlay_hdmi_incompatible_resolution()}</li>
|
||||
<li>{m.video_overlay_hdmi_source_issue()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
|
||||
theme="light"
|
||||
text="Learn more"
|
||||
text={m.video_overlay_learn_more()}
|
||||
TrailingIcon={ArrowRightIcon}
|
||||
size="SM"
|
||||
/>
|
||||
|
|
@ -334,7 +330,7 @@ export function NoAutoplayPermissionsOverlay({
|
|||
<OverlayContent>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-extrabold text-black dark:text-white">
|
||||
Autoplay permissions required
|
||||
{m.video_overlay_autoplay_permissions_required()}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2 text-center">
|
||||
|
|
@ -343,13 +339,13 @@ export function NoAutoplayPermissionsOverlay({
|
|||
size="MD"
|
||||
theme="primary"
|
||||
LeadingIcon={LuPlay}
|
||||
text="Manually start stream"
|
||||
text={m.video_overlay_manually_start_stream()}
|
||||
onClick={onPlayClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
|
@ -381,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
|||
<div className="flex items-center space-x-2">
|
||||
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Keyboard from "react-simple-keyboard";
|
||||
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 DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { useHidStore, useUiStore } from "@hooks/stores";
|
||||
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 { m } from "@localizations/messages.js";
|
||||
|
||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||
|
|
@ -244,20 +242,20 @@ function KeyboardWrapper() {
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Detach"
|
||||
text={m.detach()}
|
||||
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Attach"
|
||||
text={m.attach()}
|
||||
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
<div className="absolute right-2 flex items-center gap-x-2">
|
||||
<div className="hidden md:flex gap-x-2 items-center">
|
||||
|
|
@ -274,7 +272,7 @@ function KeyboardWrapper() {
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Hide"
|
||||
text={m.hide()}
|
||||
LeadingIcon={ChevronDownIcon}
|
||||
onClick={() => setVirtualKeyboardEnabled(false)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
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 { keys } from "@/keyboardMappings";
|
||||
import useKeyboard from "@hooks/useKeyboard";
|
||||
import useMouse from "@hooks/useMouse";
|
||||
import {
|
||||
useRTCStore,
|
||||
useSettingsStore,
|
||||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import useMouse from "@/hooks/useMouse";
|
||||
|
||||
} from "@hooks/stores";
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
import MacroBar from "@components/MacroBar";
|
||||
import InfoBar from "@components/InfoBar";
|
||||
import {
|
||||
HDMIErrorOverlay,
|
||||
LoadingVideoOverlay,
|
||||
NoAutoplayPermissionsOverlay,
|
||||
PointerLockBar,
|
||||
} from "./VideoOverlay";
|
||||
} from "@components/VideoOverlay";
|
||||
import { keys } from "@/keyboardMappings";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function WebRTCVideo() {
|
||||
// Video and stream related refs and states
|
||||
|
|
@ -168,10 +168,10 @@ export default function WebRTCVideo() {
|
|||
|
||||
const handlePointerLockChange = () => {
|
||||
if (document.pointerLockElement) {
|
||||
notifications.success("Pointer lock Enabled, press escape to unlock");
|
||||
notifications.success(m.video_pointer_lock_enabled());
|
||||
setIsPointerLockActive(true);
|
||||
} else {
|
||||
notifications.success("Pointer lock Disabled");
|
||||
notifications.success(m.video_pointer_lock_disabled());
|
||||
setIsPointerLockActive(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -234,6 +234,18 @@ export default function WebRTCVideo() {
|
|||
[getMouseWheelHandler],
|
||||
);
|
||||
|
||||
function getAdjustedKeyCode(e: KeyboardEvent) {
|
||||
const key = e.key;
|
||||
let code = e.code;
|
||||
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||
code = "Backquote";
|
||||
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
||||
code = "IntlBackslash";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -468,17 +480,6 @@ export default function WebRTCVideo() {
|
|||
};
|
||||
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||
|
||||
function getAdjustedKeyCode(e: KeyboardEvent) {
|
||||
const key = e.key;
|
||||
let code = e.code;
|
||||
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||
code = "Backquote";
|
||||
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
||||
code = "IntlBackslash";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
|
||||
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 Card from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
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
|
||||
|
||||
|
|
@ -33,9 +33,7 @@ export function ATXPowerControl() {
|
|||
useEffect(() => {
|
||||
send("getATXState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
setAtxState(resp.result as ATXState);
|
||||
|
|
@ -56,9 +54,7 @@ export function ATXPowerControl() {
|
|||
console.log("Sending long press ATX power action");
|
||||
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to send ATX power action: ${resp.error.data || "Unknown 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() }));
|
||||
}
|
||||
setIsPowerPressed(false);
|
||||
});
|
||||
|
|
@ -77,9 +73,7 @@ export function ATXPowerControl() {
|
|||
console.log("Sending short press ATX power action");
|
||||
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to send ATX power action: ${resp.error.data || "Unknown 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() }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -98,8 +92,8 @@ export function ATXPowerControl() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="ATX Power Control"
|
||||
description="Control your ATX power settings"
|
||||
title={m.extensions_atx_power_control()}
|
||||
description={m.extensions_atx_power_control_description()}
|
||||
/>
|
||||
|
||||
{atxState === null ? (
|
||||
|
|
@ -115,7 +109,7 @@ export function ATXPowerControl() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
LeadingIcon={LuPower}
|
||||
text="Power"
|
||||
text={m.atx_power_control_power_button()}
|
||||
onMouseDown={() => handlePowerPress(true)}
|
||||
onMouseUp={() => handlePowerPress(false)}
|
||||
onMouseLeave={() => handlePowerPress(false)}
|
||||
|
|
@ -125,13 +119,11 @@ export function ATXPowerControl() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
LeadingIcon={LuRotateCcw}
|
||||
text="Reset"
|
||||
text={m.atx_power_control_reset_button()}
|
||||
onClick={() => {
|
||||
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_reset_button(), error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
@ -150,7 +142,7 @@ export function ATXPowerControl() {
|
|||
atxState?.power ? "text-green-600" : "text-slate-300"
|
||||
}`}
|
||||
/>
|
||||
Power LED
|
||||
{m.atx_power_control_power_led()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -161,7 +153,7 @@ export function ATXPowerControl() {
|
|||
atxState?.hdd ? "text-blue-400" : "text-slate-300"
|
||||
}`}
|
||||
/>
|
||||
HDD LED
|
||||
{m.atx_power_control_hdd_led()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { LuPower } from "react-icons/lu";
|
||||
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 Card from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
interface DCPowerState {
|
||||
isOn: boolean;
|
||||
|
|
@ -25,9 +26,7 @@ export function DCPowerControl() {
|
|||
const getDCPowerState = useCallback(() => {
|
||||
send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.dc_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
setPowerState(resp.result as DCPowerState);
|
||||
|
|
@ -37,9 +36,7 @@ export function DCPowerControl() {
|
|||
const handlePowerToggle = (enabled: boolean) => {
|
||||
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.dc_power_control_set_power_state_error({ enabled: enabled, error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
getDCPowerState(); // Refresh state after change
|
||||
|
|
@ -49,17 +46,13 @@ export function DCPowerControl() {
|
|||
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.dc_power_control_set_restore_state_error({ state: state, error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
getDCPowerState(); // Refresh state after change
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getDCPowerState();
|
||||
// Set up polling interval to update status
|
||||
|
|
@ -70,8 +63,8 @@ export function DCPowerControl() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="DC Power Control"
|
||||
description="Control your DC power settings"
|
||||
title={m.extensions_dc_power_control()}
|
||||
description={m.extensions_dc_power_control_description()}
|
||||
/>
|
||||
|
||||
{powerState === null ? (
|
||||
|
|
@ -87,7 +80,7 @@ export function DCPowerControl() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
LeadingIcon={LuPower}
|
||||
text="Power On"
|
||||
text={m.dc_power_control_power_on_button()}
|
||||
onClick={() => handlePowerToggle(true)}
|
||||
disabled={powerState.isOn}
|
||||
/>
|
||||
|
|
@ -95,7 +88,7 @@ export function DCPowerControl() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
LeadingIcon={LuPower}
|
||||
text="Power Off"
|
||||
text={m.dc_power_control_power_off_button()}
|
||||
disabled={!powerState.isOn}
|
||||
onClick={() => handlePowerToggle(false)}
|
||||
/>
|
||||
|
|
@ -104,13 +97,13 @@ export function DCPowerControl() {
|
|||
<div className="flex items-center">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label="Restore Power Loss"
|
||||
label={m.dc_power_control_restore_power_state()}
|
||||
value={powerState.restoreState}
|
||||
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||
options={[
|
||||
{ value: '0', label: "Power OFF" },
|
||||
{ value: '1', label: "Power ON" },
|
||||
{ value: '2', label: "Last State" },
|
||||
{ value: '0', label: m.dc_power_control_power_off_state()},
|
||||
{ value: '1', label: m.dc_power_control_power_on_state()},
|
||||
{ value: '2', label: m.dc_power_control_restore_last_state()},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -120,21 +113,21 @@ export function DCPowerControl() {
|
|||
{/* Status Display */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<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">
|
||||
{powerState.voltage.toFixed(1)}V
|
||||
{powerState.voltage.toFixed(1)} {m.dc_power_control_voltage_unit()}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
{powerState.current.toFixed(1)}A
|
||||
{powerState.current.toFixed(1)} {m.dc_power_control_current_unit()}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
{powerState.power.toFixed(1)}W
|
||||
{powerState.power.toFixed(1)} {m.dc_power_control_power_unit()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { LuTerminal } from "react-icons/lu";
|
||||
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 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 { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
interface SerialSettings {
|
||||
baudRate: string;
|
||||
|
|
@ -28,9 +29,7 @@ export function SerialConsole() {
|
|||
useEffect(() => {
|
||||
send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.serial_console_get_settings_error({ error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
setSettings(resp.result as SerialSettings);
|
||||
|
|
@ -41,9 +40,7 @@ export function SerialConsole() {
|
|||
const newSettings = { ...settings, [setting]: value };
|
||||
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
notifications.error(m.serial_console_set_settings_error({ settings: setting, error: resp.error.data || m.unknown_error() }));
|
||||
return;
|
||||
}
|
||||
setSettings(newSettings);
|
||||
|
|
@ -54,8 +51,8 @@ export function SerialConsole() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Serial Console"
|
||||
description="Configure your serial console settings"
|
||||
title={m.extension_serial_console()}
|
||||
description={m.serial_console_configure_description()}
|
||||
/>
|
||||
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
|
|
@ -66,10 +63,10 @@ export function SerialConsole() {
|
|||
size="SM"
|
||||
theme="primary"
|
||||
LeadingIcon={LuTerminal}
|
||||
text="Open Console"
|
||||
text={m.serial_console_open_console()}
|
||||
onClick={() => {
|
||||
setTerminalType("serial");
|
||||
console.log("Opening serial console with settings: ", settings);
|
||||
setTerminalType("serial");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -77,7 +74,7 @@ export function SerialConsole() {
|
|||
{/* Settings */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<SelectMenuBasic
|
||||
label="Baud Rate"
|
||||
label={m.serial_console_baud_rate()}
|
||||
options={[
|
||||
{ label: "1200", value: "1200" },
|
||||
{ label: "2400", value: "2400" },
|
||||
|
|
@ -93,7 +90,7 @@ export function SerialConsole() {
|
|||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label="Data Bits"
|
||||
label={m.serial_console_data_bits()}
|
||||
options={[
|
||||
{ label: "8", value: "8" },
|
||||
{ label: "7", value: "7" },
|
||||
|
|
@ -103,7 +100,7 @@ export function SerialConsole() {
|
|||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label="Stop Bits"
|
||||
label={m.serial_console_stop_bits()}
|
||||
options={[
|
||||
{ label: "1", value: "1" },
|
||||
{ label: "1.5", value: "1.5" },
|
||||
|
|
@ -114,11 +111,13 @@ export function SerialConsole() {
|
|||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label="Parity"
|
||||
label={m.serial_console_parity()}
|
||||
options={[
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Even", value: "even" },
|
||||
{ label: "Odd", value: "odd" },
|
||||
{ label: m.serial_console_parity_none(), value: "none" },
|
||||
{ label: m.serial_console_parity_even(), value: "even" },
|
||||
{ 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}
|
||||
onChange={e => handleSettingChange("parity", e.target.value)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useEffect, useState } from "react";
|
||||
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 { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
||||
|
|
@ -20,20 +21,20 @@ interface Extension {
|
|||
const AVAILABLE_EXTENSIONS: Extension[] = [
|
||||
{
|
||||
id: "atx-power",
|
||||
name: "ATX Power Control",
|
||||
description: "Control your ATX Power extension",
|
||||
name: m.extensions_atx_power_control(),
|
||||
description: m.extensions_atx_power_control_description(),
|
||||
icon: LuPower,
|
||||
},
|
||||
{
|
||||
id: "dc-power",
|
||||
name: "DC Power Control",
|
||||
description: "Control your DC Power extension",
|
||||
name: m.extensions_dc_power_control(),
|
||||
description: m.extensions_dc_power_control(),
|
||||
icon: LuPlugZap,
|
||||
},
|
||||
{
|
||||
id: "serial-console",
|
||||
name: "Serial Console",
|
||||
description: "Access your serial console extension",
|
||||
name: m.extension_serial_console(),
|
||||
description: m.extension_serial_console_description(),
|
||||
icon: LuTerminal,
|
||||
},
|
||||
];
|
||||
|
|
@ -60,7 +61,7 @@ export default function ExtensionPopover() {
|
|||
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -101,7 +102,7 @@ export default function ExtensionPopover() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Unload Extension"
|
||||
text={m.extension_popover_unload_extension()}
|
||||
onClick={() => handleSetActiveExtension(null)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -110,8 +111,8 @@ export default function ExtensionPopover() {
|
|||
// Extensions List View
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Extensions"
|
||||
description="Load and manage your extensions"
|
||||
title={m.extensions_popover_extensions()}
|
||||
description={m.extension_popover_load_and_manage_extensions()}
|
||||
/>
|
||||
<Card className="animate-fadeIn opacity-0" >
|
||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||
|
|
@ -131,7 +132,7 @@ export default function ExtensionPopover() {
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Load"
|
||||
text={m.load()}
|
||||
onClick={() => handleSetActiveExtension(extension)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { forwardRef, useEffect, useCallback } from "react";
|
||||
import {
|
||||
LuLink,
|
||||
LuPlus,
|
||||
LuRadioReceiver,
|
||||
} from "react-icons/lu";
|
||||
import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
|
||||
import { useClose } from "@headlessui/react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { formatters } from "@/utils";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
|
|
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||
if ("error" in response) {
|
||||
notifications.error(
|
||||
`Failed to get virtual media state: ${response.error.message}`,
|
||||
);
|
||||
notifications.error(m.mount_get_state_error({ error: response.error.message }));
|
||||
} else {
|
||||
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
|
||||
}
|
||||
|
|
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
const handleUnmount = () => {
|
||||
send("unmountImage", {}, (response: JsonRpcResponse) => {
|
||||
if ("error" in response) {
|
||||
notifications.error(`Failed to unmount image: ${response.error.message}`);
|
||||
notifications.error(m.mount_unmount_error({ error: response.error.message }));
|
||||
} else {
|
||||
syncRemoteVirtualMediaState();
|
||||
}
|
||||
|
|
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
No mounted media
|
||||
{m.mount_no_mounted_media()}
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||
Streaming from URL
|
||||
{m.mount_streaming_from_url()}
|
||||
</h3>
|
||||
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(url, 55)}
|
||||
|
|
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||
Mounted from JetKVM Storage
|
||||
{m.mount_mounted_from_storage()}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||
{formatters.truncateMiddle(path, 50)}
|
||||
|
|
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<div className="h-full space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Virtual Media"
|
||||
description="Mount an image to boot from or install an operating system."
|
||||
title={m.mount_virtual_media()}
|
||||
description={m.mount_virtual_media_description()}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -162,10 +157,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
</div>
|
||||
{remoteVirtualMediaState ? (
|
||||
<div className="flex select-none items-center justify-between text-xs">
|
||||
<div className="select-none text-white dark:text-slate-300">
|
||||
<span>Mounted as</span>{" "}
|
||||
<div className="select-none text-white dark:text-slate-300">
|
||||
<span>{m.mount_mounted_as()}</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
||||
{remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
text={m.close()}
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
|
|
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Unmount"
|
||||
text={m.mount_unmount()}
|
||||
LeadingIcon={({ className }) => (
|
||||
<svg
|
||||
className={`${className} h-2.5 w-2.5 shrink-0`}
|
||||
|
|
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
text={m.close()}
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
|
|
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Add New Media"
|
||||
text={m.mount_add_new_media()}
|
||||
onClick={() => {
|
||||
setModalView("mode");
|
||||
navigateTo("/mount");
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useClose } from "@headlessui/react";
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { LuCornerDownLeft } from "react-icons/lu";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
|
||||
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
|
|
@ -105,7 +106,7 @@ export default function PasteModal() {
|
|||
}
|
||||
} catch (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]);
|
||||
|
||||
|
|
@ -122,8 +123,8 @@ export default function PasteModal() {
|
|||
<div className="h-full space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Paste text"
|
||||
description="Paste text from your client to the remote host"
|
||||
title={m.paste_modal_paste_text()}
|
||||
description={m.paste_modal_paste_text_description()}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -143,7 +144,7 @@ export default function PasteModal() {
|
|||
>
|
||||
<TextAreaWithLabel
|
||||
ref={TextAreaRef}
|
||||
label="Paste from host"
|
||||
label={m.paste_modal_paste_from_host()}
|
||||
rows={4}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
maxLength={pasteMaxLength}
|
||||
|
|
@ -176,7 +177,7 @@ export default function PasteModal() {
|
|||
<div className="mt-2 flex items-center gap-x-2">
|
||||
<ExclamationCircleIcon className="h-4 w-4 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(", ")}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -186,8 +187,8 @@ export default function PasteModal() {
|
|||
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
|
||||
<InputFieldWithLabel
|
||||
type="number"
|
||||
label="Delay between keys"
|
||||
placeholder="Delay between keys"
|
||||
label={m.paste_modal_delay_between_keys()}
|
||||
placeholder={m.paste_modal_delay_between_keys()}
|
||||
min={50}
|
||||
max={65534}
|
||||
value={delayValue}
|
||||
|
|
@ -199,15 +200,14 @@ export default function PasteModal() {
|
|||
<div className="mt-2 flex items-center gap-x-2">
|
||||
<ExclamationCircleIcon className="h-4 w-4 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
|
||||
{selectedKeyboard.name}
|
||||
{m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -224,7 +224,7 @@ export default function PasteModal() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Cancel"
|
||||
text={m.cancel()}
|
||||
onClick={() => {
|
||||
onCancelPasteMode();
|
||||
close();
|
||||
|
|
@ -233,7 +233,7 @@ export default function PasteModal() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Confirm Paste"
|
||||
text={m.paste_modal_confirm_paste()}
|
||||
disabled={isPasteInProgress}
|
||||
onClick={onConfirmPaste}
|
||||
LeadingIcon={LuCornerDownLeft}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, useRef } from "react";
|
||||
import { LuPlus, LuArrowLeft } from "react-icons/lu";
|
||||
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import { Button } from "@/components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { Button } from "@components/Button";
|
||||
|
||||
interface AddDeviceFormProps {
|
||||
onAddDevice: (name: string, macAddress: string) => void;
|
||||
|
|
@ -34,8 +35,8 @@ export default function AddDeviceForm({
|
|||
>
|
||||
<InputFieldWithLabel
|
||||
ref={nameInputRef}
|
||||
placeholder="Plex Media Server"
|
||||
label="Device Name"
|
||||
placeholder={m.wake_on_lan_add_device_example_device_name()}
|
||||
label={m.wake_on_lan_add_device_device_name()}
|
||||
required
|
||||
onChange={e => {
|
||||
setIsDeviceNameValid(e.target.validity.valid);
|
||||
|
|
@ -46,7 +47,7 @@ export default function AddDeviceForm({
|
|||
<InputFieldWithLabel
|
||||
ref={macInputRef}
|
||||
placeholder="00:b0:d0:63:c2:26"
|
||||
label="MAC Address"
|
||||
label={m.wake_on_lan_add_device_mac_address()}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
required
|
||||
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
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Back"
|
||||
text={m.wake_on_lan_add_device_back()}
|
||||
LeadingIcon={LuArrowLeft}
|
||||
onClick={() => setShowAddForm(false)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Save Device"
|
||||
text={m.wake_on_lan_add_device_save_device()}
|
||||
disabled={!isDeviceNameValid || !isMacAddressValid}
|
||||
onClick={() => {
|
||||
const deviceName = nameInputRef.current?.value || "";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import Card from "@/components/Card";
|
||||
import { FieldError } from "@/components/InputField";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { Button } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { FieldError } from "@components/InputField";
|
||||
|
||||
export interface StoredDevice {
|
||||
name: string;
|
||||
|
|
@ -46,7 +47,7 @@ export default function DeviceList({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Wake"
|
||||
text={m.wake_on_lan_device_list_wake()}
|
||||
LeadingIcon={LuSend}
|
||||
onClick={() => onSendMagicPacket(device.macAddress)}
|
||||
/>
|
||||
|
|
@ -55,7 +56,7 @@ export default function DeviceList({
|
|||
theme="danger"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => onDeleteDevice(index)}
|
||||
aria-label="Delete device"
|
||||
aria-label={m.wake_on_lan_device_list_delete_device()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -69,11 +70,11 @@ export default function DeviceList({
|
|||
animationDelay: "0.2s",
|
||||
}}
|
||||
>
|
||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
||||
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Add New Device"
|
||||
text={m.wake_on_lan_device_list_add_new_device()}
|
||||
onClick={() => setShowAddForm(true)}
|
||||
LeadingIcon={LuPlus}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { PlusCircleIcon } from "@heroicons/react/16/solid";
|
||||
import { LuPlus } from "react-icons/lu";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { Button } from "@/components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import Card from "@components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
|
||||
export default function EmptyStateCard({
|
||||
onCancelWakeOnLanModal,
|
||||
|
|
@ -25,10 +26,10 @@ export default function EmptyStateCard({
|
|||
</Card>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,11 +42,11 @@ export default function EmptyStateCard({
|
|||
animationDelay: "0.2s",
|
||||
}}
|
||||
>
|
||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
||||
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Add New Device"
|
||||
text={m.wake_on_lan_empty_add_new_device()}
|
||||
onClick={() => setShowAddForm(true)}
|
||||
LeadingIcon={LuPlus}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useClose } from "@headlessui/react";
|
||||
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useRTCStore, useUiStore } from "@hooks/stores";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import EmptyStateCard from "./EmptyStateCard";
|
||||
|
|
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
|
|||
if ("error" in resp) {
|
||||
const isInvalid = resp.error.data?.includes("invalid MAC address");
|
||||
if (isInvalid) {
|
||||
setErrorMessage("Invalid MAC address");
|
||||
setErrorMessage(m.wake_on_lan_invalid_mac());
|
||||
} else {
|
||||
setErrorMessage("Failed to send Magic Packet");
|
||||
setErrorMessage(m.wake_on_lan_failed_send_magic());
|
||||
}
|
||||
} else {
|
||||
notifications.success("Magic Packet sent successfully");
|
||||
notifications.success(m.wake_on_lan_magic_sent_success());
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
|
|||
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
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 {
|
||||
setShowAddForm(false);
|
||||
syncStoredDevices();
|
||||
|
|
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
|
|||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Wake On LAN"
|
||||
description="Send a Magic Packet to wake up a remote device."
|
||||
title={m.wake_on_lan()}
|
||||
description={m.wake_on_lan_description()}
|
||||
/>
|
||||
|
||||
{showAddForm ? (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useInterval } from "usehooks-ts";
|
||||
|
||||
import SidebarHeader from "@/components/SidebarHeader";
|
||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
||||
import { m } from "@localizations/messages.js";
|
||||
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 { createChartArray, Metric } from "../Metric";
|
||||
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
||||
|
||||
export default function ConnectionStatsSidebar() {
|
||||
const { sidebarView, setSidebarView } = useUiStore();
|
||||
const {
|
||||
|
|
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
|
|||
|
||||
return (
|
||||
<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="space-y-4">
|
||||
{sidebarView === "connection-stats" && (
|
||||
|
|
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
|
|||
{/* Connection Group */}
|
||||
<div className="space-y-3">
|
||||
<SettingsSectionHeader
|
||||
title="Connection"
|
||||
description="The connection between the client and the JetKVM."
|
||||
title={m.connection_stats_connection()}
|
||||
description={m.connection_stats_connection_description()}
|
||||
/>
|
||||
<Metric
|
||||
title="Round-Trip Time"
|
||||
description="Round-trip time for the active ICE candidate pair between peers."
|
||||
title={m.connection_stats_round_trip_time()}
|
||||
description={m.connection_stats_round_trip_time_description()}
|
||||
stream={iceCandidatePairStats}
|
||||
metric="currentRoundTripTime"
|
||||
map={x => ({
|
||||
|
|
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
|
|||
{/* Video Group */}
|
||||
<div className="space-y-3">
|
||||
<SettingsSectionHeader
|
||||
title="Video"
|
||||
description="The video stream from the JetKVM to the client."
|
||||
title={m.connection_stats_video()}
|
||||
description={m.connection_stats_video_description()}
|
||||
/>
|
||||
|
||||
{/* RTP Jitter */}
|
||||
<Metric
|
||||
title="Network Stability"
|
||||
badge="Jitter"
|
||||
title={m.connection_stats_network_stability()}
|
||||
badge={m.connection_stats_badge_jitter()}
|
||||
badgeTheme="light"
|
||||
description="How steady the flow of inbound video packets is across the network."
|
||||
description={m.connection_stats_network_stability_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="jitter"
|
||||
map={x => ({
|
||||
|
|
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
|
|||
|
||||
{/* Playback Delay */}
|
||||
<Metric
|
||||
title="Playback Delay"
|
||||
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
|
||||
badge="Jitter Buffer Avg. Delay"
|
||||
title={m.connection_stats_playback_delay()}
|
||||
description={m.connection_stats_playback_delay_description()}
|
||||
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
|
||||
badgeTheme="light"
|
||||
data={jitterBufferAvgDelayData}
|
||||
gate={inboundVideoRtpStats}
|
||||
|
|
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
|
|||
|
||||
{/* Packets Lost */}
|
||||
<Metric
|
||||
title="Packets Lost"
|
||||
description="Count of lost inbound video RTP packets."
|
||||
title={m.connection_stats_packets_lost()}
|
||||
description={m.connection_stats_packets_lost_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="packetsLost"
|
||||
domain={[0, 100]}
|
||||
|
|
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
|
|||
|
||||
{/* Frames Per Second */}
|
||||
<Metric
|
||||
title="Frames per second"
|
||||
description="Number of inbound video frames displayed per second."
|
||||
title={m.connection_stats_frames_per_second()}
|
||||
description={m.connection_stats_frames_per_second_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="framesPerSecond"
|
||||
domain={[0, 80]}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const DEFAULT_DELAY = 50;
|
||||
export const MAX_STEPS_PER_MACRO = 10;
|
||||
export const MAX_KEYS_PER_STEP = 10;
|
||||
export const MAX_TOTAL_MACROS = 25;
|
||||
export const MAX_TOTAL_MACROS = 25;
|
||||
export const COPY_SUFFIX = "(copy)";
|
||||
|
|
@ -245,6 +245,8 @@ export class KeyboardMacroReportMessage extends RpcMessage {
|
|||
...fromUint32toUint8(this.stepCount),
|
||||
]), 0);
|
||||
|
||||
let offset = 6;
|
||||
|
||||
for (let i = 0; i < this.stepCount; i++) {
|
||||
const step = this.steps[i];
|
||||
if (!withinUint8Range(step.modifier)) {
|
||||
|
|
@ -270,10 +272,9 @@ export class KeyboardMacroReportMessage extends RpcMessage {
|
|||
...keys,
|
||||
...fromUint16toUint8(step.delay),
|
||||
]);
|
||||
const offset = 6 + i * 9;
|
||||
|
||||
|
||||
data.set(macroBinary, offset);
|
||||
offset += 9;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@ export interface UIState {
|
|||
|
||||
export const useUiStore = create<UIState>(set => ({
|
||||
terminalType: "none",
|
||||
setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }),
|
||||
setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }),
|
||||
|
||||
sidebarView: null,
|
||||
setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }),
|
||||
setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }),
|
||||
|
||||
disableVideoFocusTrap: false,
|
||||
setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }),
|
||||
|
|
@ -195,7 +195,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
videoStreamStatsHistory: new Map(),
|
||||
|
||||
isTurnServerInUse: false,
|
||||
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
||||
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
||||
|
||||
inboundRtpStats: new Map(),
|
||||
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => {
|
||||
|
|
@ -461,11 +461,11 @@ export const hidKeyBufferSize = 6;
|
|||
export const hidErrorRollOver = 0x01;
|
||||
|
||||
export interface KeysDownState {
|
||||
modifier: number;
|
||||
keys: number[];
|
||||
modifier: number;
|
||||
keys: number[];
|
||||
}
|
||||
|
||||
export type USBStates =
|
||||
export type USBStates =
|
||||
| "configured"
|
||||
| "attached"
|
||||
| "not attached"
|
||||
|
|
@ -493,7 +493,7 @@ export const useHidStore = create<HidState>(set => ({
|
|||
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
|
||||
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
||||
|
||||
keysDownState: { modifier: 0, keys: [0,0,0,0,0,0] } as KeysDownState,
|
||||
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
||||
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||
|
||||
isVirtualKeyboardEnabled: false,
|
||||
|
|
@ -521,34 +521,34 @@ export type UpdateModalViews =
|
|||
| "error";
|
||||
|
||||
export interface OtaState {
|
||||
updating: boolean;
|
||||
error: string | null;
|
||||
updating: boolean;
|
||||
error: string | null;
|
||||
|
||||
metadataFetchedAt: string | null;
|
||||
metadataFetchedAt: string | null;
|
||||
|
||||
// App update
|
||||
appUpdatePending: boolean;
|
||||
// App update
|
||||
appUpdatePending: boolean;
|
||||
|
||||
appDownloadProgress: number;
|
||||
appDownloadFinishedAt: string | null;
|
||||
appDownloadProgress: number;
|
||||
appDownloadFinishedAt: string | null;
|
||||
|
||||
appVerificationProgress: number;
|
||||
appVerifiedAt: string | null;
|
||||
appVerificationProgress: number;
|
||||
appVerifiedAt: string | null;
|
||||
|
||||
appUpdateProgress: number;
|
||||
appUpdatedAt: string | null;
|
||||
appUpdateProgress: number;
|
||||
appUpdatedAt: string | null;
|
||||
|
||||
// System update
|
||||
systemUpdatePending: boolean;
|
||||
// System update
|
||||
systemUpdatePending: boolean;
|
||||
|
||||
systemDownloadProgress: number;
|
||||
systemDownloadFinishedAt: string | null;
|
||||
systemDownloadProgress: number;
|
||||
systemDownloadFinishedAt: string | null;
|
||||
|
||||
systemVerificationProgress: number;
|
||||
systemVerifiedAt: string | null;
|
||||
systemVerificationProgress: number;
|
||||
systemVerifiedAt: string | null;
|
||||
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
};
|
||||
|
||||
export interface UpdateState {
|
||||
|
|
@ -603,7 +603,7 @@ export type UsbConfigModalViews =
|
|||
| "updateUsbConfigSuccess";
|
||||
|
||||
export interface UsbConfigModalState {
|
||||
modalView: UsbConfigModalViews ;
|
||||
modalView: UsbConfigModalViews;
|
||||
errorMessage: string | null;
|
||||
setModalView: (view: UsbConfigModalViews) => void;
|
||||
setErrorMessage: (message: string | null) => void;
|
||||
|
|
@ -620,7 +620,7 @@ export interface UsbConfigState {
|
|||
export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
|
||||
modalView: "updateUsbConfig",
|
||||
errorMessage: null,
|
||||
setModalView: (view: UsbConfigModalViews) => set({ modalView: view }),
|
||||
setModalView: (view: UsbConfigModalViews) => set({ modalView: view }),
|
||||
setErrorMessage: (message: string | null) => set({ errorMessage: message }),
|
||||
}));
|
||||
|
||||
|
|
@ -633,13 +633,13 @@ export type LocalAuthModalViews =
|
|||
| "updateSuccess";
|
||||
|
||||
export interface LocalAuthModalState {
|
||||
modalView:LocalAuthModalViews;
|
||||
setModalView: (view:LocalAuthModalViews) => void;
|
||||
modalView: LocalAuthModalViews;
|
||||
setModalView: (view: LocalAuthModalViews) => void;
|
||||
}
|
||||
|
||||
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
|
||||
modalView: "createPassword",
|
||||
setModalView: (view: LocalAuthModalViews) => set({ modalView: view }),
|
||||
setModalView: (view: LocalAuthModalViews) => set({ modalView: view }),
|
||||
}));
|
||||
|
||||
export interface DeviceState {
|
||||
|
|
@ -779,12 +779,12 @@ export interface MacrosState {
|
|||
loadMacros: () => Promise<void>;
|
||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||
sendFn:
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
setSendFn: (
|
||||
sendFn: (
|
||||
method: string,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@hooks/stores";
|
||||
|
||||
import {
|
||||
CancelKeyboardMacroReportMessage,
|
||||
|
|
@ -71,7 +71,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
}: sendMessageParams = {},
|
||||
) => {
|
||||
if (hidRpcDisabled) return;
|
||||
if (rpcHidChannel?.readyState !== "open") return;
|
||||
if (rpcHidChannel?.readyState !== "open") return;
|
||||
if (!rpcHidReady && !ignoreHandshakeState) return;
|
||||
|
||||
let data: Uint8Array | undefined;
|
||||
|
|
@ -163,7 +163,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
(message: HandshakeMessage) => {
|
||||
if (hidRpcDisabled) return;
|
||||
|
||||
if (!message.version) {
|
||||
if (!message.version) {
|
||||
console.error("Received handshake message without version", message);
|
||||
return;
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
setRpcHidProtocolVersion,
|
||||
sendHandshake,
|
||||
handleHandshake,
|
||||
hidRpcDisabled,
|
||||
hidRpcDisabled,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@hooks/stores";
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: string;
|
||||
|
|
@ -79,7 +79,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
|||
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||
};
|
||||
},
|
||||
[rpcDataChannel, onRequest]);
|
||||
[rpcDataChannel, onRequest]);
|
||||
|
||||
return { send };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export default function useKeyboard() {
|
|||
// 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.
|
||||
// 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.
|
||||
|
||||
// HidRPC is a binary format for exchanging keyboard and mouse events
|
||||
|
|
@ -148,66 +149,6 @@ export default function useKeyboard() {
|
|||
}
|
||||
}, [rpcHidReady, sendKeyboardEventHidRpc, handleLegacyKeyboardReport, cancelKeepAlive]);
|
||||
|
||||
// handleKeyPress is used to handle a key press or release event.
|
||||
// This function handle both key press and key release events.
|
||||
// It checks if the keyPressReport API is available and sends the key press event.
|
||||
// If the keyPressReport API is not available, it simulates the device-side key
|
||||
// handling for legacy devices and updates the keysDownState accordingly.
|
||||
// It then sends the full keyboard state to the device.
|
||||
|
||||
const sendKeypress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
cancelKeepAlive();
|
||||
|
||||
sendKeypressEventHidRpc(key, press);
|
||||
|
||||
if (press) {
|
||||
scheduleKeepAlive();
|
||||
}
|
||||
},
|
||||
[sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
|
||||
);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
async (key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||
|
||||
if (rpcHidReady) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||
// It sends the key and whether it is pressed or released.
|
||||
// Older device version doesn't support this API, so we will switch to local key handling
|
||||
// In that case we will switch to local key handling and update the keysDownState
|
||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||
sendKeypress(key, press);
|
||||
} else {
|
||||
// Older backends don't support the hidRpc API, so we need:
|
||||
// 1. Calculate the state
|
||||
// 2. Send the newly calculated state to the device
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
keysDownState,
|
||||
key,
|
||||
press,
|
||||
);
|
||||
|
||||
handleLegacyKeyboardReport(downState.keys, downState.modifier);
|
||||
|
||||
// if we just sent ErrorRollOver, reset to empty state
|
||||
if (downState.keys[0] === hidErrorRollOver) {
|
||||
resetKeyboardState();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
rpcDataChannel?.readyState,
|
||||
rpcHidReady,
|
||||
keysDownState,
|
||||
handleLegacyKeyboardReport,
|
||||
resetKeyboardState,
|
||||
sendKeypress,
|
||||
],
|
||||
);
|
||||
|
||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
|
|
@ -272,12 +213,71 @@ export default function useKeyboard() {
|
|||
return { modifier: modifiers, keys };
|
||||
}
|
||||
|
||||
const sendKeypress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
cancelKeepAlive();
|
||||
|
||||
sendKeypressEventHidRpc(key, press);
|
||||
|
||||
if (press) {
|
||||
scheduleKeepAlive();
|
||||
}
|
||||
},
|
||||
[sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
|
||||
);
|
||||
|
||||
// handleKeyPress is used to handle a key press or release event.
|
||||
// This function handle both key press and key release events.
|
||||
// It checks if the keyPressReport API is available and sends the key press event.
|
||||
// If the keyPressReport API is not available, it simulates the device-side key
|
||||
// handling for legacy devices and updates the keysDownState accordingly.
|
||||
// It then sends the full keyboard state to the device.
|
||||
const handleKeyPress = useCallback(
|
||||
async (key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||
|
||||
if (rpcHidReady) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||
// It sends the key and whether it is pressed or released.
|
||||
// Older device version doesn't support this API, so we will switch to local key handling
|
||||
// In that case we will switch to local key handling and update the keysDownState
|
||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||
sendKeypress(key, press);
|
||||
} else {
|
||||
// Older backends don't support the hidRpc API, so we need:
|
||||
// 1. Calculate the state
|
||||
// 2. Send the newly calculated state to the device
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
keysDownState,
|
||||
key,
|
||||
press,
|
||||
);
|
||||
|
||||
handleLegacyKeyboardReport(downState.keys, downState.modifier);
|
||||
|
||||
// if we just sent ErrorRollOver, reset to empty state
|
||||
if (downState.keys[0] === hidErrorRollOver) {
|
||||
resetKeyboardState();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
rpcDataChannel?.readyState,
|
||||
rpcHidReady,
|
||||
keysDownState,
|
||||
handleLegacyKeyboardReport,
|
||||
resetKeyboardState,
|
||||
sendKeypress,
|
||||
],
|
||||
);
|
||||
|
||||
// Cleanup function to cancel keepalive timer
|
||||
const cleanup = useCallback(() => {
|
||||
cancelKeepAlive();
|
||||
}, [cancelKeepAlive]);
|
||||
|
||||
|
||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||
// The keys and modifiers are pressed together and held for the delay duration.
|
||||
|
|
@ -306,6 +306,7 @@ export default function useKeyboard() {
|
|||
|
||||
sendKeyboardMacroEventHidRpc(macro);
|
||||
}, [sendKeyboardMacroEventHidRpc]);
|
||||
|
||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||
const promises: (() => Promise<void>)[] = [];
|
||||
|
||||
|
|
@ -355,6 +356,7 @@ export default function useKeyboard() {
|
|||
});
|
||||
});
|
||||
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
|
||||
|
||||
const executeMacro = useCallback(async (steps: MacroSteps) => {
|
||||
if (rpcHidReady) {
|
||||
return executeMacroRemote(steps);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default function useKeyboardLayout() {
|
|||
const selectedKeyboard = useMemo(() => {
|
||||
// fallback to original behaviour of en-US if no isoCode given or matching layout not found
|
||||
return keyboards.find(keyboard => keyboard.isoCode === isoCode)
|
||||
?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!;
|
||||
?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!;
|
||||
}, [isoCode]);
|
||||
|
||||
return { keyboardOptions, isoCode, selectedKeyboard };
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useCallback } from "react";
|
|||
import { useDeviceStore } from "@/hooks/stores";
|
||||
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export interface VersionInfo {
|
||||
appVersion: string;
|
||||
|
|
@ -29,7 +30,7 @@ export function useVersion() {
|
|||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
|
||||
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"));
|
||||
} else {
|
||||
const result = resp.result as SystemVersionInfo;
|
||||
|
|
@ -37,7 +38,7 @@ export function useVersion() {
|
|||
setSystemVersion(result.local.systemVersion);
|
||||
|
||||
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"));
|
||||
} else {
|
||||
resolve(result);
|
||||
|
|
@ -57,7 +58,7 @@ export function useVersion() {
|
|||
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
|
||||
}
|
||||
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"));
|
||||
} else {
|
||||
const result = resp.result as VersionInfo;
|
||||
|
|
|
|||
|
|
@ -29,4 +29,4 @@ import { nb_NO } from "@/keyboardLayouts/nb_NO"
|
|||
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { da_DK } from "@/keyboardLayouts/da_DK"
|
||||
|
||||
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE, da_DK ];
|
||||
export const keyboards: KeyboardLayout[] = [cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE, da_DK];
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ const chars = {
|
|||
z: { key: "KeyZ" },
|
||||
"ż": { key: "KeyZ", accentKey: keyOverdot },
|
||||
";": { key: "Backquote" },
|
||||
"°": { key: "Backquote", shift: true, deadKey: true },
|
||||
"°": { key: "Backquote", shift: true, deadKey: true },
|
||||
"+": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"ě": { key: "Digit2" },
|
||||
|
|
@ -251,7 +251,7 @@ export const cs_CZ: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -6,10 +6,10 @@ export const name = "Dansk";
|
|||
const isoCode = "da-DK";
|
||||
|
||||
const keyTrema = { key: "BracketRight" }
|
||||
const keyAcute = { key: "Equal", altRight: true }
|
||||
const keyHat = { key: "BracketRight", shift: true }
|
||||
const keyGrave = { key: "Equal", shift: true }
|
||||
const keyTilde = { key: "BracketRight", altRight: true }
|
||||
const keyAcute = { key: "Equal", altRight: true }
|
||||
const keyHat = { key: "BracketRight", shift: true }
|
||||
const keyGrave = { key: "Equal", shift: true }
|
||||
const keyTilde = { key: "BracketRight", altRight: true }
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
|
|
@ -61,8 +61,8 @@ export const chars = {
|
|||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
|
|
@ -115,7 +115,7 @@ export const chars = {
|
|||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" }, // <-- corrected
|
||||
z: { key: "KeyZ" }, // <-- corrected
|
||||
"½": { key: "Backquote" },
|
||||
"½": { key: "Backquote" },
|
||||
"§": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
|
|
@ -163,11 +163,11 @@ export const chars = {
|
|||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"~": { key: "BracketRight", deadKey: true, altRight: true },
|
||||
"^": { key: "BracketRight", deadKey: true, shift: true },
|
||||
"¨": { key: "BracketRight", deadKey: true, },
|
||||
"|": { key: "Equal", deadKey: true, altRight: true},
|
||||
"`": { key: "Equal", deadKey: true, shift: true, },
|
||||
"~": { key: "BracketRight", deadKey: true, altRight: true },
|
||||
"^": { key: "BracketRight", deadKey: true, shift: true },
|
||||
"¨": { key: "BracketRight", deadKey: true, },
|
||||
"|": { key: "Equal", deadKey: true, altRight: true },
|
||||
"`": { key: "Equal", deadKey: true, shift: true, },
|
||||
"´": { key: "Equal", deadKey: true, },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
|
|
@ -181,7 +181,7 @@ export const da_DK: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export const de_CH: KeyboardLayout = {
|
|||
isoCode: isoCode,
|
||||
name: name,
|
||||
chars: chars,
|
||||
keyDisplayMap: keyDisplayMap,
|
||||
keyDisplayMap: keyDisplayMap,
|
||||
// TODO need to localize these maps and layouts
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export const en_UK: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -113,7 +113,7 @@ export const chars = {
|
|||
"~": { key: "Backquote", shift: true },
|
||||
"§": { key: "IntlBackslash" },
|
||||
"±": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Escape: { key: "Escape" },
|
||||
|
|
@ -264,13 +264,13 @@ export const virtualKeyboard = {
|
|||
},
|
||||
control: {
|
||||
default: [
|
||||
"PrintScreen ScrollLock Pause",
|
||||
"PrintScreen ScrollLock Pause",
|
||||
"Insert Home PageUp",
|
||||
"Delete End PageDown"
|
||||
],
|
||||
shift: [
|
||||
"(PrintScreen) ScrollLock (Pause)",
|
||||
"Insert Home PageUp",
|
||||
"Insert Home PageUp",
|
||||
"Delete End PageDown"
|
||||
],
|
||||
},
|
||||
|
|
@ -303,7 +303,7 @@ export const en_US: KeyboardLayout = {
|
|||
isoCode,
|
||||
name,
|
||||
chars,
|
||||
keyDisplayMap,
|
||||
keyDisplayMap,
|
||||
modifierDisplayMap,
|
||||
virtualKeyboard
|
||||
};
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ const chars = {
|
|||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"º": { key: "Backquote" },
|
||||
"ª": { key: "Backquote", shift: true },
|
||||
"\\": { key: "Backquote", altRight: true },
|
||||
"ª": { key: "Backquote", shift: true },
|
||||
"\\": { key: "Backquote", altRight: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
|
|
@ -175,7 +175,7 @@ export const es_ES: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -174,7 +174,7 @@ export const fr_BE: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ export const fr_CH: KeyboardLayout = {
|
|||
isoCode: isoCode,
|
||||
name: name,
|
||||
chars: chars,
|
||||
keyDisplayMap: keyDisplayMap,
|
||||
keyDisplayMap: keyDisplayMap,
|
||||
// TODO need to localize these maps and layouts
|
||||
modifierDisplayMap: de_CH.modifierDisplayMap,
|
||||
virtualKeyboard: de_CH.virtualKeyboard
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export const fr_FR: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -60,7 +60,7 @@ const chars = {
|
|||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"\\": { key: "Backquote" },
|
||||
"|": { key: "Backquote", shift: true },
|
||||
"|": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
|
|
@ -120,7 +120,7 @@ export const it_IT: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -115,7 +115,7 @@ const chars = {
|
|||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"|": { key: "Backquote" },
|
||||
"|": { key: "Backquote" },
|
||||
"§": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const chars = {
|
|||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"§": { key: "Backquote" },
|
||||
"½": { key: "Backquote", shift: true },
|
||||
"½": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
|
|
@ -171,7 +171,7 @@ export const sv_SE: KeyboardLayout = {
|
|||
name: name,
|
||||
chars: chars,
|
||||
// TODO need to localize these maps and layouts
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
keyDisplayMap: en_US.keyDisplayMap,
|
||||
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||
virtualKeyboard: en_US.virtualKeyboard
|
||||
};
|
||||
|
|
@ -121,7 +121,7 @@ export const keys = {
|
|||
Hanja: 0x91,
|
||||
Katakana: 0x92,
|
||||
Hiragana: 0x93,
|
||||
ZenkakuHankaku:0x94,
|
||||
ZenkakuHankaku: 0x94,
|
||||
LockingCapsLock: 0x82,
|
||||
LockingNumLock: 0x83,
|
||||
LockingScrollLock: 0x84,
|
||||
|
|
@ -279,7 +279,7 @@ export const hidKeyToModifierMask = {
|
|||
export const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "Meta", "Compose", "Kana"];
|
||||
|
||||
export function decodeModifiers(modifier: number) {
|
||||
return {
|
||||
return {
|
||||
isShiftActive: (modifier & (modifiers.ShiftLeft | modifiers.ShiftRight)) !== 0,
|
||||
isControlActive: (modifier & (modifiers.ControlLeft | modifiers.ControlRight)) !== 0,
|
||||
isAltActive: (modifier & (modifiers.AltLeft | modifiers.AltRight)) !== 0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { lazy } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
isRouteErrorResponse,
|
||||
|
|
@ -8,11 +7,13 @@ import {
|
|||
RouterProvider,
|
||||
useRouteError,
|
||||
} from "react-router";
|
||||
import "./index.css";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import Root from "@/root";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import Card from "@components/Card";
|
||||
import EmptyCard from "@components/EmptyCard";
|
||||
import NotFoundPage from "@components/NotFoundPage";
|
||||
|
|
@ -28,12 +29,12 @@ import DeviceIdRename from "@routes/devices.$id.rename";
|
|||
import DevicesRoute from "@routes/devices";
|
||||
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
||||
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
||||
import Notifications from "@/notifications";
|
||||
import Notifications from "@/notifications";
|
||||
const SignupRoute = lazy(() => import("@routes/signup"));
|
||||
const LoginRoute = lazy(() => import("@routes/login"));
|
||||
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
||||
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 SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse"));
|
||||
const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard"));
|
||||
|
|
@ -116,7 +117,7 @@ if (isOnDevice) {
|
|||
path: "/",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <DeviceRoute />,
|
||||
HydrateFallback: () => <div className="p-4">Loading...</div>,
|
||||
HydrateFallback: () => <div className="p-4">{m.loading()}</div>,
|
||||
loader: DeviceRoute.loader,
|
||||
children: [
|
||||
{
|
||||
|
|
@ -390,22 +391,46 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const errorMessage = error?.data?.error?.message || error?.message;
|
||||
if (isRouteErrorResponse(error)) {
|
||||
if (error.status === 404) return <NotFoundPage />;
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: unknown): string | null => {
|
||||
// If it's a route error response, try to read a string at err.data.error.message or err.data.error safely
|
||||
if (isRouteErrorResponse(err)) {
|
||||
const data = (err as { data?: unknown }).data;
|
||||
if (data && typeof data === "object") {
|
||||
const maybeError = (data as Record<string, unknown>)["error"];
|
||||
if (maybeError) {
|
||||
if (typeof maybeError === "object") {
|
||||
const msg = (maybeError as Record<string, unknown>)["message"];
|
||||
if (typeof msg === "string") return msg;
|
||||
} else if (typeof maybeError === "string") {
|
||||
return maybeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check plain object message property
|
||||
if (err && typeof err === "object") {
|
||||
const maybeMsg = (err as Record<string, unknown>)["message"];
|
||||
if (typeof maybeMsg === "string") return maybeMsg;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="w-full max-w-2xl">
|
||||
<EmptyCard
|
||||
IconElm={ExclamationTriangleIcon}
|
||||
headline="Oh no!"
|
||||
description="Something went wrong. Please try again later or contact support"
|
||||
headline={m.oh_no()}
|
||||
description={m.something_went_wrong()}
|
||||
BtnElm={
|
||||
errorMessage && (
|
||||
<Card>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
|
||||
import React, { useEffect } from "react";
|
||||
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
|
||||
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
|
||||
import Card from "@components/Card";
|
||||
|
||||
interface NotificationOptions {
|
||||
duration?: number;
|
||||
|
|
@ -34,7 +33,7 @@ const ToastContent = ({
|
|||
const notifications = {
|
||||
success: (message: string, options?: NotificationOptions) => {
|
||||
return toast.custom(
|
||||
t => (
|
||||
(t: Toast) => (
|
||||
<ToastContent
|
||||
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
|
||||
message={message}
|
||||
|
|
@ -47,7 +46,7 @@ const notifications = {
|
|||
|
||||
error: (message: string, options?: NotificationOptions) => {
|
||||
return toast.custom(
|
||||
t => (
|
||||
(t: Toast) => (
|
||||
<ToastContent
|
||||
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
|
||||
message={message}
|
||||
|
|
@ -64,9 +63,9 @@ function useMaxToasts(max: number) {
|
|||
|
||||
useEffect(() => {
|
||||
toasts
|
||||
.filter(t => t.visible) // Only consider visible toasts
|
||||
.filter((_, i) => i >= max) // Is toast index over limit?
|
||||
.forEach(t => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
||||
.filter((t: Toast) => t.visible) // Only consider visible toasts
|
||||
.filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
|
||||
.forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
||||
}, [toasts, max]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
|||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { User } from "@hooks/stores";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { CardHeader } from "@components/CardHeader";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { checkAuth } from "@/main";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface LoaderData {
|
||||
device: { id: string; name: string; user: { googleId: string } };
|
||||
|
|
@ -28,11 +29,12 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { message: "There was an error deregistering your device. Please try again." };
|
||||
return { message: m.deregister_error({ status: res.statusText }) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: "There was an error deregistering your device. Please try again." };
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return { message: m.deregister_error({ status: message }) };
|
||||
}
|
||||
|
||||
return redirect("/devices");
|
||||
|
|
@ -68,7 +70,7 @@ export default function DevicesIdDeregister() {
|
|||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<DashboardNavbar
|
||||
isLoggedIn={!!user}
|
||||
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
|
||||
primaryLinks={[{ title: m.deregister_cloud_devices(), to: "/devices" }]}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={device?.name}
|
||||
|
|
@ -82,21 +84,14 @@ export default function DevicesIdDeregister() {
|
|||
size="SM"
|
||||
theme="blank"
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
text="Back to Devices"
|
||||
text={m.back_to_devices()}
|
||||
to="/devices"
|
||||
/>
|
||||
<Card className="max-w-3xl p-6">
|
||||
<div className="max-w-xl space-y-4">
|
||||
<CardHeader
|
||||
headline={`Deregister ${device.name || device.id} from your cloud account`}
|
||||
description={
|
||||
<>
|
||||
This will remove the device from your cloud account and revoke
|
||||
remote access to it.
|
||||
<br />
|
||||
Please note that local access will still be possible
|
||||
</>
|
||||
}
|
||||
headline={m.deregister_headline({ device: device.name || device.id })}
|
||||
description={m.deregister_description()}
|
||||
/>
|
||||
|
||||
<Fieldset>
|
||||
|
|
@ -107,20 +102,20 @@ export default function DevicesIdDeregister() {
|
|||
size="MD"
|
||||
theme="light"
|
||||
to="/devices"
|
||||
text="Cancel"
|
||||
text={m.cancel()}
|
||||
textAlign="center"
|
||||
/>
|
||||
<Button
|
||||
size="MD"
|
||||
theme="danger"
|
||||
type="submit"
|
||||
text="Deregister from Cloud"
|
||||
text={m.deregister_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</div>
|
||||
{error?.message && (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">
|
||||
{error?.message}
|
||||
{m.deregister_error({ status: error.message })}
|
||||
</p>
|
||||
)}
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
LuLink,
|
||||
LuRadioReceiver,
|
||||
|
|
@ -7,28 +8,28 @@ import {
|
|||
} from "react-icons/lu";
|
||||
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import Card, { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import { formatters } from "@/utils";
|
||||
import DebianIcon from "@assets/debian-icon.png";
|
||||
import UbuntuIcon from "@assets/ubuntu-icon.png";
|
||||
import FedoraIcon from "@assets/fedora-icon.png";
|
||||
import OpenSUSEIcon from "@assets/opensuse-icon.png";
|
||||
import ArchIcon from "@assets/arch-icon.png";
|
||||
import NetBootIcon from "@assets/netboot-icon.svg";
|
||||
import LogoBlueIcon from "@assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import DebianIcon from "@/assets/debian-icon.png";
|
||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||
import FedoraIcon from "@/assets/fedora-icon.png";
|
||||
import OpenSUSEIcon from "@/assets/opensuse-icon.png";
|
||||
import ArchIcon from "@/assets/arch-icon.png";
|
||||
import NetBootIcon from "@/assets/netboot-icon.svg";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { formatters } from "@/utils";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { isOnDevice } from "../main";
|
||||
import { cx } from "../cva.config";
|
||||
import {
|
||||
MountMediaState,
|
||||
RemoteVirtualMediaState,
|
||||
|
|
@ -145,12 +146,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<img
|
||||
src={LogoBlueIcon}
|
||||
alt="JetKVM Logo"
|
||||
alt={m.jetkvm_logo()}
|
||||
className="block h-[24px] dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt="JetKVM Logo"
|
||||
alt={m.jetkvm_logo()}
|
||||
className="hidden h-[24px] dark:mt-0! dark:block"
|
||||
/>
|
||||
{modalView === "mode" && (
|
||||
|
|
@ -238,26 +239,26 @@ function ModeSelectionView({
|
|||
<div className="w-full space-y-4">
|
||||
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||
Virtual Media Source
|
||||
{m.mount_virtual_media_source()}
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
Choose how you want to mount your virtual media
|
||||
{m.mount_virtual_media_source_description()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
label: "URL Mount",
|
||||
label: m.mount_url_mount(),
|
||||
value: "url",
|
||||
description: "Mount files from any public web address",
|
||||
description: m.mount_url_description(),
|
||||
icon: LuLink,
|
||||
tag: "Experimental",
|
||||
tag: m.experimental(),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "JetKVM Storage Mount",
|
||||
label: m.mount_jetkvm_storage(),
|
||||
value: "device",
|
||||
description: "Mount previously uploaded files from the JetKVM storage",
|
||||
description: m.mount_jetkvm_storage_description(),
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
|
|
@ -332,7 +333,7 @@ function ModeSelectionView({
|
|||
onClick={() => {
|
||||
setModalView(selectedMode);
|
||||
}}
|
||||
text="Continue"
|
||||
text={m.continue()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -351,6 +352,7 @@ function UrlView({
|
|||
}) {
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [isUrlValid, setIsUrlValid] = useState(false);
|
||||
|
||||
const popularImages = [
|
||||
{
|
||||
|
|
@ -398,6 +400,12 @@ function UrlView({
|
|||
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlRef.current) {
|
||||
setIsUrlValid(urlRef.current.validity.valid);
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
function handleUrlChange(url: string) {
|
||||
setUrl(url);
|
||||
if (url.endsWith(".iso")) {
|
||||
|
|
@ -410,8 +418,8 @@ function UrlView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from URL"
|
||||
description="Enter an URL to the image file to mount"
|
||||
title={m.mount_view_url_title()}
|
||||
description={m.mount_view_url_description()}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -423,7 +431,7 @@ function UrlView({
|
|||
<InputFieldWithLabel
|
||||
placeholder="https://example.com/image.iso"
|
||||
type="url"
|
||||
label="Image URL"
|
||||
label={m.mount_url_input_label()}
|
||||
ref={urlRef}
|
||||
value={url}
|
||||
onChange={e => handleUrlChange(e.target.value)}
|
||||
|
|
@ -436,19 +444,19 @@ function UrlView({
|
|||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<Fieldset disabled={!urlRef.current?.validity.valid || url.length === 0}>
|
||||
<Fieldset disabled={!isUrlValid || url.length === 0}>
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
||||
<Button size="MD" theme="blank" text={m.back()} onClick={onBack} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
loading={mountInProgress}
|
||||
text="Mount URL"
|
||||
text={m.mount_button_mount_url()}
|
||||
onClick={() => onMount(url, usbMode)}
|
||||
disabled={
|
||||
mountInProgress || !urlRef.current?.validity.valid || url.length === 0
|
||||
mountInProgress || !isUrlValid || url.length === 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -463,7 +471,7 @@ function UrlView({
|
|||
}}
|
||||
>
|
||||
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
||||
Popular images
|
||||
{m.mount_popular_images()}
|
||||
</h2>
|
||||
<Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||
{popularImages.map((image, index) => (
|
||||
|
|
@ -487,7 +495,7 @@ function UrlView({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Select"
|
||||
text={m.mount_button_select()}
|
||||
onClick={() => handleUrlChange(image.url)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -553,7 +561,7 @@ function DeviceFileView({
|
|||
const syncStorage = useCallback(() => {
|
||||
send("listStorageFiles", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error listing storage files: ${resp.error}`);
|
||||
notifications.error(m.mount_error_list_storage({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
const { files } = resp.result as StorageFiles;
|
||||
|
|
@ -568,7 +576,7 @@ function DeviceFileView({
|
|||
|
||||
send("getStorageSpace", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error getting storage space: ${resp.error}`);
|
||||
notifications.error(m.mount_error_get_storage_space({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +605,7 @@ function DeviceFileView({
|
|||
console.log("Deleting file:", file);
|
||||
send("deleteStorageFile", { filename: file.name }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error deleting file: ${resp.error}`);
|
||||
notifications.error(m.mount_error_delete_file({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -630,8 +638,8 @@ function DeviceFileView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from JetKVM Storage"
|
||||
description="Select an image to mount from the JetKVM storage"
|
||||
title={m.mount_view_device_title()}
|
||||
description={m.mount_view_device_description()}
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
|
|
@ -647,17 +655,17 @@ function DeviceFileView({
|
|||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
No images available
|
||||
{m.mount_no_images_title()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload an image to start virtual media mounting.
|
||||
{m.mount_no_images_description()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
text={m.mount_upload_title()}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -677,9 +685,7 @@ function DeviceFileView({
|
|||
const selectedFile = onStorageFiles.find(f => f.name === file.name);
|
||||
if (!selectedFile) return;
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete " + selectedFile.name + "?",
|
||||
)
|
||||
window.confirm(m.mount_confirm_delete({ name: selectedFile.name }))
|
||||
) {
|
||||
handleDeleteFile(selectedFile);
|
||||
}
|
||||
|
|
@ -692,24 +698,24 @@ function DeviceFileView({
|
|||
{onStorageFiles.length > filesPerPage && (
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||
Showing <span className="font-bold">{indexOfFirstFile + 1}</span> to{" "}
|
||||
<span className="font-bold">
|
||||
{Math.min(indexOfLastFile, onStorageFiles.length)}
|
||||
</span>{" "}
|
||||
of <span className="font-bold">{onStorageFiles.length}</span> results
|
||||
{m.mount_button_showing_results({
|
||||
from: indexOfFirstFile + 1,
|
||||
to: Math.min(indexOfLastFile, onStorageFiles.length),
|
||||
total: onStorageFiles.length
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Previous"
|
||||
text={m.previous()}
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
text={m.next()}
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
/>
|
||||
|
|
@ -738,7 +744,7 @@ function DeviceFileView({
|
|||
size="MD"
|
||||
disabled={selected === null || mountInProgress}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
text={m.mount_button_mount_file()}
|
||||
loading={mountInProgress}
|
||||
onClick={() =>
|
||||
onMountStorageFile(
|
||||
|
|
@ -772,10 +778,10 @@ function DeviceFileView({
|
|||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{m.mount_available_storage()}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{m.mount_percentage_used({ percentageUsed })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
|
||||
|
|
@ -786,10 +792,10 @@ function DeviceFileView({
|
|||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{m.mount_bytes_used({ bytesUsed: formatters.bytes(bytesUsed) })}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{m.mount_bytes_free({ bytesFree: formatters.bytes(bytesFree) })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -806,7 +812,7 @@ function DeviceFileView({
|
|||
size="MD"
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
text={m.mount_button_upload_new_image()}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -862,7 +868,7 @@ function UploadFileView({
|
|||
|
||||
if (!rtcDataChannel) {
|
||||
console.error("Failed to create data channel for file upload");
|
||||
notifications.error("Failed to create data channel for file upload");
|
||||
notifications.error(m.mount_upload_failed_datachannel());
|
||||
setUploadState("idle");
|
||||
console.log("Upload state set to 'idle'");
|
||||
|
||||
|
|
@ -952,7 +958,7 @@ function UploadFileView({
|
|||
|
||||
rtcDataChannel.onerror = error => {
|
||||
console.error("RTC Data channel error:", error);
|
||||
notifications.error(`Upload failed: ${error}`);
|
||||
notifications.error(m.mount_upload_failed_rtc({ error: error }));
|
||||
setUploadState("idle");
|
||||
console.log("Upload state set to 'idle'");
|
||||
};
|
||||
|
|
@ -1037,7 +1043,7 @@ function UploadFileView({
|
|||
file.name !== incompleteFileName.replace(".incomplete", "")
|
||||
) {
|
||||
setFileError(
|
||||
`Please select the file "${incompleteFileName.replace(".incomplete", "")}" to continue the upload.`,
|
||||
m.mount_please_select_file({ name: incompleteFileName.replace(".incomplete", "") }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1080,11 +1086,11 @@ function UploadFileView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Upload New Image"
|
||||
title={m.mount_upload_title()}
|
||||
description={
|
||||
incompleteFileName
|
||||
? `Continue uploading "${incompleteFileName}"`
|
||||
: "Select an image file to upload to JetKVM storage"
|
||||
? m.mount_continue_uploading_with_name({ name: incompleteFileName.replace(".incomplete", "") })
|
||||
: m.mount_upload_description()
|
||||
}
|
||||
/>
|
||||
<div
|
||||
|
|
@ -1121,11 +1127,11 @@ function UploadFileView({
|
|||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
{incompleteFileName
|
||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: "Click to select a file"}
|
||||
? m.mount_click_to_select_incomplete({ name: incompleteFileName.replace(".incomplete", "") })
|
||||
: m.mount_click_to_select_file()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Supported formats: ISO, IMG
|
||||
{m.mount_supported_formats()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1140,7 +1146,7 @@ function UploadFileView({
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="leading-non text-lg font-semibold text-black dark:text-white">
|
||||
Uploading {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
{m.mount_uploading_with_name({ name: formatters.truncateMiddle(uploadedFileName, 30) })}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(uploadedFileSize || 0)}
|
||||
|
|
@ -1153,11 +1159,11 @@ function UploadFileView({
|
|||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-slate-600 dark:text-slate-400">
|
||||
<span>Uploading...</span>
|
||||
<span>{m.mount_uploading()}</span>
|
||||
<span>
|
||||
{uploadSpeed !== null
|
||||
? `${formatters.bytes(uploadSpeed)}/s`
|
||||
: "Calculating..."}
|
||||
: m.mount_calculating()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1174,11 +1180,10 @@ function UploadFileView({
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
Upload successful
|
||||
{m.mount_upload_successful()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} has been
|
||||
uploaded
|
||||
{m.mount_uploaded_has_been_uploaded({ name: formatters.truncateMiddle(uploadedFileName, 40) })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1205,7 +1210,7 @@ function UploadFileView({
|
|||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0"
|
||||
style={{ animationDuration: "0.7s" }}
|
||||
>
|
||||
Error: {uploadError}
|
||||
{m.mount_upload_error({ error: String(uploadError) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1221,7 +1226,7 @@ function UploadFileView({
|
|||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Cancel Upload"
|
||||
text={m.mount_button_cancel_upload()}
|
||||
onClick={() => {
|
||||
onCancelUpload();
|
||||
setUploadState("idle");
|
||||
|
|
@ -1235,7 +1240,7 @@ function UploadFileView({
|
|||
<Button
|
||||
size="MD"
|
||||
theme={uploadState === "success" ? "primary" : "light"}
|
||||
text="Back to Overview"
|
||||
text={m.mount_button_back_to_overview()}
|
||||
onClick={onBack}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1259,10 +1264,10 @@ function ErrorView({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<h2 className="text-lg leading-tight font-bold">Mount Error</h2>
|
||||
<h2 className="text-lg leading-tight font-bold">{m.mount_error_title()}</h2>
|
||||
</div>
|
||||
<p className="text-sm leading-snug text-slate-600">
|
||||
An error occurred while attempting to mount the media. Please try again.
|
||||
{m.mount_error_description()}
|
||||
</p>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
|
|
@ -1271,8 +1276,8 @@ function ErrorView({
|
|||
</Card>
|
||||
)}
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button size="SM" theme="light" text="Close" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text="Back to Overview" onClick={onRetry} />
|
||||
<Button size="SM" theme="light" text={m.close()} onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.mount_button_back_to_overview()} onClick={onRetry} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1341,7 +1346,7 @@ function PreUploadedImageItem({
|
|||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={TrashIcon}
|
||||
text="Delete"
|
||||
text={m.delete()}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
|
|
@ -1362,7 +1367,7 @@ function PreUploadedImageItem({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Continue uploading"
|
||||
text={m.mount_button_continue_upload()}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onContinueUpload();
|
||||
|
|
@ -1408,7 +1413,7 @@ function UsbModeSelector({
|
|||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
CD/DVD
|
||||
{m.mount_mode_cdrom()}
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="disk" className="flex items-center">
|
||||
|
|
@ -1421,7 +1426,7 @@ function UsbModeSelector({
|
|||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
Disk
|
||||
{m.mount_mode_disk()}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useNavigate, useOutletContext } from "react-router";
|
||||
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlue from "@/assets/logo-blue.svg";
|
||||
import LogoWhite from "@/assets/logo-white.svg";
|
||||
import { GridCard } from "@components/Card";
|
||||
import LogoBlue from "@assets/logo-blue.svg";
|
||||
import LogoWhite from "@assets/logo-white.svg";
|
||||
import { m } from "@localizations/messages";
|
||||
|
||||
interface ContextType {
|
||||
setupPeerConnection: () => Promise<void>;
|
||||
|
|
@ -30,14 +31,13 @@ export default function OtherSessionRoute() {
|
|||
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">
|
||||
Another Active Session Detected
|
||||
{m.other_session_detected()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
Only one active session is supported at a time. Would you like to take over
|
||||
this session?
|
||||
{m.other_session_take_over()}
|
||||
</p>
|
||||
<div className="flex items-center justify-start space-x-4">
|
||||
<Button size="SM" theme="primary" text="Use Here" onClick={handleClose} />
|
||||
<Button size="SM" theme="primary" text={m.other_session_use_here_button()} onClick={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
|||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { User } from "@hooks/stores";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { CardHeader } from "@components/CardHeader";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { checkAuth } from "@/main";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import api from "@/api";
|
||||
import { m } from "@localizations/messages";
|
||||
|
||||
interface LoaderData {
|
||||
device: { id: string; name: string; user: { googleId: string } };
|
||||
|
|
@ -24,7 +24,7 @@ const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) =
|
|||
const { name } = Object.fromEntries(await request.formData());
|
||||
|
||||
if (!name || name === "") {
|
||||
return { message: "Please specify a name" };
|
||||
return { message: m.rename_device_no_name() };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -32,11 +32,11 @@ const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) =
|
|||
name,
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { message: "There was an error renaming your device. Please try again." };
|
||||
return { message: m.rename_device_error({ error: res.statusText }) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: "There was an error renaming your device. Please try again." };
|
||||
return { message: m.rename_device_error({ error: String(e) }) };
|
||||
}
|
||||
|
||||
return redirect("/devices");
|
||||
|
|
@ -86,24 +86,24 @@ export default function DeviceIdRename() {
|
|||
size="SM"
|
||||
theme="blank"
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
text="Back to Devices"
|
||||
text={m.back_to_devices()}
|
||||
to="/devices"
|
||||
/>
|
||||
<Card className="max-w-3xl p-6">
|
||||
<div className="space-y-4">
|
||||
<CardHeader
|
||||
headline={`Rename ${device.name || device.id}`}
|
||||
description="Properly name your device to easily identify it."
|
||||
headline={m.rename_device_headline({ name: device.name || device.id })}
|
||||
description={m.rename_device_description()}
|
||||
/>
|
||||
|
||||
<Fieldset>
|
||||
<Form method="POST" className="max-w-sm space-y-4">
|
||||
<div className="group relative">
|
||||
<InputFieldWithLabel
|
||||
label="New device name"
|
||||
label={m.rename_device_new_name_label()}
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Plex Media Server"
|
||||
placeholder={m.rename_device_new_name_placeholder()}
|
||||
size="MD"
|
||||
autoFocus
|
||||
error={error?.message.toString()}
|
||||
|
|
@ -114,7 +114,7 @@ export default function DeviceIdRename() {
|
|||
size="MD"
|
||||
theme="primary"
|
||||
type="submit"
|
||||
text="Rename Device"
|
||||
text={m.rename_device_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { redirect } from "react-router";
|
||||
import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||
|
||||
import { getDeviceUiPath } from "../hooks/useAppNavigation";
|
||||
import { getDeviceUiPath } from "@hooks/useAppNavigation";
|
||||
|
||||
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||
return redirect(getDeviceUiPath("/settings/general", params.id));
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useLoaderData, useNavigate } from "react-router";
|
||||
import type { LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import api from "@/api";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button, LinkButton } from "@/components/Button";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import api from "@/api";
|
||||
import notifications from "@/notifications";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { CloudState } from "./adopt";
|
||||
|
|
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to de-register device: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_deregister({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
const onCloudAdoptClick = useCallback(
|
||||
(cloudApiUrl: string, cloudAppUrl: string) => {
|
||||
if (!deviceId) {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
return;
|
||||
}
|
||||
|
||||
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -160,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success("TLS settings updated successfully");
|
||||
notifications.success(m.access_tls_updated());
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -206,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Access"
|
||||
description="Manage the Access Control of the device"
|
||||
title={m.access_title()}
|
||||
description={m.access_description()}
|
||||
/>
|
||||
|
||||
{loaderData?.authMode && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Local"
|
||||
description="Manage the mode of local access to the device"
|
||||
title={m.access_local_title()}
|
||||
description={m.access_local_description()}
|
||||
/>
|
||||
<>
|
||||
<SettingsItem
|
||||
title="HTTPS Mode"
|
||||
title={m.access_https_mode_title()}
|
||||
badge="Experimental"
|
||||
description="Configure secure HTTPS access to your device"
|
||||
description={m.access_https_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -229,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
|
|||
onChange={e => handleTlsModeChange(e.target.value)}
|
||||
disabled={tlsMode === "unknown"}
|
||||
options={[
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "self-signed", label: "Self-signed" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "disabled", label: m.access_tls_disabled() },
|
||||
{ value: "self-signed", label: m.access_tls_self_signed() },
|
||||
{ value: "custom", label: m.access_tls_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -240,12 +240,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="TLS Certificate"
|
||||
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
|
||||
title={m.access_tls_certificate_title()}
|
||||
description={m.access_tls_certificate_description()}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Certificate"
|
||||
label={m.access_certificate_label()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
|
|
@ -258,8 +258,8 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Private Key"
|
||||
description="For security reasons, it will not be displayed after saving."
|
||||
label={m.access_private_key_label()}
|
||||
description={m.access_private_key_description()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
|
|
@ -274,7 +274,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update TLS Settings"
|
||||
text={m.access_update_tls_settings()}
|
||||
onClick={handleCustomTlsUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -282,14 +282,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Authentication Mode"
|
||||
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
|
||||
title={m.access_authentication_mode_title()}
|
||||
description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
|
||||
>
|
||||
{loaderData.authMode === "password" ? (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Disable Protection"
|
||||
text={m.access_disable_protection()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
|
|
@ -298,7 +298,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable Password"
|
||||
text={m.access_enable_password()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
|
|
@ -309,13 +309,13 @@ export default function SettingsAccessIndexRoute() {
|
|||
|
||||
{loaderData.authMode === "password" && (
|
||||
<SettingsItem
|
||||
title="Change Password"
|
||||
description="Update your device access password"
|
||||
title={m.access_change_password_title()}
|
||||
description={m.access_change_password_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Change Password"
|
||||
text={m.access_change_password_button()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
|
|
@ -330,23 +330,23 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Remote"
|
||||
description="Manage the mode of Remote access to the device"
|
||||
description={m.access_remote_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!isAdopted && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Cloud Provider"
|
||||
description="Select the cloud provider for your device"
|
||||
title={m.access_cloud_provider_title()}
|
||||
description={m.access_cloud_provider_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedProvider}
|
||||
onChange={e => handleProviderChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "jetkvm", label: "JetKVM Cloud" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "jetkvm", label: m.access_provider_jetkvm() },
|
||||
{ value: "custom", label: m.access_provider_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -356,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud API URL"
|
||||
label={m.access_cloud_api_url_label()}
|
||||
value={cloudApiUrl}
|
||||
onChange={e => setCloudApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
|
|
@ -365,7 +365,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud App URL"
|
||||
label={m.access_cloud_app_url_label()}
|
||||
value={cloudAppUrl}
|
||||
onChange={e => setCloudAppUrl(e.target.value)}
|
||||
placeholder="https://app.example.com"
|
||||
|
|
@ -384,26 +384,26 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Cloud Security
|
||||
{m.access_cloud_security_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li>
|
||||
<li>Zero Trust security model</li>
|
||||
<li>OIDC (OpenID Connect) authentication</li>
|
||||
<li>All streams encrypted in transit</li>
|
||||
<li>{m.access_security_encryption()}</li>
|
||||
<li>{m.access_security_zero_trust()}</li>
|
||||
<li>{m.access_security_oidc()}</li>
|
||||
<li>{m.access_security_streams()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
All cloud components are open-source and available on{" "}
|
||||
{m.access_security_open_source()}{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
GitHub
|
||||
{m.access_github_link()}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
|
@ -415,7 +415,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
to="https://jetkvm.com/docs/networking/remote-access"
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Learn about our cloud security"
|
||||
text={m.access_learn_security()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -429,32 +429,32 @@ export default function SettingsAccessIndexRoute() {
|
|||
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Adopt KVM to Cloud"
|
||||
text={m.access_adopt_kvm()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Your device is adopted to the Cloud
|
||||
{m.access_adopted_message()}
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="De-register from Cloud"
|
||||
text={m.access_deregister()}
|
||||
className="text-red-600"
|
||||
onClick={() => {
|
||||
if (deviceId) {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to de-register this device?",
|
||||
m.access_confirm_deregister(),
|
||||
)
|
||||
) {
|
||||
deregisterDevice();
|
||||
}
|
||||
} else {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useRevalidator } from "react-router";
|
||||
|
||||
import { useLocalAuthModalStore } from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { Button } from "@components/Button";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import api from "@/api";
|
||||
import { useLocalAuthModalStore } from "@/hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SecurityAccessLocalAuthRoute() {
|
||||
const { setModalView } = useLocalAuthModalStore();
|
||||
|
|
@ -34,12 +35,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
const handleCreatePassword = async (password: string, confirmPassword: string) => {
|
||||
if (password === "") {
|
||||
setError("Please enter a password");
|
||||
setError(m.local_auth_error_enter_password());
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(m.local_auth_error_passwords_not_match());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +52,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while setting the password");
|
||||
setError(data.error || m.local_auth_error_setting_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while setting the password");
|
||||
setError(m.local_auth_error_setting_password());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -65,17 +66,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
confirmNewPassword: string,
|
||||
) => {
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(m.local_auth_error_passwords_not_match());
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPassword === "") {
|
||||
setError("Please enter your old password");
|
||||
setError(m.local_auth_error_enter_old_password());
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === "") {
|
||||
setError("Please enter a new password");
|
||||
setError(m.local_auth_error_enter_new_password());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -91,17 +92,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while changing the password");
|
||||
setError(data.error || m.local_auth_error_changing_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while changing the password");
|
||||
setError(m.local_auth_error_changing_password());
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePassword = async (password: string) => {
|
||||
if (password === "") {
|
||||
setError("Please enter your current password");
|
||||
setError(m.local_auth_error_enter_current_password());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -113,11 +114,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while disabling the password");
|
||||
setError(data.error || m.local_auth_error_disabling_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while disabling the password");
|
||||
setError(m.local_auth_error_disabling_password());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -150,24 +151,24 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
{modalView === "creationSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Set Successfully"
|
||||
description="You've successfully set up local device protection. Your device is now secure against unauthorized local access."
|
||||
headline={m.local_auth_success_password_set_title()}
|
||||
description={m.local_auth_success_password_set_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "deleteSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Protection Disabled"
|
||||
description="You've successfully disabled the password protection for local access. Remember, your device is now less secure."
|
||||
headline={m.local_auth_success_password_disabled_title()}
|
||||
description={m.local_auth_success_password_disabled_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "updateSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Updated Successfully"
|
||||
description="You've successfully changed your local device protection password. Make sure to remember your new password for future access."
|
||||
headline={m.local_auth_success_password_updated_title()}
|
||||
description={m.local_auth_success_password_updated_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -198,24 +199,24 @@ function CreatePasswordModal({
|
|||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Local Device Protection
|
||||
{m.local_auth_create_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Create a password to protect your device from unauthorized local access.
|
||||
{m.local_auth_create_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={m.local_auth_create_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter a strong password"
|
||||
placeholder={m.local_auth_create_new_password_placeholder()}
|
||||
value={password}
|
||||
autoFocus
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={m.local_auth_confirm_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Re-enter your password"
|
||||
placeholder={m.local_auth_create_confirm_password_placeholder()}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -224,10 +225,10 @@ function CreatePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Secure Device"
|
||||
text={m.local_auth_create_secure_button()}
|
||||
onClick={() => onSetPassword(password, confirmPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.local_auth_create_not_now_button()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
|
@ -251,16 +252,16 @@ function DeletePasswordModal({
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Disable Local Device Protection
|
||||
{m.local_auth_disable_local_device_protection_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password to disable local device protection.
|
||||
{m.local_auth_disable_local_device_protection_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={m.local_auth_current_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={m.local_auth_enter_current_password_placeholder()}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -268,10 +269,10 @@ function DeletePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Disable Protection"
|
||||
text={m.local_auth_disable_protection_button()}
|
||||
onClick={() => onDeletePassword(password)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
|
|
@ -306,31 +307,30 @@ function UpdatePasswordModal({
|
|||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Change Local Device Password
|
||||
{m.local_auth_change_local_device_password_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password and a new password to update your local device
|
||||
protection.
|
||||
{m.local_auth_change_local_device_password_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={m.local_auth_current_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={m.local_auth_enter_current_password_placeholder()}
|
||||
value={oldPassword}
|
||||
onChange={e => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={m.local_auth_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter a new strong password"
|
||||
placeholder={m.local_auth_enter_new_password_placeholder()}
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={m.local_auth_confirm_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Re-enter your new password"
|
||||
placeholder={m.local_auth_reenter_new_password_placeholder()}
|
||||
value={confirmNewPassword}
|
||||
onChange={e => setConfirmNewPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -338,10 +338,10 @@ function UpdatePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update Password"
|
||||
text={m.local_auth_update_password_button()}
|
||||
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
|
@ -365,7 +365,7 @@ function SuccessModal({
|
|||
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
|
||||
</div>
|
||||
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.close()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox from "@components/Checkbox";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
|
||||
import { Button } from "../components/Button";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "../components/TextArea";
|
||||
import { useSettingsStore } from "../hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import { isOnDevice } from "../main";
|
||||
import notifications from "../notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -65,7 +65,9 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? m.advanced_error_usb_emulation_enable({error: resp.error.data || m.unknown_error()})
|
||||
: m.advanced_error_usb_emulation_disable({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -80,11 +82,11 @@ export default function SettingsAdvancedRoute() {
|
|||
send("resetConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_reset_config({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Configuration reset to default successfully");
|
||||
notifications.success(m.advanced_success_reset_config());
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -92,11 +94,11 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_update_ssh_key({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("SSH key updated successfully");
|
||||
notifications.success(m.advanced_success_update_ssh_key());
|
||||
});
|
||||
}, [send, sshKey]);
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_set_dev_mode({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -120,7 +122,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_set_dev_channel({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -135,19 +137,17 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? m.advanced_error_loopback_enable({error: resp.error.data || m.unknown_error()})
|
||||
: m.advanced_error_loopback_disable({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success(m.advanced_success_loopback_enabled());
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success(m.advanced_success_loopback_disabled());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -175,14 +175,14 @@ export default function SettingsAdvancedRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
title={m.advanced_title()}
|
||||
description={m.advanced_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Dev Channel Updates"
|
||||
description="Receive early updates from the development channel"
|
||||
title={m.advanced_dev_channel_title()}
|
||||
description={m.advanced_dev_channel_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={devChannel}
|
||||
|
|
@ -192,8 +192,8 @@ export default function SettingsAdvancedRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Developer Mode"
|
||||
description="Enable advanced features for developers"
|
||||
title={m.advanced_developer_mode_title()}
|
||||
description={m.advanced_developer_mode_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={settings.developerMode}
|
||||
|
|
@ -219,18 +219,17 @@ export default function SettingsAdvancedRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Developer Mode Enabled
|
||||
{m.advanced_developer_mode_enabled_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>Security is weakened while active</li>
|
||||
<li>Only use if you understand the risks</li>
|
||||
<li>{m.advanced_developer_mode_warning_security()}</li>
|
||||
<li>{m.advanced_developer_mode_warning_risks()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
For advanced users only. Not for production use.
|
||||
{m.advanced_developer_mode_warning_advanced()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -238,8 +237,8 @@ export default function SettingsAdvancedRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Loopback-Only Mode"
|
||||
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||
title={m.advanced_loopback_only_title()}
|
||||
description={m.advanced_loopback_only_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
|
|
@ -250,25 +249,25 @@ export default function SettingsAdvancedRoute() {
|
|||
{isOnDevice && settings.developerMode && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="SSH Access"
|
||||
description="Add your SSH public key to enable secure remote access to the device"
|
||||
title={m.advanced_ssh_access_title()}
|
||||
description={m.advanced_ssh_access_description()}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="SSH Public Key"
|
||||
label={m.advanced_ssh_public_key_label()}
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder="Enter your SSH public key"
|
||||
placeholder={m.advanced_ssh_public_key_placeholder()}
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The default SSH user is <strong>root</strong>.
|
||||
{m.advanced_ssh_default_user()}<strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update SSH Key"
|
||||
text={m.advanced_update_ssh_key_button()}
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -277,8 +276,8 @@ export default function SettingsAdvancedRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Troubleshooting Mode"
|
||||
description="Diagnostic tools and additional controls for troubleshooting and development purposes"
|
||||
title={m.advanced_troubleshooting_mode_title()}
|
||||
description={m.advanced_troubleshooting_mode_description()}
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={settings.debugMode}
|
||||
|
|
@ -291,27 +290,27 @@ export default function SettingsAdvancedRoute() {
|
|||
{settings.debugMode && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="USB Emulation"
|
||||
description="Control the USB emulation state"
|
||||
title={m.advanced_usb_emulation_title()}
|
||||
description={m.advanced_usb_emulation_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={
|
||||
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
|
||||
usbEmulationEnabled ? m.advanced_disable_usb_emulation() : m.advanced_enable_usb_emulation()
|
||||
}
|
||||
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Reset Configuration"
|
||||
description="Reset configuration to default. This will log you out."
|
||||
title={m.advanced_reset_config_title()}
|
||||
description={m.advanced_reset_config_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
text={m.advanced_reset_config_button()}
|
||||
onClick={() => {
|
||||
handleResetConfig();
|
||||
window.location.reload();
|
||||
|
|
@ -327,22 +326,23 @@ export default function SettingsAdvancedRoute() {
|
|||
onClose={() => {
|
||||
setShowLoopbackWarning(false);
|
||||
}}
|
||||
title="Enable Loopback-Only Mode?"
|
||||
title={m.advanced_loopback_warning_title()}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||
only.
|
||||
{m.advanced_loopback_warning_description()}
|
||||
</p>
|
||||
<p>
|
||||
{m.advanced_loopback_warning_before()}
|
||||
</p>
|
||||
<p>Before enabling this feature, make sure you have either:</p>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>SSH access configured and tested</li>
|
||||
<li>Cloud access enabled and working</li>
|
||||
<li>{m.advanced_loopback_warning_ssh()}</li>
|
||||
<li>{m.advanced_loopback_warning_cloud()}</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
confirmText="I Understand, Enable Anyway"
|
||||
confirmText={m.advanced_loopback_warning_confirm()}
|
||||
onConfirm={confirmLoopbackModeEnable}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback, useState } from "react";
|
||||
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsAppearanceRoute() {
|
||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||
|
|
@ -28,22 +28,24 @@ export default function SettingsAppearanceRoute() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const themeOptions = [
|
||||
{ value: "system", label: m.appearance_theme_system() },
|
||||
{ value: "light", label: m.appearance_theme_light() },
|
||||
{ value: "dark", label: m.appearance_theme_dark() },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Appearance"
|
||||
description="Customize the look and feel of your JetKVM interface"
|
||||
title={m.appearance_title()}
|
||||
description={m.appearance_page_description()}
|
||||
/>
|
||||
<SettingsItem title="Theme" description="Choose your preferred color theme">
|
||||
<SettingsItem title={m.appearance_theme()} description={m.appearance_description()}>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={currentTheme}
|
||||
options={[
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
]}
|
||||
options={themeOptions}
|
||||
onChange={e => {
|
||||
setCurrentTheme(e.target.value);
|
||||
handleThemeChange(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useState , useEffect } from "react";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox from "@components/Checkbox";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { Button } from "../components/Button";
|
||||
import notifications from "../notifications";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "../hooks/stores";
|
||||
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -34,7 +32,7 @@ export default function SettingsGeneralRoute() {
|
|||
send("setAutoUpdateState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`,
|
||||
m.general_auto_update_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -45,44 +43,36 @@ export default function SettingsGeneralRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="General"
|
||||
description="Configure device settings and update preferences"
|
||||
title={m.general_title()}
|
||||
description={m.general_page_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Check for Updates"
|
||||
title={m.general_check_for_updates()}
|
||||
description={
|
||||
currentVersions ? (
|
||||
<>
|
||||
App: {currentVersions.appVersion}
|
||||
<br />
|
||||
System: {currentVersions.systemVersion}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
App: Loading...
|
||||
<br />
|
||||
System: Loading...
|
||||
</>
|
||||
)
|
||||
<>
|
||||
{m.general_app_version({ version: currentVersions ? currentVersions.appVersion : m.loading() })}
|
||||
<br />
|
||||
{m.general_system_version({ version: currentVersions ? currentVersions.systemVersion : m.loading() })}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Check for Updates"
|
||||
text={m.general_check_for_updates()}
|
||||
onClick={() => navigateTo("./update")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Auto Update"
|
||||
description="Automatically update the device to the latest version"
|
||||
title={m.general_auto_update_title()}
|
||||
description={m.general_auto_update_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={autoUpdate}
|
||||
|
|
@ -95,14 +85,14 @@ export default function SettingsGeneralRoute() {
|
|||
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Reboot Device"
|
||||
description="Power cycle the JetKVM"
|
||||
title={m.general_reboot_device()}
|
||||
description={m.general_reboot_device_description()}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reboot Device"
|
||||
text={m.general_reboot_device()}
|
||||
onClick={() => navigateTo("./reboot")}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useNavigate } from "react-router";
|
||||
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 { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -10,7 +11,7 @@ export default function SettingsGeneralRebootRoute() {
|
|||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
// This is where we send the RPC to the golang binary
|
||||
send("reboot", {force: true});
|
||||
send("reboot", { force: true });
|
||||
}, [send]);
|
||||
|
||||
{
|
||||
|
|
@ -30,10 +31,10 @@ export function Dialog({
|
|||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
<ConfirmationBox
|
||||
onYes={onConfirmUpdate}
|
||||
onNo={onClose}
|
||||
/>
|
||||
<ConfirmationBox
|
||||
onYes={onConfirmUpdate}
|
||||
onNo={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -50,15 +51,15 @@ function ConfirmationBox({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Reboot JetKVM
|
||||
{m.general_reboot_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Do you want to proceed with rebooting the system?
|
||||
{m.general_reboot_description()}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||
<Button size="SM" theme="light" text={m.general_reboot_yes_button()} onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text={m.general_reboot_no_button()} onClick={onNo} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
|
||||
import { Button } from "@components/Button";
|
||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { SystemVersionInfo, useVersion } from "@/hooks/useVersion";
|
||||
import Card from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralUpdateRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -41,8 +42,6 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
|
|
@ -71,11 +70,6 @@ export function Dialog({
|
|||
[setModalView],
|
||||
);
|
||||
|
||||
// Reset modal view when dialog is opened
|
||||
useEffect(() => {
|
||||
setVersionInfo(null);
|
||||
}, [setModalView]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
|
|
@ -133,8 +127,6 @@ function LoadingState({
|
|||
|
||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
setProgressWidth("0%");
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
const signal = abortControllerRef.current.signal;
|
||||
|
||||
|
|
@ -169,10 +161,10 @@ function LoadingState({
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-0">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Checking for updates...
|
||||
{m.general_update_checking_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
We{"'"}re ensuring your device has the latest features and improvements.
|
||||
{m.general_update_checking_description()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
|
||||
|
|
@ -183,7 +175,7 @@ function LoadingState({
|
|||
></div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancelCheck} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancelCheck} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -237,16 +229,18 @@ function UpdatingDeviceState({
|
|||
const verfiedAt = otaState[`${type}VerifiedAt`];
|
||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||
|
||||
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_app_type());
|
||||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
return m.general_update_status_fetching();
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
return m.general_update_status_downloading({ update_type });
|
||||
} else if (!verfiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
return m.general_update_status_verifying({ update_type });
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
return m.general_update_status_installing({ update_type });
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
return m.general_update_status_awaiting_reboot();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -269,10 +263,10 @@ function UpdatingDeviceState({
|
|||
<div className="w-full max-w-sm space-y-4">
|
||||
<div className="space-y-0">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Updating your device
|
||||
{m.general_update_updating_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Please don{"'"}t turn off your device. This process may take a few minutes.
|
||||
{m.general_update_updating_description()}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="space-y-4 p-4">
|
||||
|
|
@ -281,7 +275,7 @@ function UpdatingDeviceState({
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Rebooting to complete the update...
|
||||
{m.general_update_rebooting()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -297,7 +291,7 @@ function UpdatingDeviceState({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
Linux System Update
|
||||
{m.general_update_system_update_title()}
|
||||
</p>
|
||||
{calculateOverallProgress("system") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
|
|
@ -329,7 +323,7 @@ function UpdatingDeviceState({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
App Update
|
||||
{m.general_update_app_update_title()}
|
||||
</p>
|
||||
{calculateOverallProgress("app") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
|
|
@ -361,7 +355,7 @@ function UpdatingDeviceState({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Update in Background"
|
||||
text={m.general_update_background_button()}
|
||||
onClick={onMinimizeUpgradeDialog}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -381,15 +375,15 @@ function SystemUpToDateState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
System is up to date
|
||||
{m.general_update_up_to_date_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Your system is running the latest version. No updates are currently available.
|
||||
{m.general_update_up_to_date_description()}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Check Again" onClick={checkUpdate} />
|
||||
<Button size="SM" theme="blank" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="light" text={m.general_update_check_again_button()} onClick={checkUpdate} />
|
||||
<Button size="SM" theme="blank" text={m.general_update_back_button()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -409,30 +403,27 @@ function UpdateAvailableState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Update available
|
||||
{m.general_update_available_title()}
|
||||
</p>
|
||||
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
A new update is available to enhance system performance and improve
|
||||
compatibility. We recommend updating to ensure everything runs smoothly.
|
||||
{m.general_update_available_description()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
||||
{versionInfo?.systemUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">System:</span>{" "}
|
||||
{versionInfo?.remote?.systemVersion}
|
||||
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
|
||||
<br />
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.appUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">App:</span>{" "}
|
||||
{versionInfo?.remote?.appVersion}
|
||||
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion}
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
<div className="flex items-center justify-start gap-x-2">
|
||||
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirmUpdate} />
|
||||
<Button size="SM" theme="light" text="Do it later" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirmUpdate} />
|
||||
<Button size="SM" theme="light" text={m.general_update_later_button()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -444,14 +435,13 @@ function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">
|
||||
Update Completed Successfully
|
||||
{m.general_update_completed_title()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
Your device has been successfully updated to the latest version. Enjoy the new
|
||||
features and improvements!
|
||||
{m.general_update_completed_description()}
|
||||
</p>
|
||||
<div className="flex items-center justify-start">
|
||||
<Button size="SM" theme="primary" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.back()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -470,18 +460,18 @@ function UpdateErrorState({
|
|||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">Update Error</p>
|
||||
<p className="text-base font-semibold dark:text-white">{m.general_update_error_title()}</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
An error occurred while updating your device. Please try again later.
|
||||
{m.general_update_error_description()}
|
||||
</p>
|
||||
{errorMessage && (
|
||||
<p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400">
|
||||
Error details: {errorMessage}
|
||||
{m.general_update_error_details({ errorMessage })}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-start gap-x-2">
|
||||
<Button size="SM" theme="light" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="blank" text="Retry" onClick={onRetryUpdate} />
|
||||
<Button size="SM" theme="light" text={m.back()} onClick={onClose} />
|
||||
<Button size="SM" theme="blank" text={m.retry()} onClick={onRetryUpdate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue