mirror of https://github.com/jetkvm/kvm.git
Compare commits
2 Commits
ad188365b2
...
a3aa086e3c
| Author | SHA1 | Date |
|---|---|---|
|
|
a3aa086e3c | |
|
|
c6d4fffca4 |
115
DEVELOPMENT.md
115
DEVELOPMENT.md
|
|
@ -1,23 +1,19 @@
|
||||||
<div align="center">
|
# JetKVM Development Guide
|
||||||
<img alt="JetKVM logo" src="https://jetkvm.com/logo-blue.png" height="28">
|
|
||||||
|
|
||||||
### Development Guide
|
<div align="center" width="100%">
|
||||||
|
<img src="https://jetkvm.com/logo-blue.png" align="center" height="28px">
|
||||||
|
|
||||||
[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs)
|
[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs)
|
||||||
|
|
||||||
[](https://twitter.com/jetkvm)
|
[](https://twitter.com/jetkvm)
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# JetKVM Development Guide
|
|
||||||
|
|
||||||
Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
|
Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- **A JetKVM device** (for full development)
|
- **A JetKVM device** (for full development)
|
||||||
- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)**
|
- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)**
|
||||||
- **[Git](https://git-scm.com/downloads)** for version control
|
- **[Git](https://git-scm.com/downloads)** for version control
|
||||||
|
|
@ -25,9 +21,10 @@ Welcome to JetKVM development! This guide will help you get started quickly, whe
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
**Recommended:** Development is best done on **Linux** or **macOS**.
|
**Recommended:** Development is best done on **Linux** or **macOS**.
|
||||||
|
|
||||||
If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience:
|
If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience:
|
||||||
|
|
||||||
- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install)
|
- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install)
|
||||||
- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment)
|
- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment)
|
||||||
|
|
||||||
|
|
@ -36,12 +33,14 @@ This ensures compatibility with shell scripts and build tools used in the projec
|
||||||
### Project Setup
|
### Project Setup
|
||||||
|
|
||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/jetkvm/kvm.git
|
git clone https://github.com/jetkvm/kvm.git
|
||||||
cd kvm
|
cd kvm
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Check your tools:**
|
2. **Check your tools:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go version && node --version
|
go version && node --version
|
||||||
```
|
```
|
||||||
|
|
@ -49,6 +48,7 @@ This ensures compatibility with shell scripts and build tools used in the projec
|
||||||
3. **Find your JetKVM IP address** (check your router or device screen)
|
3. **Find your JetKVM IP address** (check your router or device screen)
|
||||||
|
|
||||||
4. **Deploy and test:**
|
4. **Deploy and test:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP
|
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP
|
||||||
```
|
```
|
||||||
|
|
@ -95,7 +95,7 @@ tail -f /var/log/jetkvm.log
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
```
|
```plaintext
|
||||||
/kvm/
|
/kvm/
|
||||||
├── main.go # App entry point
|
├── main.go # App entry point
|
||||||
├── config.go # Settings & configuration
|
├── config.go # Settings & configuration
|
||||||
|
|
@ -130,7 +130,7 @@ tail -f /var/log/jetkvm.log
|
||||||
├── components/ # UI components
|
├── components/ # UI components
|
||||||
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
||||||
├── keyboardLayouts/ # Keyboard layout definitions
|
├── keyboardLayouts/ # Keyboard layout definitions
|
||||||
│ ├── paraglide/ # (localization compiled messages output)
|
├── paraglide/ # (localization compiled messages output)
|
||||||
├── providers/ # Feature flags
|
├── providers/ # Feature flags
|
||||||
└── routes/ # Pages (login, settings, etc.)
|
└── routes/ # Pages (login, settings, etc.)
|
||||||
```
|
```
|
||||||
|
|
@ -148,7 +148,7 @@ tail -f /var/log/jetkvm.log
|
||||||
|
|
||||||
### Full Development (Recommended)
|
### Full Development (Recommended)
|
||||||
|
|
||||||
*Best for: Complete feature development*
|
#### _Best for: Complete feature development_
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Deploy everything to your JetKVM device
|
# Deploy everything to your JetKVM device
|
||||||
|
|
@ -157,7 +157,7 @@ tail -f /var/log/jetkvm.log
|
||||||
|
|
||||||
### Frontend Only
|
### Frontend Only
|
||||||
|
|
||||||
*Best for: UI changes without device*
|
#### _Best for: UI changes without device_
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ui
|
cd ui
|
||||||
|
|
@ -171,7 +171,7 @@ Please click the `Build` button in EEZ Studio then run `./dev_deploy.sh -r <YOUR
|
||||||
|
|
||||||
### Quick Backend Changes
|
### Quick Backend Changes
|
||||||
|
|
||||||
*Best for: API or backend logic changes*
|
#### _Best for: API or backend logic changes_
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Skip frontend build for faster deployment
|
# Skip frontend build for faster deployment
|
||||||
|
|
@ -276,6 +276,7 @@ npm install
|
||||||
### "Device UI Fails to Build"
|
### "Device UI Fails to Build"
|
||||||
|
|
||||||
If while trying to build you run into an error message similar to :
|
If while trying to build you run into an error message similar to :
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
|
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
|
||||||
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
|
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
|
||||||
|
|
@ -283,17 +284,21 @@ In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
|
||||||
^~~~~~~~~
|
^~~~~~~~~
|
||||||
compilation terminated.
|
compilation terminated.
|
||||||
```
|
```
|
||||||
|
|
||||||
This means that your system didn't create the directory-link to from _./internal/native/cgo/ui_ to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if _./internal/native/cgo/ui_ appears as a plain text file with only the textual contents:
|
This means that your system didn't create the directory-link to from _./internal/native/cgo/ui_ to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if _./internal/native/cgo/ui_ appears as a plain text file with only the textual contents:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
../eez/src/ui
|
../eez/src/ui
|
||||||
```
|
```
|
||||||
|
|
||||||
If this happens to you need to [enable git creation of symbolic links](https://stackoverflow.com/a/59761201/2076) either globally or for the KVM repository:
|
If this happens to you need to [enable git creation of symbolic links](https://stackoverflow.com/a/59761201/2076) either globally or for the KVM repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Globally enable git to create symlinks
|
# Globally enable git to create symlinks
|
||||||
git config --global core.symlinks true
|
git config --global core.symlinks true
|
||||||
git restore internal/native/cgo/ui
|
git restore internal/native/cgo/ui
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable git to create symlinks only in this project
|
# Enable git to create symlinks only in this project
|
||||||
git config core.symlinks true
|
git config core.symlinks true
|
||||||
|
|
@ -301,13 +306,15 @@ If this happens to you need to [enable git creation of symbolic links](https://s
|
||||||
```
|
```
|
||||||
|
|
||||||
Or if you want to manually create the symlink use:
|
Or if you want to manually create the symlink use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# linux
|
# linux
|
||||||
cd internal/native/cgo
|
cd internal/native/cgo
|
||||||
rm ui
|
rm ui
|
||||||
ln -s ../eez/src/ui ui
|
ln -s ../eez/src/ui ui
|
||||||
```
|
```
|
||||||
```dos
|
|
||||||
|
```batch
|
||||||
rem Windows
|
rem Windows
|
||||||
cd internal/native/cgo
|
cd internal/native/cgo
|
||||||
del ui
|
del ui
|
||||||
|
|
@ -330,6 +337,7 @@ Or if you want to manually create the symlink use:
|
||||||
- **Go:** Follow standard Go conventions
|
- **Go:** Follow standard Go conventions
|
||||||
- **TypeScript:** Use TypeScript for type safety
|
- **TypeScript:** Use TypeScript for type safety
|
||||||
- **React:** Keep components small and reusable
|
- **React:** Keep components small and reusable
|
||||||
|
- **Localization:** Ensure all user-facing strings in the frontend are [localized](#localization)
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
|
|
@ -362,11 +370,12 @@ export JETKVM_PROXY_URL="ws://<IP>"
|
||||||
4. Test thoroughly
|
4. Test thoroughly
|
||||||
5. Submit a pull request
|
5. Submit a pull request
|
||||||
|
|
||||||
### Before submitting:
|
### Before submitting
|
||||||
|
|
||||||
- [ ] Code works on device
|
- [ ] Code works on device
|
||||||
- [ ] Tests pass
|
- [ ] Tests pass
|
||||||
- [ ] Code follows style guidelines
|
- [ ] Code follows style guidelines
|
||||||
|
- [ ] Frontend user-facing strings [localized](#localization)
|
||||||
- [ ] Documentation updated (if needed)
|
- [ ] Documentation updated (if needed)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -422,7 +431,6 @@ The application uses a JSON configuration file stored at `/userdata/kvm_config.j
|
||||||
|
|
||||||
3. **Add migration logic if needed for existing installations**
|
3. **Add migration logic if needed for existing installations**
|
||||||
|
|
||||||
|
|
||||||
### LVGL Build
|
### LVGL Build
|
||||||
|
|
||||||
We modified the LVGL code a little bit to remove unused fonts and examples.
|
We modified the LVGL code a little bit to remove unused fonts and examples.
|
||||||
|
|
@ -433,6 +441,79 @@ git diff --cached --diff-filter=d > ../internal/native/cgo/lvgl-minify.patch &&
|
||||||
git diff --name-only --diff-filter=D --cached > ../internal/native/cgo/lvgl-minify.del
|
git diff --name-only --diff-filter=D --cached > ../internal/native/cgo/lvgl-minify.del
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
The browser/client frontend uses the [paraglide-js](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) plug-in from the [inlang.com](https://inlang.com/) project to allow compile-time validated localization of all user-facing UI strings in the browser/client UI. This includes `title`, `text`, `name`, `description`, `placeholder`, `label`, `aria-label`, _message attributes_ (such as `confirmText`, `unit`, `badge`, `tag`, or `flag`), HTML _element text_ (such as `<h?>`, `<span>`, or `<p>` elements), _notifications messages_, and option _label_ strings, etc.
|
||||||
|
|
||||||
|
We **do not** translate the console log messages, CSS class names, theme names, nor the various _value_ strings (e.g. for value/label pair options), nor URL routes.
|
||||||
|
|
||||||
|
The localizations are stored in _.json_ files in the `ui/localizations/messages` directory, with one language-per-file using the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (e.g. en for English, de for German, etc.)
|
||||||
|
|
||||||
|
#### m-function-matcher
|
||||||
|
|
||||||
|
The translations are extracted into language files (e.g. _en.json_ for English) and then paraglide-js compiles them into helpers for use with the [m-function-matcher](https://inlang.com/m/632iow21/plugin-inlang-mFunctionMatcher). An example:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SettingsPageHeader
|
||||||
|
title={m.extensions_atx_power_control()}
|
||||||
|
description={m.extensions_atx_power_control_description()}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### shakespere plug-in
|
||||||
|
|
||||||
|
If you enable the [Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) plug-in, the localized text "tooltip" is shown in the VSCode editor after any localized text in the language you've selected for preview. In this image, it's the blue text at the end of the line :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Process
|
||||||
|
|
||||||
|
##### Localizing a UI
|
||||||
|
|
||||||
|
1. Locate a string that is visible to the end user on the client/browser
|
||||||
|
2. Assign that string a "key" that reflects the logical meaning of the string in snake-case (look at existing localizations for examples), for example if there's a string `This is a test` on the _thing edit page_ it would be "thing_edit_this_is_a_test"
|
||||||
|
|
||||||
|
```json
|
||||||
|
"thing_edit_this_is_a_test": "This is a test",
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add the key and string to the _en.json_ like this:
|
||||||
|
|
||||||
|
- **Note** if the string has replacement parameters (line a user-entered name), the syntax for the localized string has `{ }` around the replacement token (e.g. _This is your name: {name}_). An complex example:
|
||||||
|
|
||||||
|
```react
|
||||||
|
{m.mount_button_showing_results({
|
||||||
|
from: indexOfFirstFile + 1,
|
||||||
|
to: Math.min(indexOfLastFile, onStorageFiles.length),
|
||||||
|
total: onStorageFiles.length
|
||||||
|
})}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Save the _en.json_ file and execute `npm run i18n` to resort the language files, validate the translations, and create the m-functions
|
||||||
|
5. Edit the _.tsx_ file and replace the string with the new m-function which will be the key-string you chose in snake-case. For example `This is a test` in _thing edit page_ turns into `m.thing_edit_this_is_a_test()`
|
||||||
|
- **Note** if the string has a replacement token, supply that to the m-function, for example `m.profile_your_name({ name: edit.value })`
|
||||||
|
6. When all your strings are extracted, run `npm run i18n:machine-translate` to get a first-stab at the translations for the other supported languages.
|
||||||
|
|
||||||
|
### Adding a new language
|
||||||
|
|
||||||
|
1. Get the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (for example AT for Austria)
|
||||||
|
2. Create a new file in the _ui/localization/messages_ directory (example _at.json_)
|
||||||
|
3. Add the new country code to the _ui/localizations/settings.json_ file in both the `"locales"` and the `"languageTags"` section (inlang and Sherlock aren't exactly current to each other, so we need it in both places).
|
||||||
|
4. That file also declares the baseLocale/sourceLanguageTag which is `"en"` because this project started out in English. Do NOT change that.
|
||||||
|
5. Run `npm run i18n:machine-translate` to do an initial pass at localizing all existing messages to the new language.
|
||||||
|
- **Note** you will get an error _DB has been closed_, ignore that message, we're not using a database.
|
||||||
|
- **Note** you likely will get errors while running this command due to rate limits and such (it uses anonymous Google Translate). Just keep running the command over and over... it'll translate a bunch each time until it says _Machine translate complete_
|
||||||
|
|
||||||
|
### Other notes
|
||||||
|
|
||||||
|
- Run `npm run i18n:validate` to ensure that language files and settings are well-formed.
|
||||||
|
- Run `npm run i18n:find-excess` to look for extra keys in other language files that have been deleted from the master-list in _en.json_.
|
||||||
|
- Run `npm run i18n:find-dupes` to look for multiple keys in _en.json_ that have the same translated value (this is normal)
|
||||||
|
- Run `npm run i18n:find-unused` to look for keys in _en.json_ that are not referenced in the UI anywhere.
|
||||||
|
- **Note** there are a few that are not currently used, only concern yourself with ones you obsoleted.
|
||||||
|
- Run `npm run i18n:audit` to do all the above checks.
|
||||||
|
- Using [inlang CLI](https://inlang.com/m/2qj2w8pu/app-inlang-cli) to support the npm commands.
|
||||||
|
- You can install the [Sherlock VS Code extension](https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension) in your devcontainer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Spænding",
|
"dc_power_control_voltage": "Spænding",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Slet",
|
"delete": "Slet",
|
||||||
"deregister_button": "Afregistrering fra Cloud",
|
"deregister_from_cloud": "Afregistrering fra Cloud",
|
||||||
"deregister_cloud_devices": "Cloud-enheder",
|
"deregister_cloud_devices": "Cloud-enheder",
|
||||||
"deregister_description": "Dette vil fjerne enheden fra din cloud-konto og tilbagekalde fjernadgang til den. Bemærk venligst, at lokal adgang stadig vil være mulig.",
|
"deregister_description": "Dette vil fjerne enheden fra din cloud-konto og tilbagekalde fjernadgang til den. Bemærk venligst, at lokal adgang stadig vil være mulig.",
|
||||||
"deregister_error": "Der opstod en fejl {status} under afregistreringen af din enhed. Prøv igen.",
|
"deregister_error": "Der opstod en fejl {status} under afregistreringen af din enhed. Prøv igen.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient der skal bruges",
|
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient der skal bruges",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM Intern",
|
"network_dhcp_client_jetkvm": "JetKVM Intern",
|
||||||
"network_dhcp_client_title": "DHCP-klient",
|
"network_dhcp_client_title": "DHCP-klient",
|
||||||
"network_dhcp_lease_renew": "Forny DHCP-lease",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Forny lejekontrakt",
|
"network_dhcp_lease_renew_confirm": "Forny lejekontrakt",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Dette vil anmode om en ny IP-adresse fra din DHCP-server. Din enhed kan midlertidigt miste netværksforbindelsen under denne proces.",
|
"network_dhcp_lease_renew_confirm_description": "Dette vil anmode om en ny IP-adresse fra din DHCP-server. Din enhed kan midlertidigt miste netværksforbindelsen under denne proces.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Hvis du modtager en ny IP-adresse",
|
"network_dhcp_lease_renew_confirm_new_a": "Hvis du modtager en ny IP-adresse",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Enhedsnavn",
|
"register_device_name_label": "Enhedsnavn",
|
||||||
"register_device_name_placeholder": "Plex-medieserver",
|
"register_device_name_placeholder": "Plex-medieserver",
|
||||||
"register_device_no_name": "Angiv venligst et navn",
|
"register_device_no_name": "Angiv venligst et navn",
|
||||||
"rename_device_button": "Omdøb enhed",
|
"rename_device": "Omdøb enhed",
|
||||||
"rename_device_description": "Navngiv din enhed korrekt, så den nemt kan identificeres.",
|
"rename_device_description": "Navngiv din enhed korrekt, så den nemt kan identificeres.",
|
||||||
"rename_device_error": "Der opstod en fejl {error} under omdøbning af din enhed.",
|
"rename_device_error": "Der opstod en fejl {error} under omdøbning af din enhed.",
|
||||||
"rename_device_headline": "Omdøb {name}",
|
"rename_device_headline": "Omdøb {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Stromspannung",
|
"dc_power_control_voltage": "Stromspannung",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"deregister_button": "Abmelden von der Cloud",
|
"deregister_from_cloud": "Abmelden von der Cloud",
|
||||||
"deregister_cloud_devices": "Cloud-Geräte",
|
"deregister_cloud_devices": "Cloud-Geräte",
|
||||||
"deregister_description": "Dadurch wird das Gerät aus Ihrem Cloud-Konto entfernt und der Fernzugriff darauf widerrufen. Bitte beachten Sie, dass der lokale Zugriff weiterhin möglich ist.",
|
"deregister_description": "Dadurch wird das Gerät aus Ihrem Cloud-Konto entfernt und der Fernzugriff darauf widerrufen. Bitte beachten Sie, dass der lokale Zugriff weiterhin möglich ist.",
|
||||||
"deregister_error": "Beim Abmelden Ihres Geräts ist ein Fehler aufgetreten {status} . Bitte versuchen Sie es erneut.",
|
"deregister_error": "Beim Abmelden Ihres Geräts ist ein Fehler aufgetreten {status} . Bitte versuchen Sie es erneut.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Konfigurieren Sie, welcher DHCP-Client verwendet werden soll",
|
"network_dhcp_client_description": "Konfigurieren Sie, welcher DHCP-Client verwendet werden soll",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM intern",
|
"network_dhcp_client_jetkvm": "JetKVM intern",
|
||||||
"network_dhcp_client_title": "DHCP-Client",
|
"network_dhcp_client_title": "DHCP-Client",
|
||||||
"network_dhcp_lease_renew": "DHCP-Lease erneuern",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Mietvertrag verlängern",
|
"network_dhcp_lease_renew_confirm": "Mietvertrag verlängern",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Dadurch wird eine neue IP-Adresse von Ihrem DHCP-Server angefordert. Während dieses Vorgangs kann die Netzwerkverbindung Ihres Geräts vorübergehend unterbrochen werden.",
|
"network_dhcp_lease_renew_confirm_description": "Dadurch wird eine neue IP-Adresse von Ihrem DHCP-Server angefordert. Während dieses Vorgangs kann die Netzwerkverbindung Ihres Geräts vorübergehend unterbrochen werden.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Wenn Sie eine neue IP-Adresse erhalten",
|
"network_dhcp_lease_renew_confirm_new_a": "Wenn Sie eine neue IP-Adresse erhalten",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Gerätename",
|
"register_device_name_label": "Gerätename",
|
||||||
"register_device_name_placeholder": "Plex Media Server",
|
"register_device_name_placeholder": "Plex Media Server",
|
||||||
"register_device_no_name": "Bitte geben Sie einen Namen an",
|
"register_device_no_name": "Bitte geben Sie einen Namen an",
|
||||||
"rename_device_button": "Gerät umbenennen",
|
"rename_device": "Gerät umbenennen",
|
||||||
"rename_device_description": "Geben Sie Ihrem Gerät einen passenden Namen, damit es leicht identifiziert werden kann.",
|
"rename_device_description": "Geben Sie Ihrem Gerät einen passenden Namen, damit es leicht identifiziert werden kann.",
|
||||||
"rename_device_error": "Beim Umbenennen Ihres Geräts ist ein Fehler {error} aufgetreten.",
|
"rename_device_error": "Beim Umbenennen Ihres Geräts ist ein Fehler {error} aufgetreten.",
|
||||||
"rename_device_headline": "Umbenennen {name}",
|
"rename_device_headline": "Umbenennen {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Voltage",
|
"dc_power_control_voltage": "Voltage",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deregister_button": "Deregister from Cloud",
|
"deregister_from_cloud": "Deregister from Cloud",
|
||||||
"deregister_cloud_devices": "Cloud Devices",
|
"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_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_error": "There was an error {status} deregistering your device. Please try again.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Configure which DHCP client to use",
|
"network_dhcp_client_description": "Configure which DHCP client to use",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM Internal",
|
"network_dhcp_client_jetkvm": "JetKVM Internal",
|
||||||
"network_dhcp_client_title": "DHCP Client",
|
"network_dhcp_client_title": "DHCP Client",
|
||||||
"network_dhcp_lease_renew": "Renew DHCP Lease",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Renew Lease",
|
"network_dhcp_lease_renew_confirm": "Renew Lease",
|
||||||
"network_dhcp_lease_renew_confirm_description": "This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process.",
|
"network_dhcp_lease_renew_confirm_description": "This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "If you receive a new IP address",
|
"network_dhcp_lease_renew_confirm_new_a": "If you receive a new IP address",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Device Name",
|
"register_device_name_label": "Device Name",
|
||||||
"register_device_name_placeholder": "Plex Media Server",
|
"register_device_name_placeholder": "Plex Media Server",
|
||||||
"register_device_no_name": "Please specify a name",
|
"register_device_no_name": "Please specify a name",
|
||||||
"rename_device_button": "Rename Device",
|
"rename_device": "Rename Device",
|
||||||
"rename_device_description": "Properly name your device to easily identify it.",
|
"rename_device_description": "Properly name your device to easily identify it.",
|
||||||
"rename_device_error": "There was an error {error} renaming your device.",
|
"rename_device_error": "There was an error {error} renaming your device.",
|
||||||
"rename_device_headline": "Rename {name}",
|
"rename_device_headline": "Rename {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Voltaje",
|
"dc_power_control_voltage": "Voltaje",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"deregister_button": "Darse de baja de la nube",
|
"deregister_from_cloud": "Darse de baja de la nube",
|
||||||
"deregister_cloud_devices": "Dispositivos en la nube",
|
"deregister_cloud_devices": "Dispositivos en la nube",
|
||||||
"deregister_description": "Esto eliminará el dispositivo de su cuenta en la nube y revocará el acceso remoto. Tenga en cuenta que el acceso local seguirá siendo posible.",
|
"deregister_description": "Esto eliminará el dispositivo de su cuenta en la nube y revocará el acceso remoto. Tenga en cuenta que el acceso local seguirá siendo posible.",
|
||||||
"deregister_error": "Se produjo un error {status} al cancelar el registro de su dispositivo. Inténtelo de nuevo.",
|
"deregister_error": "Se produjo un error {status} al cancelar el registro de su dispositivo. Inténtelo de nuevo.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Configurar qué cliente DHCP utilizar",
|
"network_dhcp_client_description": "Configurar qué cliente DHCP utilizar",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM interno",
|
"network_dhcp_client_jetkvm": "JetKVM interno",
|
||||||
"network_dhcp_client_title": "Cliente DHCP",
|
"network_dhcp_client_title": "Cliente DHCP",
|
||||||
"network_dhcp_lease_renew": "Renovar la concesión de DHCP",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Renovar el contrato de arrendamiento",
|
"network_dhcp_lease_renew_confirm": "Renovar el contrato de arrendamiento",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Esto solicitará una nueva dirección IP a su servidor DHCP. Es posible que su dispositivo pierda temporalmente la conexión a la red durante este proceso.",
|
"network_dhcp_lease_renew_confirm_description": "Esto solicitará una nueva dirección IP a su servidor DHCP. Es posible que su dispositivo pierda temporalmente la conexión a la red durante este proceso.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Si recibe una nueva dirección IP",
|
"network_dhcp_lease_renew_confirm_new_a": "Si recibe una nueva dirección IP",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Nombre del dispositivo",
|
"register_device_name_label": "Nombre del dispositivo",
|
||||||
"register_device_name_placeholder": "Servidor multimedia Plex",
|
"register_device_name_placeholder": "Servidor multimedia Plex",
|
||||||
"register_device_no_name": "Por favor especifique un nombre",
|
"register_device_no_name": "Por favor especifique un nombre",
|
||||||
"rename_device_button": "Cambiar el nombre del dispositivo",
|
"rename_device": "Cambiar el nombre del dispositivo",
|
||||||
"rename_device_description": "Nombra correctamente tu dispositivo para identificarlo fácilmente.",
|
"rename_device_description": "Nombra correctamente tu dispositivo para identificarlo fácilmente.",
|
||||||
"rename_device_error": "Se produjo un error {error} al cambiar el nombre de su dispositivo.",
|
"rename_device_error": "Se produjo un error {error} al cambiar el nombre de su dispositivo.",
|
||||||
"rename_device_headline": "Cambiar nombre {name}",
|
"rename_device_headline": "Cambiar nombre {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Tension",
|
"dc_power_control_voltage": "Tension",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"deregister_button": "Se désinscrire du Cloud",
|
"deregister_from_cloud": "Se désinscrire du Cloud",
|
||||||
"deregister_cloud_devices": "Appareils Cloud",
|
"deregister_cloud_devices": "Appareils Cloud",
|
||||||
"deregister_description": "Cela supprimera l'appareil de votre compte cloud et révoquera l'accès à distance. Veuillez noter que l'accès local restera possible.",
|
"deregister_description": "Cela supprimera l'appareil de votre compte cloud et révoquera l'accès à distance. Veuillez noter que l'accès local restera possible.",
|
||||||
"deregister_error": "Une erreur {status} s'est produite lors de l'annulation de l'enregistrement de votre appareil. Veuillez réessayer.",
|
"deregister_error": "Une erreur {status} s'est produite lors de l'annulation de l'enregistrement de votre appareil. Veuillez réessayer.",
|
||||||
|
|
@ -581,8 +581,6 @@
|
||||||
"network_description": "Configurez vos paramètres réseau",
|
"network_description": "Configurez vos paramètres réseau",
|
||||||
"network_dhcp_client_description": "Configurer le client DHCP à utiliser",
|
"network_dhcp_client_description": "Configurer le client DHCP à utiliser",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM interne",
|
"network_dhcp_client_jetkvm": "JetKVM interne",
|
||||||
"network_dhcp_client_title": "Client DHCP",
|
|
||||||
"network_dhcp_lease_renew": "Renouveler le bail DHCP",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Renouveler le bail",
|
"network_dhcp_lease_renew_confirm": "Renouveler le bail",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Cette opération demandera une nouvelle adresse IP à votre serveur DHCP. Votre appareil pourrait perdre temporairement sa connectivité réseau pendant cette opération.",
|
"network_dhcp_lease_renew_confirm_description": "Cette opération demandera une nouvelle adresse IP à votre serveur DHCP. Votre appareil pourrait perdre temporairement sa connectivité réseau pendant cette opération.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Si vous recevez une nouvelle adresse IP",
|
"network_dhcp_lease_renew_confirm_new_a": "Si vous recevez une nouvelle adresse IP",
|
||||||
|
|
@ -700,7 +698,7 @@
|
||||||
"register_device_name_label": "Nom de l'appareil",
|
"register_device_name_label": "Nom de l'appareil",
|
||||||
"register_device_name_placeholder": "Serveur multimédia Plex",
|
"register_device_name_placeholder": "Serveur multimédia Plex",
|
||||||
"register_device_no_name": "Veuillez spécifier un nom",
|
"register_device_no_name": "Veuillez spécifier un nom",
|
||||||
"rename_device_button": "Renommer l'appareil",
|
"rename_device": "Renommer l'appareil",
|
||||||
"rename_device_description": "Nommez correctement votre appareil pour l'identifier facilement.",
|
"rename_device_description": "Nommez correctement votre appareil pour l'identifier facilement.",
|
||||||
"rename_device_error": "Une erreur {error} s'est produite lors du changement de nom de votre appareil.",
|
"rename_device_error": "Une erreur {error} s'est produite lors du changement de nom de votre appareil.",
|
||||||
"rename_device_headline": "Renommer {name}",
|
"rename_device_headline": "Renommer {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Voltaggio",
|
"dc_power_control_voltage": "Voltaggio",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Eliminare",
|
"delete": "Eliminare",
|
||||||
"deregister_button": "Annulla registrazione dal cloud",
|
"deregister_from_cloud": "Annulla registrazione dal cloud",
|
||||||
"deregister_cloud_devices": "Dispositivi cloud",
|
"deregister_cloud_devices": "Dispositivi cloud",
|
||||||
"deregister_description": "Questo rimuoverà il dispositivo dal tuo account cloud e ne revocherà l'accesso remoto. Tieni presente che l'accesso locale sarà comunque possibile.",
|
"deregister_description": "Questo rimuoverà il dispositivo dal tuo account cloud e ne revocherà l'accesso remoto. Tieni presente che l'accesso locale sarà comunque possibile.",
|
||||||
"deregister_error": "Si è verificato un errore {status} durante l'annullamento della registrazione del dispositivo. Riprova.",
|
"deregister_error": "Si è verificato un errore {status} durante l'annullamento della registrazione del dispositivo. Riprova.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Configurare quale client DHCP utilizzare",
|
"network_dhcp_client_description": "Configurare quale client DHCP utilizzare",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM interno",
|
"network_dhcp_client_jetkvm": "JetKVM interno",
|
||||||
"network_dhcp_client_title": "Cliente DHCP",
|
"network_dhcp_client_title": "Cliente DHCP",
|
||||||
"network_dhcp_lease_renew": "Rinnova il contratto di locazione DHCP",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Rinnovare il contratto di locazione",
|
"network_dhcp_lease_renew_confirm": "Rinnovare il contratto di locazione",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Verrà richiesto un nuovo indirizzo IP al server DHCP. Durante questo processo, il dispositivo potrebbe perdere temporaneamente la connettività di rete.",
|
"network_dhcp_lease_renew_confirm_description": "Verrà richiesto un nuovo indirizzo IP al server DHCP. Durante questo processo, il dispositivo potrebbe perdere temporaneamente la connettività di rete.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Se ricevi un nuovo indirizzo IP",
|
"network_dhcp_lease_renew_confirm_new_a": "Se ricevi un nuovo indirizzo IP",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Nome del dispositivo",
|
"register_device_name_label": "Nome del dispositivo",
|
||||||
"register_device_name_placeholder": "Server multimediale Plex",
|
"register_device_name_placeholder": "Server multimediale Plex",
|
||||||
"register_device_no_name": "Si prega di specificare un nome",
|
"register_device_no_name": "Si prega di specificare un nome",
|
||||||
"rename_device_button": "Rinomina dispositivo",
|
"rename_device": "Rinomina dispositivo",
|
||||||
"rename_device_description": "Assegna un nome appropriato al tuo dispositivo per identificarlo facilmente.",
|
"rename_device_description": "Assegna un nome appropriato al tuo dispositivo per identificarlo facilmente.",
|
||||||
"rename_device_error": "Si è verificato un errore {error} durante la ridenominazione del dispositivo.",
|
"rename_device_error": "Si è verificato un errore {error} durante la ridenominazione del dispositivo.",
|
||||||
"rename_device_headline": "Rinomina {name}",
|
"rename_device_headline": "Rinomina {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Spenning",
|
"dc_power_control_voltage": "Spenning",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Slett",
|
"delete": "Slett",
|
||||||
"deregister_button": "Avregistrer deg fra skyen",
|
"deregister_from_cloud": "Avregistrer deg fra skyen",
|
||||||
"deregister_cloud_devices": "Skyenheter",
|
"deregister_cloud_devices": "Skyenheter",
|
||||||
"deregister_description": "Dette vil fjerne enheten fra skykontoen din og oppheve ekstern tilgang til den. Vær oppmerksom på at lokal tilgang fortsatt vil være mulig.",
|
"deregister_description": "Dette vil fjerne enheten fra skykontoen din og oppheve ekstern tilgang til den. Vær oppmerksom på at lokal tilgang fortsatt vil være mulig.",
|
||||||
"deregister_error": "Det oppsto en feil {status} enheten din skulle avregistreres. Prøv på nytt.",
|
"deregister_error": "Det oppsto en feil {status} enheten din skulle avregistreres. Prøv på nytt.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient som skal brukes",
|
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient som skal brukes",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM intern",
|
"network_dhcp_client_jetkvm": "JetKVM intern",
|
||||||
"network_dhcp_client_title": "DHCP-klient",
|
"network_dhcp_client_title": "DHCP-klient",
|
||||||
"network_dhcp_lease_renew": "Forny DHCP-leieavtale",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Forny leieavtalen",
|
"network_dhcp_lease_renew_confirm": "Forny leieavtalen",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Dette vil be om en ny IP-adresse fra DHCP-serveren din. Enheten din kan midlertidig miste nettverkstilkoblingen under denne prosessen.",
|
"network_dhcp_lease_renew_confirm_description": "Dette vil be om en ny IP-adresse fra DHCP-serveren din. Enheten din kan midlertidig miste nettverkstilkoblingen under denne prosessen.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Hvis du får en ny IP-adresse",
|
"network_dhcp_lease_renew_confirm_new_a": "Hvis du får en ny IP-adresse",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Enhetsnavn",
|
"register_device_name_label": "Enhetsnavn",
|
||||||
"register_device_name_placeholder": "Plex Media Server",
|
"register_device_name_placeholder": "Plex Media Server",
|
||||||
"register_device_no_name": "Vennligst oppgi et navn",
|
"register_device_no_name": "Vennligst oppgi et navn",
|
||||||
"rename_device_button": "Gi nytt navn til enheten",
|
"rename_device": "Gi nytt navn til enheten",
|
||||||
"rename_device_description": "Gi enheten din riktig navn slik at du enkelt kan identifisere den.",
|
"rename_device_description": "Gi enheten din riktig navn slik at du enkelt kan identifisere den.",
|
||||||
"rename_device_error": "Det oppsto en feil {error} enheten skulle gis nytt navn.",
|
"rename_device_error": "Det oppsto en feil {error} enheten skulle gis nytt navn.",
|
||||||
"rename_device_headline": "Gi nytt navn til {name}",
|
"rename_device_headline": "Gi nytt navn til {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "Spänning",
|
"dc_power_control_voltage": "Spänning",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "Radera",
|
"delete": "Radera",
|
||||||
"deregister_button": "Avregistrera dig från molnet",
|
"deregister_from_cloud": "Avregistrera dig från molnet",
|
||||||
"deregister_cloud_devices": "Molnenheter",
|
"deregister_cloud_devices": "Molnenheter",
|
||||||
"deregister_description": "Detta kommer att ta bort enheten från ditt molnkonto och återkalla fjärråtkomst till den. Observera att lokal åtkomst fortfarande kommer att vara möjlig.",
|
"deregister_description": "Detta kommer att ta bort enheten från ditt molnkonto och återkalla fjärråtkomst till den. Observera att lokal åtkomst fortfarande kommer att vara möjlig.",
|
||||||
"deregister_error": "Det uppstod ett fel {status} enheten avregistrerades. Försök igen.",
|
"deregister_error": "Det uppstod ett fel {status} enheten avregistrerades. Försök igen.",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "Konfigurera vilken DHCP-klient som ska användas",
|
"network_dhcp_client_description": "Konfigurera vilken DHCP-klient som ska användas",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM Intern",
|
"network_dhcp_client_jetkvm": "JetKVM Intern",
|
||||||
"network_dhcp_client_title": "DHCP-klient",
|
"network_dhcp_client_title": "DHCP-klient",
|
||||||
"network_dhcp_lease_renew": "Förnya DHCP-lease",
|
|
||||||
"network_dhcp_lease_renew_confirm": "Förnya hyresavtalet",
|
"network_dhcp_lease_renew_confirm": "Förnya hyresavtalet",
|
||||||
"network_dhcp_lease_renew_confirm_description": "Detta kommer att begära en ny IP-adress från din DHCP-server. Din enhet kan tillfälligt förlora nätverksanslutningen under denna process.",
|
"network_dhcp_lease_renew_confirm_description": "Detta kommer att begära en ny IP-adress från din DHCP-server. Din enhet kan tillfälligt förlora nätverksanslutningen under denna process.",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "Om du får en ny IP-adress",
|
"network_dhcp_lease_renew_confirm_new_a": "Om du får en ny IP-adress",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "Enhetsnamn",
|
"register_device_name_label": "Enhetsnamn",
|
||||||
"register_device_name_placeholder": "Plex Media Server",
|
"register_device_name_placeholder": "Plex Media Server",
|
||||||
"register_device_no_name": "Vänligen ange ett namn",
|
"register_device_no_name": "Vänligen ange ett namn",
|
||||||
"rename_device_button": "Byt namn på enhet",
|
"rename_device": "Byt namn på enhet",
|
||||||
"rename_device_description": "Namnge din enhet korrekt för att enkelt kunna identifiera den.",
|
"rename_device_description": "Namnge din enhet korrekt för att enkelt kunna identifiera den.",
|
||||||
"rename_device_error": "Det uppstod ett fel {error} enheten döptes om.",
|
"rename_device_error": "Det uppstod ett fel {error} enheten döptes om.",
|
||||||
"rename_device_headline": "Byt namn på {name}",
|
"rename_device_headline": "Byt namn på {name}",
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
"dc_power_control_voltage": "电压",
|
"dc_power_control_voltage": "电压",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"deregister_button": "从云端注销",
|
"deregister_from_cloud": "从云端注销",
|
||||||
"deregister_cloud_devices": "云设备",
|
"deregister_cloud_devices": "云设备",
|
||||||
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
|
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
|
||||||
"deregister_error": "注销您的设备时出现错误{status} 。请重试。",
|
"deregister_error": "注销您的设备时出现错误{status} 。请重试。",
|
||||||
|
|
@ -582,7 +582,6 @@
|
||||||
"network_dhcp_client_description": "配置要使用的 DHCP 客户端",
|
"network_dhcp_client_description": "配置要使用的 DHCP 客户端",
|
||||||
"network_dhcp_client_jetkvm": "JetKVM 内部",
|
"network_dhcp_client_jetkvm": "JetKVM 内部",
|
||||||
"network_dhcp_client_title": "DHCP客户端",
|
"network_dhcp_client_title": "DHCP客户端",
|
||||||
"network_dhcp_lease_renew": "续订 DHCP 租约",
|
|
||||||
"network_dhcp_lease_renew_confirm": "续租",
|
"network_dhcp_lease_renew_confirm": "续租",
|
||||||
"network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。",
|
"network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。",
|
||||||
"network_dhcp_lease_renew_confirm_new_a": "如果您收到新的 IP 地址",
|
"network_dhcp_lease_renew_confirm_new_a": "如果您收到新的 IP 地址",
|
||||||
|
|
@ -700,7 +699,7 @@
|
||||||
"register_device_name_label": "设备名称",
|
"register_device_name_label": "设备名称",
|
||||||
"register_device_name_placeholder": "Plex媒体服务器",
|
"register_device_name_placeholder": "Plex媒体服务器",
|
||||||
"register_device_no_name": "请指定名称",
|
"register_device_no_name": "请指定名称",
|
||||||
"rename_device_button": "重命名设备",
|
"rename_device": "重命名设备",
|
||||||
"rename_device_description": "正确命名您的设备以便轻松识别它。",
|
"rename_device_description": "正确命名您的设备以便轻松识别它。",
|
||||||
"rename_device_error": "重命名您的设备时出现错误{error} 。",
|
"rename_device_error": "重命名您的设备时出现错误{error} 。",
|
||||||
"rename_device_headline": "重命名{name}",
|
"rename_device_headline": "重命名{name}",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "^2.2.9",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.2",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
|
@ -1389,9 +1388,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.1.tgz",
|
||||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
"integrity": "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
|
@ -2435,12 +2434,6 @@
|
||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/uuid": {
|
|
||||||
"version": "10.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
|
||||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/validator": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.15.3",
|
"version": "13.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
|
||||||
|
|
@ -3093,9 +3086,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.16",
|
"version": "2.8.17",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz",
|
||||||
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
|
"integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -3217,9 +3210,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001750",
|
"version": "1.0.30001751",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||||
"integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
|
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -5986,9 +5979,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.23",
|
"version": "2.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz",
|
||||||
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
|
"integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,17 @@
|
||||||
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
||||||
"dev:cloud": "vite dev --mode=cloud-development",
|
"dev:cloud": "vite dev --mode=cloud-development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "npm run paraglide && tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "npm run i18n:compile && tsc && vite build --mode=device --emptyOutDir",
|
||||||
"build:staging": "npm run paraglide && tsc && vite build --mode=cloud-staging",
|
"build:staging": "npm run i18n:compile && tsc && vite build --mode=cloud-staging",
|
||||||
"build:prod": "npm run paraglide && tsc && vite build --mode=cloud-production",
|
"build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production",
|
||||||
"lint": "npm run paraglide && eslint './src/**/*.{ts,tsx}'",
|
"lint": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}'",
|
||||||
"lint:fix": "npm run paraglide && eslint './src/**/*.{ts,tsx}' --fix",
|
"lint:fix": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}' --fix",
|
||||||
"i18n": "npm run i18n:resort && npm run i18n:validate && npm run i18n:compile",
|
"i18n": "npm run i18n:resort && npm run i18n:validate && npm run i18n:compile",
|
||||||
"i18n:resort": "python3 tools/resort_messages.py",
|
"i18n:resort": "python3 tools/resort_messages.py",
|
||||||
"i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
|
"i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
|
||||||
"i18n:compile": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
|
"i18n:compile": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
|
||||||
"i18n:machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang",
|
"i18n:machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang",
|
||||||
|
"i18n:audit": "npm run i18n:find-dupes && npm i18n:find-excess && npm run i18n:find-unused",
|
||||||
"i18n:find-excess": "python3 ./tools/find_excess_messages.py",
|
"i18n:find-excess": "python3 ./tools/find_excess_messages.py",
|
||||||
"i18n:find-unused": "python3 ./tools/find_unused_messages.py",
|
"i18n:find-unused": "python3 ./tools/find_unused_messages.py",
|
||||||
"i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py"
|
"i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py"
|
||||||
|
|
@ -29,7 +30,6 @@
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "^2.2.9",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.2",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export default function KvmCard({
|
||||||
className="block w-full py-1.5 text-black dark:text-white"
|
className="block w-full py-1.5 text-black dark:text-white"
|
||||||
to={`./${id}/rename`}
|
to={`./${id}/rename`}
|
||||||
>
|
>
|
||||||
Rename
|
{m.rename_device()}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -135,7 +135,7 @@ export default function KvmCard({
|
||||||
className="block w-full py-1.5 text-black dark:text-white"
|
className="block w-full py-1.5 text-black dark:text-white"
|
||||||
to={`./${id}/deregister`}
|
to={`./${id}/deregister`}
|
||||||
>
|
>
|
||||||
Deregister from cloud
|
{m.deregister_from_cloud()}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export function MacroForm({
|
||||||
await onSubmit(macro);
|
await onSubmit(macro);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
showTemporaryError(m.macro_save_failed_error(error.message || m.unknown_error));
|
showTemporaryError(m.macro_save_failed_error({error: error.message || m.unknown_error()}));
|
||||||
} else {
|
} else {
|
||||||
showTemporaryError(m.macro_save_failed());
|
showTemporaryError(m.macro_save_failed());
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +214,7 @@ export function MacroForm({
|
||||||
<div className="mt-2 space-y-4">
|
<div className="mt-2 space-y-4">
|
||||||
{(macro.steps || []).map((step, stepIndex) => (
|
{(macro.steps || []).map((step, stepIndex) => (
|
||||||
<MacroStepCard
|
<MacroStepCard
|
||||||
key={`step-{stepIndex}`}
|
key={`step-${stepIndex}`}
|
||||||
step={step}
|
step={step}
|
||||||
stepIndex={stepIndex}
|
stepIndex={stepIndex}
|
||||||
onDelete={
|
onDelete={
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export function MacroStepCard({
|
||||||
<div className="flex flex-wrap gap-1 pb-2">
|
<div className="flex flex-wrap gap-1 pb-2">
|
||||||
{step.keys.map((key, keyIndex) => (
|
{step.keys.map((key, keyIndex) => (
|
||||||
<span
|
<span
|
||||||
key={`key-{keyIndex}`}
|
key={`key-${keyIndex}`}
|
||||||
className="inline-flex items-center rounded-md bg-blue-100 px-1 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-200"
|
className="inline-flex items-center rounded-md bg-blue-100 px-1 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-200"
|
||||||
>
|
>
|
||||||
<span className="px-1">{keyDisplay(keyDisplayMap, key)}</span>
|
<span className="px-1">{keyDisplay(keyDisplayMap, key)}</span>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to load USB devices:", resp.error);
|
console.error("Failed to load USB devices:", resp.error);
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.usb_device_failed_load({ error: String(resp.error.data || "Unknown error") }),
|
m.usb_device_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||||
|
|
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
|
||||||
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
|
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.usb_device_failed_set({ error: String(resp.error.data || "Unknown error") }),
|
m.usb_device_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function UsbInfoSetting() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to load USB Config:", resp.error);
|
console.error("Failed to load USB Config:", resp.error);
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.usb_config_failed_load({ error: String(resp.error.data || "Unknown error") }),
|
m.usb_config_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const usbConfigState = resp.result as UsbConfigState;
|
const usbConfigState = resp.result as UsbConfigState;
|
||||||
|
|
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
|
||||||
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
|
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.usb_config_failed_set({ error: String(resp.error.data || "Unknown error") }),
|
m.usb_config_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -139,7 +139,7 @@ export function UsbInfoSetting() {
|
||||||
send("getDeviceID", {}, (resp: JsonRpcResponse) => {
|
send("getDeviceID", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
return notifications.error(
|
return notifications.error(
|
||||||
`Failed to get device ID: ${resp.error.data || "Unknown error"}`,
|
`Failed to get device ID: ${resp.error.data || m.unknown_error()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setDeviceId(resp.result as string);
|
setDeviceId(resp.result as string);
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
||||||
})}
|
})}
|
||||||
domain={[0, 600]}
|
domain={[0, 600]}
|
||||||
unit={m.connection_stats_units_milliseconds()}
|
unit={m.connection_stats_unit_milliseconds()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
||||||
})}
|
})}
|
||||||
domain={[0, 10]}
|
domain={[0, 10]}
|
||||||
unit={m.connection_stats_units_milliseconds()}
|
unit={m.connection_stats_unit_milliseconds()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Playback Delay */}
|
{/* Playback Delay */}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export default function DevicesIdDeregister() {
|
||||||
size="MD"
|
size="MD"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
type="submit"
|
type="submit"
|
||||||
text={m.deregister_button()}
|
text={m.deregister_from_cloud()}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,10 @@ import {
|
||||||
|
|
||||||
export default function MountRoute() {
|
export default function MountRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
{
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
|
||||||
}
|
|
||||||
return <Dialog onClose={() => navigate("..")} />;
|
return <Dialog onClose={() => navigate("..")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({ onClose }: { onClose: () => void }) {
|
export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
|
||||||
const {
|
const {
|
||||||
modalView,
|
modalView,
|
||||||
setModalView,
|
setModalView,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
||||||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { User } from "@hooks/stores";
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { CardHeader } from "@components/CardHeader";
|
import { CardHeader } from "@components/CardHeader";
|
||||||
|
|
@ -14,11 +13,6 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { m } from "@localizations/messages";
|
import { m } from "@localizations/messages";
|
||||||
|
|
||||||
interface LoaderData {
|
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { name } = Object.fromEntries(await request.formData());
|
const { name } = Object.fromEntries(await request.formData());
|
||||||
|
|
@ -65,7 +59,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeviceIdRename() {
|
export default function DeviceIdRename() {
|
||||||
const { device, user } = useLoaderData() as LoaderData;
|
const { device, user } = useLoaderData();
|
||||||
const error = useActionData() as { message: string };
|
const error = useActionData() as { message: string };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -114,7 +108,7 @@ export default function DeviceIdRename() {
|
||||||
size="MD"
|
size="MD"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
text={m.rename_device_button()}
|
text={m.rename_device()}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.access_failed_deregister({ error: resp.error.data || "Unknown error" }),
|
m.access_failed_deregister({ error: resp.error.data || m.unknown_error() }),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }),
|
m.access_failed_update_cloud_url({ error: resp.error.data || m.unknown_error() }),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +160,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }),
|
m.access_failed_update_tls({ error: resp.error.data || m.unknown_error() }),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,10 @@ export default function SecurityAccessLocalAuthRoute() {
|
||||||
}
|
}
|
||||||
}, [init, navigateTo, setModalView]);
|
}, [init, navigateTo, setModalView]);
|
||||||
|
|
||||||
{
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
|
||||||
}
|
|
||||||
return <Dialog onClose={() => navigateTo("..")} />;
|
return <Dialog onClose={() => navigateTo("..")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({ onClose }: { onClose: () => void }) {
|
export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
|
||||||
const { modalView, setModalView } = useLocalAuthModalStore();
|
const { modalView, setModalView } = useLocalAuthModalStore();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const revalidator = useRevalidator();
|
const revalidator = useRevalidator();
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ export default function SettingsGeneralRebootRoute() {
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
onClose,
|
onClose,
|
||||||
onConfirmUpdate,
|
onConfirmUpdate,
|
||||||
}: {
|
}: Readonly<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirmUpdate: () => void;
|
onConfirmUpdate: () => void;
|
||||||
}) {
|
}>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto relative mx-auto text-left">
|
<div className="pointer-events-auto relative mx-auto text-left">
|
||||||
|
|
|
||||||
|
|
@ -36,19 +36,16 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
}
|
}
|
||||||
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
||||||
|
|
||||||
{
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
|
||||||
}
|
|
||||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
onClose,
|
onClose,
|
||||||
onConfirmUpdate,
|
onConfirmUpdate,
|
||||||
}: {
|
}: Readonly<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirmUpdate: () => void;
|
onConfirmUpdate: () => void;
|
||||||
}) {
|
}>) {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export default function SettingsMacrosRoute() {
|
||||||
setActionLoadingId(macro.id);
|
setActionLoadingId(macro.id);
|
||||||
|
|
||||||
const newMacroCopy: KeySequence = {
|
const newMacroCopy: KeySequence = {
|
||||||
...JSON.parse(JSON.stringify(macro)),
|
...structuredClone(macro),
|
||||||
id: generateMacroId(),
|
id: generateMacroId(),
|
||||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||||
sortOrder: macros.length + 1,
|
sortOrder: macros.length + 1,
|
||||||
|
|
@ -170,7 +170,7 @@ export default function SettingsMacrosRoute() {
|
||||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={stepIndex} className="inline-flex items-center">
|
<span key={`step-${stepIndex}`} className="inline-flex items-center">
|
||||||
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
|
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
|
||||||
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
||||||
{(Array.isArray(step.modifiers) &&
|
{(Array.isArray(step.modifiers) &&
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const resolveOnRtcReady = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
export function LifeTimeLabel({ lifetime }: Readonly<{ lifetime: string }>) {
|
||||||
const [remaining, setRemaining] = useState<string | null>(null);
|
const [remaining, setRemaining] = useState<string | null>(null);
|
||||||
|
|
||||||
// rrecalculate remaining time every 30 seconds
|
// rrecalculate remaining time every 30 seconds
|
||||||
|
|
@ -106,7 +106,7 @@ export default function SettingsNetworkRoute() {
|
||||||
getNetworkState(),
|
getNetworkState(),
|
||||||
])) as [NetworkSettings, NetworkState];
|
])) as [NetworkSettings, NetworkState];
|
||||||
|
|
||||||
setNetworkState(state as NetworkState);
|
setNetworkState(state);
|
||||||
|
|
||||||
const settingsWithDefaults = {
|
const settingsWithDefaults = {
|
||||||
...settings,
|
...settings,
|
||||||
|
|
@ -160,8 +160,8 @@ export default function SettingsNetworkRoute() {
|
||||||
const onSubmit = useCallback(async (settings: NetworkSettings) => {
|
const onSubmit = useCallback(async (settings: NetworkSettings) => {
|
||||||
if (settings.ipv4_static?.address?.includes("/")) {
|
if (settings.ipv4_static?.address?.includes("/")) {
|
||||||
const parts = settings.ipv4_static.address.split("/");
|
const parts = settings.ipv4_static.address.split("/");
|
||||||
const cidrNotation = parseInt(parts[1]);
|
const cidrNotation = Number.parseInt(parts[1]);
|
||||||
if (isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) {
|
if (Number.isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) {
|
||||||
return notifications.error(m.network_ipv4_invalid_cidr());
|
return notifications.error(m.network_ipv4_invalid_cidr());
|
||||||
}
|
}
|
||||||
settings.ipv4_static.netmask = netMaskFromCidr4(cidrNotation);
|
settings.ipv4_static.netmask = netMaskFromCidr4(cidrNotation);
|
||||||
|
|
@ -309,18 +309,16 @@ export default function SettingsNetworkRoute() {
|
||||||
title={m.network_title()}
|
title={m.network_title()}
|
||||||
description={m.network_description()}
|
description={m.network_description()}
|
||||||
action={
|
action={
|
||||||
<>
|
<div>
|
||||||
<div>
|
<Button
|
||||||
<Button
|
size="SM"
|
||||||
size="SM"
|
theme="primary"
|
||||||
theme="primary"
|
disabled={!(formState.isDirty || formState.isSubmitting)}
|
||||||
disabled={!(formState.isDirty || formState.isSubmitting)}
|
loading={formState.isSubmitting}
|
||||||
loading={formState.isSubmitting}
|
type="submit"
|
||||||
type="submit"
|
text={formState.isSubmitting ? m.saving() : m.network_save_settings()}
|
||||||
text={formState.isSubmitting ? m.saving() : m.network_save_settings()}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ export default function SettingsVideoRoute() {
|
||||||
max="2.0"
|
max="2.0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={videoSaturation}
|
value={videoSaturation}
|
||||||
onChange={e => setVideoSaturation(parseFloat(e.target.value))}
|
onChange={e => setVideoSaturation(Number.parseFloat(e.target.value))}
|
||||||
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
@ -200,7 +200,7 @@ export default function SettingsVideoRoute() {
|
||||||
max="1.5"
|
max="1.5"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={videoBrightness}
|
value={videoBrightness}
|
||||||
onChange={e => setVideoBrightness(parseFloat(e.target.value))}
|
onChange={e => setVideoBrightness(Number.parseFloat(e.target.value))}
|
||||||
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
@ -215,7 +215,7 @@ export default function SettingsVideoRoute() {
|
||||||
max="2.0"
|
max="2.0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={videoContrast}
|
value={videoContrast}
|
||||||
onChange={e => setVideoContrast(parseFloat(e.target.value))}
|
onChange={e => setVideoContrast(Number.parseFloat(e.target.value))}
|
||||||
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
@ -226,9 +226,9 @@ export default function SettingsVideoRoute() {
|
||||||
theme="light"
|
theme="light"
|
||||||
text={m.video_reset_to_default()}
|
text={m.video_reset_to_default()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVideoSaturation(1.0);
|
setVideoSaturation(1);
|
||||||
setVideoBrightness(1.0);
|
setVideoBrightness(1);
|
||||||
setVideoContrast(1.0);
|
setVideoContrast(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,7 +250,7 @@ export default function SettingsVideoRoute() {
|
||||||
setCustomEdidValue("");
|
setCustomEdidValue("");
|
||||||
} else {
|
} else {
|
||||||
setCustomEdidValue(null);
|
setCustomEdidValue(null);
|
||||||
handleEDIDChange(e.target.value as string);
|
handleEDIDChange(e.target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={[...edids, { value: "custom", label: m.video_edid_custom() }]}
|
options={[...edids, { value: "custom", label: m.video_edid_custom() }]}
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,6 @@ import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
||||||
import { DeviceStatus } from "@routes/welcome-local";
|
import { DeviceStatus } from "@routes/welcome-local";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
|
||||||
authMode: "password" | "noPassword" | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CloudLoaderResp {
|
interface CloudLoaderResp {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
|
@ -117,7 +113,7 @@ const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function KvmIdRoute() {
|
export default function KvmIdRoute() {
|
||||||
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
|
const loaderResp = useLoaderData();
|
||||||
// Depending on the mode, we set the appropriate variables
|
// Depending on the mode, we set the appropriate variables
|
||||||
const user = "user" in loaderResp ? loaderResp.user : null;
|
const user = "user" in loaderResp ? loaderResp.user : null;
|
||||||
const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null;
|
const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null;
|
||||||
|
|
@ -280,7 +276,7 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessage(event: WebSocketEventMap['message']) {
|
onMessage(event: WebSocketEventMap['message']) {
|
||||||
const message = event as MessageEvent;
|
const message = event;
|
||||||
if (message.data === "pong") return;
|
if (message.data === "pong") return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -877,6 +873,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
role="form"
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
onMouseUp={e => e.stopPropagation()}
|
onMouseUp={e => e.stopPropagation()}
|
||||||
onMouseDown={e => e.stopPropagation()}
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useLoaderData, useRevalidator } from "react-router";
|
import { useLoaderData, useRevalidator, type LoaderFunction } from "react-router";
|
||||||
import type { LoaderFunction } from "react-router";
|
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
@ -17,8 +16,7 @@ interface LoaderData {
|
||||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
const loader: LoaderFunction = async ()=> {
|
||||||
const loader: LoaderFunction = async () => {
|
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -81,21 +79,19 @@ export default function DevicesRoute() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
|
{devices.map(x => {
|
||||||
{devices.map(x => {
|
return (
|
||||||
return (
|
<KvmCard
|
||||||
<KvmCard
|
key={x.id}
|
||||||
key={x.id}
|
id={x.id}
|
||||||
id={x.id}
|
title={x.name ?? x.id}
|
||||||
title={x.name ?? x.id}
|
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
||||||
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
online={x.online}
|
||||||
online={x.online}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ export default function LoginLocalRoute() {
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowPassword(false)}
|
onClick={() => setShowPassword(false)}
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={showPassword}
|
||||||
>
|
>
|
||||||
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,6 +104,8 @@ export default function LoginLocalRoute() {
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowPassword(true)}
|
onClick={() => setShowPassword(true)}
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={!showPassword}
|
||||||
>
|
>
|
||||||
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ export default function WelcomeLocalModeRoute() {
|
||||||
<div
|
<div
|
||||||
className="relative flex cursor-pointer flex-col items-center p-6 select-none"
|
className="relative flex cursor-pointer flex-col items-center p-6 select-none"
|
||||||
onClick={() => setSelectedMode(mode as "password" | "noPassword")}
|
onClick={() => setSelectedMode(mode as "password" | "noPassword")}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={selectedMode === "password"}
|
||||||
>
|
>
|
||||||
<div className="space-y-0 text-center">
|
<div className="space-y-0 text-center">
|
||||||
<h3 className="text-base font-bold text-black dark:text-white">
|
<h3 className="text-base font-bold text-black dark:text-white">
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,8 @@ export default function WelcomeLocalPasswordRoute() {
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowPassword(false)}
|
onClick={() => setShowPassword(false)}
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={showPassword}
|
||||||
>
|
>
|
||||||
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,6 +121,8 @@ export default function WelcomeLocalPasswordRoute() {
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowPassword(true)}
|
onClick={() => setShowPassword(true)}
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={!showPassword}
|
||||||
>
|
>
|
||||||
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ const loader: LoaderFunction = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LogoLeadingIcon = ({ className }: { className?: string }) => (
|
||||||
|
<img src={LogoMark} className={cx(className, "mr-1.5 h-5!")} alt={m.jetkvm_logo()} />
|
||||||
|
);
|
||||||
|
|
||||||
export default function WelcomeRoute() {
|
export default function WelcomeRoute() {
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
|
||||||
|
|
@ -89,9 +93,7 @@ export default function WelcomeRoute() {
|
||||||
size="LG"
|
size="LG"
|
||||||
theme="light"
|
theme="light"
|
||||||
text={m.jetkvm_setup()}
|
text={m.jetkvm_setup()}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={LogoLeadingIcon}
|
||||||
<img src={LogoMark} className={cx(className, "mr-1.5 h-5!")} />
|
|
||||||
)}
|
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
to="/welcome/mode"
|
to="/welcome/mode"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
def flatten_strings(obj, prefix=""):
|
def flatten_strings(obj, prefix=""):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
|
|
@ -24,7 +25,7 @@ def normalize(s, ignore_case=False, trim=False, collapse_ws=False):
|
||||||
s = s.lower()
|
s = s.lower()
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def main():
|
def main(argv):
|
||||||
p = argparse.ArgumentParser(
|
p = argparse.ArgumentParser(
|
||||||
description="Find identical translation targets with different keys in en.json"
|
description="Find identical translation targets with different keys in en.json"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue