Compare commits

..

3 Commits

Author SHA1 Message Date
Marc Brooks 938a94fcba
Merge 01891e8aee into 74e64f69a7 2025-10-17 15:17:31 +00:00
Marc Brooks 01891e8aee
Add note to do back-translation 2025-10-17 10:17:19 -05:00
Marc Brooks c6d4fffca4
Add inlang/paraglide-js localization
Remove the temporary directory after extracting buildkit
Localize the extension popovers.
Update package and fix tsconfig.json
Expand development directory guide
Move messages under localization
Popovers and sidebar
Update Chinese translations
Accidentally lost the changes that @ym provided, brought them back
File formatting pass
Localized all components, hooks, providers, hooks
Localize all pages except Settings
Bump packages
Settings Access page
Settings local auth page
Fix ref lint warning
Settings Advanced page
Fix UI lint warnings there were a bunch of ref and useEffect violations.
Settings appearance page
Settings general pages
Settings hardware page
Settings keyboard page
Settings macros pages
Settings mouse page
Settings page
Settings video page
Settings network page
Fix compilation issues
Ran machine translate
Use getLocale for date, relative time, and money formatting
Fix eslint
Delete unused messages
Added setting to choose locale
Merged in dev hotfix
Fix update status rendering
2025-10-17 00:03:49 -05:00
24 changed files with 124 additions and 372 deletions

View File

@ -491,9 +491,9 @@ If you enable the [Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtensi
``` ```
4. Save the _en.json_ file and execute `npm run i18n` to resort the language files, validate the translations, and create the m-functions 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 calls to 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()` 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 for the literal `I will call you {name}`, use `m.profile_i_will_call_you({ name: edit.value })` - **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. Make sure you use an LLM (you can use [aifiesta](https://chat.aifiesta.ai/chat/) to use multiple LLMs) or a [translator](https://translate.google.com) of some form to back-translate each **new** machine-generation in each _language_ to ensure those terms translate reasonably. 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. Make sure you use an LLM or [translator](https://translate.google.com) of some form to back-translate each **new** machine-generation in each _langauge_ to ensure those terms translate reasonably.
### Adding a new language ### Adding a new language

View File

@ -106,7 +106,6 @@ type Config struct {
NetworkConfig *types.NetworkConfig `json:"network_config"` NetworkConfig *types.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"` VideoSleepAfterSec int `json:"video_sleep_after_sec"`
VideoQualityFactor float64 `json:"video_quality_factor"`
} }
func (c *Config) GetDisplayRotation() uint16 { func (c *Config) GetDisplayRotation() uint16 {

View File

@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status(); return (char *)videoc_log_status();
} }
int jetkvm_video_init(float factor) { int jetkvm_video_init() {
return video_init(factor); return video_init();
} }
void jetkvm_video_shutdown() { void jetkvm_video_shutdown() {

View File

@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
const char *jetkvm_ui_event_code_to_name(int code); const char *jetkvm_ui_event_code_to_name(int code);
int jetkvm_video_init(float quality_factor); int jetkvm_video_init();
void jetkvm_video_shutdown(); void jetkvm_video_shutdown();
void jetkvm_video_start(); void jetkvm_video_start();
void jetkvm_video_stop(); void jetkvm_video_stop();

View File

@ -29,7 +29,6 @@
#define VIDEO_DEV "/dev/video0" #define VIDEO_DEV "/dev/video0"
#define SUB_DEV "/dev/v4l-subdev2" #define SUB_DEV "/dev/v4l-subdev2"
#define SLEEP_MODE_FILE "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1)) #define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
#define RK_ALIGN_2(x) RK_ALIGN(x, 2) #define RK_ALIGN_2(x) RK_ALIGN(x, 2)
@ -40,7 +39,6 @@ int sub_dev_fd = -1;
#define VENC_CHANNEL 0 #define VENC_CHANNEL 0
MB_POOL memPool = MB_INVALID_POOLID; MB_POOL memPool = MB_INVALID_POOLID;
bool sleep_mode_available = false;
bool should_exit = false; bool should_exit = false;
float quality_factor = 1.0f; float quality_factor = 1.0f;
@ -53,45 +51,6 @@ RK_U64 get_us()
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */ return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
} }
static void ensure_sleep_mode_disabled()
{
if (!sleep_mode_available)
{
return;
}
int fd = open(SLEEP_MODE_FILE, O_RDWR);
if (fd < 0)
{
log_error("Failed to open sleep mode file: %s", strerror(errno));
return;
}
lseek(fd, 0, SEEK_SET);
char buffer[1];
read(fd, buffer, 1);
if (buffer[0] == '0') {
close(fd);
return;
}
log_warn("HDMI sleep mode is not disabled, disabling it");
lseek(fd, 0, SEEK_SET);
write(fd, "0", 1);
close(fd);
usleep(1000); // give some time to the system to disable the sleep mode
return;
}
static void detect_sleep_mode()
{
if (access(SLEEP_MODE_FILE, F_OK) != 0) {
sleep_mode_available = false;
return;
}
sleep_mode_available = true;
ensure_sleep_mode_disabled();
}
double calculate_bitrate(float bitrate_factor, int width, int height) double calculate_bitrate(float bitrate_factor, int width, int height)
{ {
const int32_t base_bitrate_high = 2000; const int32_t base_bitrate_high = 2000;
@ -231,15 +190,8 @@ static int32_t buf_init()
pthread_t *format_thread = NULL; pthread_t *format_thread = NULL;
int video_init(float factor) int video_init()
{ {
detect_sleep_mode();
if (factor < 0 || factor > 1) {
factor = 1.0f;
}
quality_factor = factor;
if (RK_MPI_SYS_Init() != RK_SUCCESS) if (RK_MPI_SYS_Init() != RK_SUCCESS)
{ {
log_error("RK_MPI_SYS_Init failed"); log_error("RK_MPI_SYS_Init failed");
@ -349,29 +301,11 @@ static void *venc_read_stream(void *arg)
} }
uint32_t detected_width, detected_height; uint32_t detected_width, detected_height;
bool detected_signal = false, streaming_flag = false, streaming_stopped = true; bool detected_signal = false, streaming_flag = false;
pthread_t *streaming_thread = NULL; pthread_t *streaming_thread = NULL;
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
bool get_streaming_flag()
{
log_info("getting streaming flag");
pthread_mutex_lock(&streaming_mutex);
bool flag = streaming_flag;
pthread_mutex_unlock(&streaming_mutex);
return flag;
}
void set_streaming_flag(bool flag)
{
log_info("setting streaming flag to %d", flag);
pthread_mutex_lock(&streaming_mutex);
streaming_flag = flag;
pthread_mutex_unlock(&streaming_mutex);
}
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename) void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
{ {
FILE *file = fopen(filename, "wb"); FILE *file = fopen(filename, "wb");
@ -385,8 +319,6 @@ void *run_video_stream(void *arg)
log_info("running video stream"); log_info("running video stream");
streaming_stopped = false;
while (streaming_flag) while (streaming_flag)
{ {
if (detected_signal == false) if (detected_signal == false)
@ -469,7 +401,7 @@ void *run_video_stream(void *arg)
{ {
log_error("get mb blk failed!"); log_error("get mb blk failed!");
close(video_dev_fd); close(video_dev_fd);
return (void *)errno; return ;
} }
log_info("Got memory block for buffer %d", i); log_info("Got memory block for buffer %d", i);
@ -606,18 +538,6 @@ void *run_video_stream(void *arg)
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno)); log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
} }
// Explicitly free V4L2 buffer queue
struct v4l2_requestbuffers req_free;
memset(&req_free, 0, sizeof(req_free));
req_free.count = 0;
req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
req_free.memory = V4L2_MEMORY_DMABUF;
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
{
log_error("Failed to free V4L2 buffers: %s", strerror(errno));
}
venc_stop(); venc_stop();
for (int i = 0; i < input_buffer_count; i++) for (int i = 0; i < input_buffer_count; i++)
@ -633,9 +553,6 @@ void *run_video_stream(void *arg)
} }
log_info("video stream thread exiting"); log_info("video stream thread exiting");
streaming_stopped = true;
return NULL; return NULL;
} }
@ -665,75 +582,56 @@ void video_shutdown()
log_info("Destroyed streaming mutex"); log_info("Destroyed streaming mutex");
} }
void video_start_streaming() void video_start_streaming()
{ {
log_info("starting video streaming"); pthread_mutex_lock(&streaming_mutex);
if (streaming_thread != NULL) if (streaming_thread != NULL)
{ {
if (streaming_stopped == true) {
log_error("video streaming already stopped but streaming_thread is not NULL");
assert(streaming_stopped == true);
}
log_warn("video streaming already started"); log_warn("video streaming already started");
return; goto cleanup;
} }
pthread_t *new_thread = malloc(sizeof(pthread_t)); pthread_t *new_thread = malloc(sizeof(pthread_t));
if (new_thread == NULL) if (new_thread == NULL)
{ {
log_error("Failed to allocate memory for streaming thread"); log_error("Failed to allocate memory for streaming thread");
return; goto cleanup;
} }
set_streaming_flag(true); streaming_flag = true;
int result = pthread_create(new_thread, NULL, run_video_stream, NULL); int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
if (result != 0) if (result != 0)
{ {
log_error("Failed to create streaming thread: %s", strerror(result)); log_error("Failed to create streaming thread: %s", strerror(result));
set_streaming_flag(false); streaming_flag = false;
free(new_thread); free(new_thread);
return; goto cleanup;
} }
// Only set streaming_thread after successful creation // Only set streaming_thread after successful creation, and before unlocking the mutex
streaming_thread = new_thread; streaming_thread = new_thread;
cleanup:
pthread_mutex_unlock(&streaming_mutex);
return;
} }
void video_stop_streaming() void video_stop_streaming()
{ {
if (streaming_thread == NULL) { pthread_mutex_lock(&streaming_mutex);
log_info("video streaming already stopped"); if (streaming_thread != NULL)
return;
}
log_info("stopping video streaming");
set_streaming_flag(false);
log_info("waiting for video streaming thread to exit");
int attempts = 0;
while (!streaming_stopped && attempts < 30) {
usleep(100000); // 100ms
attempts++;
}
if (!streaming_stopped) {
log_error("video streaming thread did not exit after 30s");
}
pthread_join(*streaming_thread, NULL);
free(streaming_thread);
streaming_thread = NULL;
log_info("video streaming stopped");
}
void video_restart_streaming()
{
if (get_streaming_flag() == true)
{ {
log_info("restarting video streaming"); streaming_flag = false;
video_stop_streaming(); log_info("stopping video streaming");
// wait 100ms for the thread to exit
usleep(1000000);
log_info("waiting for video streaming thread to exit");
pthread_join(*streaming_thread, NULL);
free(streaming_thread);
streaming_thread = NULL;
log_info("video streaming stopped");
} }
video_start_streaming(); pthread_mutex_unlock(&streaming_mutex);
} }
void *run_detect_format(void *arg) void *run_detect_format(void *arg)
@ -752,8 +650,6 @@ void *run_detect_format(void *arg)
while (!should_exit) while (!should_exit)
{ {
ensure_sleep_mode_disabled();
memset(&dv_timings, 0, sizeof(dv_timings)); memset(&dv_timings, 0, sizeof(dv_timings));
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0) if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
{ {
@ -793,17 +689,21 @@ void *run_detect_format(void *arg)
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync + (dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
dv_timings.bt.hbackporch)); dv_timings.bt.hbackporch));
log_info("Frames per second: %.2f fps", frames_per_second); log_info("Frames per second: %.2f fps", frames_per_second);
bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal;
detected_width = dv_timings.bt.width; detected_width = dv_timings.bt.width;
detected_height = dv_timings.bt.height; detected_height = dv_timings.bt.height;
detected_signal = true; detected_signal = true;
video_report_format(true, NULL, detected_width, detected_height, frames_per_second); video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
pthread_mutex_lock(&streaming_mutex);
if (should_restart) { if (streaming_flag == true)
log_info("restarting video streaming due to format change"); {
video_restart_streaming(); pthread_mutex_unlock(&streaming_mutex);
log_info("restarting on going video streaming");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
} }
} }
@ -831,7 +731,19 @@ void video_set_quality_factor(float factor)
quality_factor = factor; quality_factor = factor;
// TODO: update venc bitrate without stopping streaming // TODO: update venc bitrate without stopping streaming
video_restart_streaming();
pthread_mutex_lock(&streaming_mutex);
if (streaming_flag == true)
{
pthread_mutex_unlock(&streaming_mutex);
log_info("restarting on going video streaming due to quality factor change");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
}
} }
float video_get_quality_factor() { float video_get_quality_factor() {

View File

@ -6,7 +6,7 @@
* *
* @return int 0 on success, -1 on failure * @return int 0 on success, -1 on failure
*/ */
int video_init(float quality_factor); int video_init();
/** /**
* @brief Shutdown the video subsystem * @brief Shutdown the video subsystem

View File

@ -129,13 +129,11 @@ func uiTick() {
C.jetkvm_ui_tick() C.jetkvm_ui_tick()
} }
func videoInit(factor float64) error { func videoInit() error {
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
factorC := C.float(factor) ret := C.jetkvm_video_init()
ret := C.jetkvm_video_init(factorC)
if ret != 0 { if ret != 0 {
return fmt.Errorf("failed to initialize video: %d", ret) return fmt.Errorf("failed to initialize video: %d", ret)
} }

View File

@ -15,7 +15,6 @@ type Native struct {
systemVersion *semver.Version systemVersion *semver.Version
appVersion *semver.Version appVersion *semver.Version
displayRotation uint16 displayRotation uint16
defaultQualityFactor float64
onVideoStateChange func(state VideoState) onVideoStateChange func(state VideoState)
onVideoFrameReceived func(frame []byte, duration time.Duration) onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string) onIndevEvent func(event string)
@ -23,14 +22,12 @@ type Native struct {
sleepModeSupported bool sleepModeSupported bool
videoLock sync.Mutex videoLock sync.Mutex
screenLock sync.Mutex screenLock sync.Mutex
extraLock sync.Mutex
} }
type NativeOptions struct { type NativeOptions struct {
SystemVersion *semver.Version SystemVersion *semver.Version
AppVersion *semver.Version AppVersion *semver.Version
DisplayRotation uint16 DisplayRotation uint16
DefaultQualityFactor float64
OnVideoStateChange func(state VideoState) OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration) OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string) OnIndevEvent func(event string)
@ -68,11 +65,6 @@ func NewNative(opts NativeOptions) *Native {
sleepModeSupported := isSleepModeSupported() sleepModeSupported := isSleepModeSupported()
defaultQualityFactor := opts.DefaultQualityFactor
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
defaultQualityFactor = 1.0
}
return &Native{ return &Native{
ready: make(chan struct{}), ready: make(chan struct{}),
l: nativeLogger, l: nativeLogger,
@ -80,7 +72,6 @@ func NewNative(opts NativeOptions) *Native {
systemVersion: opts.SystemVersion, systemVersion: opts.SystemVersion,
appVersion: opts.AppVersion, appVersion: opts.AppVersion,
displayRotation: opts.DisplayRotation, displayRotation: opts.DisplayRotation,
defaultQualityFactor: defaultQualityFactor,
onVideoStateChange: onVideoStateChange, onVideoStateChange: onVideoStateChange,
onVideoFrameReceived: onVideoFrameReceived, onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent, onIndevEvent: onIndevEvent,
@ -106,7 +97,7 @@ func (n *Native) Start() {
n.initUI() n.initUI()
go n.tickUI() go n.tickUI()
if err := videoInit(n.defaultQualityFactor); err != nil { if err := videoInit(); err != nil {
n.l.Error().Err(err).Msg("failed to initialize video") n.l.Error().Err(err).Msg("failed to initialize video")
} }

View File

@ -1,18 +1,11 @@
package native package native
import ( import (
"fmt"
"os" "os"
"time"
) )
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
// DefaultEDID is the default EDID for the video stream.
const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
var extraLockTimeout = 5 * time.Second
// VideoState is the state of the video stream. // VideoState is the state of the video stream.
type VideoState struct { type VideoState struct {
Ready bool `json:"ready"` Ready bool `json:"ready"`
@ -73,32 +66,12 @@ func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported return n.sleepModeSupported
} }
// useExtraLock uses the extra lock to execute a function.
// if the lock is currently held by another goroutine, returns an error.
//
// it's used to ensure that only one change is made to the video stream at a time.
// as the change usually requires to restart video streaming
// TODO: check video streaming status instead of using a hardcoded timeout
func (n *Native) useExtraLock(fn func() error) error {
if !n.extraLock.TryLock() {
return fmt.Errorf("the previous change hasn't been completed yet")
}
err := fn()
if err == nil {
time.Sleep(extraLockTimeout)
}
n.extraLock.Unlock()
return err
}
// VideoSetQualityFactor sets the quality factor for the video stream. // VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error { func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
return n.useExtraLock(func() error { return videoSetStreamQualityFactor(factor)
return videoSetStreamQualityFactor(factor)
})
} }
// VideoGetQualityFactor gets the quality factor for the video stream. // VideoGetQualityFactor gets the quality factor for the video stream.
@ -114,13 +87,7 @@ func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
if edid == "" { return videoSetEDID(edid)
edid = DefaultEDID
}
return n.useExtraLock(func() error {
return videoSetEDID(edid)
})
} }
// VideoGetEDID gets the EDID for the video stream. // VideoGetEDID gets the EDID for the video stream.

View File

@ -243,6 +243,7 @@ func rpcGetEDID() (string, error) {
func rpcSetEDID(edid string) error { func rpcSetEDID(edid string) error {
if edid == "" { if edid == "" {
logger.Info().Msg("Restoring EDID to default") logger.Info().Msg("Restoring EDID to default")
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
} else { } else {
logger.Info().Str("edid", edid).Msg("Setting EDID") logger.Info().Str("edid", edid).Msg("Setting EDID")
} }

View File

@ -17,10 +17,9 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) { func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{ nativeInstance = native.NewNative(native.NativeOptions{
SystemVersion: systemVersion, SystemVersion: systemVersion,
AppVersion: appVersion, AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(), DisplayRotation: config.GetDisplayRotation(),
DefaultQualityFactor: config.VideoQualityFactor,
OnVideoStateChange: func(state native.VideoState) { OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state lastVideoState = state
triggerVideoStateUpdate() triggerVideoStateUpdate()
@ -59,13 +58,7 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
} }
}, },
}) })
nativeInstance.Start() nativeInstance.Start()
go func() {
if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil {
nativeLogger.Warn().Err(err).Msg("error setting EDID")
}
}()
if os.Getenv("JETKVM_CRASH_TESTING") == "1" { if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
nativeInstance.DoNotUseThisIsForCrashTestingOnly() nativeInstance.DoNotUseThisIsForCrashTestingOnly()

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Afregistrering fra Cloud",
"deregister_headline": "Afregistrér {device} fra din cloud-konto", "deregister_headline": "Afregistrér {device} fra din cloud-konto",
"detach": "Løsrive", "detach": "Løsrive",
"dhcp_empty_lease_description": "Vi har endnu ikke modtaget nogen DHCP-leaseoplysninger fra enheden.", "dhcp_empty_lease_description": "Vi har endnu ikke modtaget nogen DHCP-leaseoplysninger fra enheden.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Skærmretning", "hardware_display_orientation_title": "Skærmretning",
"hardware_display_wake_up_note": "Skærmen vågner op, når forbindelsestilstanden ændres, eller når den berøres.", "hardware_display_wake_up_note": "Skærmen vågner op, når forbindelsestilstanden ændres, eller når den berøres.",
"hardware_page_description": "Konfigurer skærmindstillinger og hardwareindstillinger for din JetKVM-enhed", "hardware_page_description": "Konfigurer skærmindstillinger og hardwareindstillinger for din JetKVM-enhed",
"hardware_power_saving_description": "Reducer strømforbruget, når det ikke er i brug",
"hardware_power_saving_disabled": "Strømsparetilstand deaktiveret",
"hardware_power_saving_enabled": "Strømsparetilstand aktiveret",
"hardware_power_saving_failed_error": "Kunne ikke indstille strømsparetilstand: {error}",
"hardware_power_saving_hdmi_sleep_description": "Slå optagelse fra efter 90 sekunders inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-dvaletilstand",
"hardware_power_saving_title": "Strømbesparelse",
"hardware_time_10_minutes": "10 minutter", "hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time", "hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minut", "hardware_time_1_minute": "1 minut",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Abmelden von der Cloud",
"deregister_headline": "Melden Sie {device}", "deregister_headline": "Melden Sie {device}",
"detach": "Abtrennen", "detach": "Abtrennen",
"dhcp_empty_lease_description": "Wir haben noch keine DHCP-Lease-Informationen vom Gerät erhalten.", "dhcp_empty_lease_description": "Wir haben noch keine DHCP-Lease-Informationen vom Gerät erhalten.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Anzeigeausrichtung", "hardware_display_orientation_title": "Anzeigeausrichtung",
"hardware_display_wake_up_note": "Das Display wird aktiviert, wenn sich der Verbindungsstatus ändert oder wenn es berührt wird.", "hardware_display_wake_up_note": "Das Display wird aktiviert, wenn sich der Verbindungsstatus ändert oder wenn es berührt wird.",
"hardware_page_description": "Konfigurieren Sie Anzeigeeinstellungen und Hardwareoptionen für Ihr JetKVM-Gerät", "hardware_page_description": "Konfigurieren Sie Anzeigeeinstellungen und Hardwareoptionen für Ihr JetKVM-Gerät",
"hardware_power_saving_description": "Reduzieren Sie den Stromverbrauch bei Nichtgebrauch",
"hardware_power_saving_disabled": "Energiesparmodus deaktiviert",
"hardware_power_saving_enabled": "Energiesparmodus aktiviert",
"hardware_power_saving_failed_error": "Fehler beim Einstellen des Energiesparmodus: {error}",
"hardware_power_saving_hdmi_sleep_description": "Schalten Sie die Aufnahme nach 90 Sekunden Inaktivität aus",
"hardware_power_saving_hdmi_sleep_title": "HDMI-Ruhemodus",
"hardware_power_saving_title": "Energiesparen",
"hardware_time_10_minutes": "10 Minuten", "hardware_time_10_minutes": "10 Minuten",
"hardware_time_1_hour": "1 Stunde", "hardware_time_1_hour": "1 Stunde",
"hardware_time_1_minute": "1 Minute", "hardware_time_1_minute": "1 Minute",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Deregister from Cloud",
"deregister_headline": "Deregister {device} from your cloud account", "deregister_headline": "Deregister {device} from your cloud account",
"detach": "Detach", "detach": "Detach",
"dhcp_empty_lease_description": "We haven't received any DHCP lease information from the device yet.", "dhcp_empty_lease_description": "We haven't received any DHCP lease information from the device yet.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Display Orientation", "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_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_page_description": "Configure display settings and hardware options for your JetKVM device",
"hardware_power_saving_description": "Reduce power consumption when not in use",
"hardware_power_saving_disabled": "Power saving mode disabled",
"hardware_power_saving_enabled": "Power saving mode enabled",
"hardware_power_saving_failed_error": "Failed to set power saving mode: {error}",
"hardware_power_saving_hdmi_sleep_description": "Turn off capture after 90 seconds of inactivity",
"hardware_power_saving_hdmi_sleep_title": "HDMI Sleep Mode",
"hardware_power_saving_title": "Power Saving",
"hardware_time_10_minutes": "10 Minutes", "hardware_time_10_minutes": "10 Minutes",
"hardware_time_1_hour": "1 Hour", "hardware_time_1_hour": "1 Hour",
"hardware_time_1_minute": "1 Minute", "hardware_time_1_minute": "1 Minute",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Darse de baja de la nube",
"deregister_headline": "Anular el registro de {device} en su cuenta en la nube", "deregister_headline": "Anular el registro de {device} en su cuenta en la nube",
"detach": "Despegar", "detach": "Despegar",
"dhcp_empty_lease_description": "Aún no hemos recibido ninguna información de concesión de DHCP del dispositivo.", "dhcp_empty_lease_description": "Aún no hemos recibido ninguna información de concesión de DHCP del dispositivo.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Orientación de la pantalla", "hardware_display_orientation_title": "Orientación de la pantalla",
"hardware_display_wake_up_note": "La pantalla se activará cuando cambie el estado de la conexión o cuando se toque.", "hardware_display_wake_up_note": "La pantalla se activará cuando cambie el estado de la conexión o cuando se toque.",
"hardware_page_description": "Configure los ajustes de pantalla y las opciones de hardware para su dispositivo JetKVM", "hardware_page_description": "Configure los ajustes de pantalla y las opciones de hardware para su dispositivo JetKVM",
"hardware_power_saving_description": "Reduce el consumo de energía cuando no esté en uso",
"hardware_power_saving_disabled": "Modo de ahorro de energía deshabilitado",
"hardware_power_saving_enabled": "Modo de ahorro de energía habilitado",
"hardware_power_saving_failed_error": "No se pudo establecer el modo de ahorro de energía: {error}",
"hardware_power_saving_hdmi_sleep_description": "Desactivar la captura después de 90 segundos de inactividad",
"hardware_power_saving_hdmi_sleep_title": "Modo de suspensión HDMI",
"hardware_power_saving_title": "Ahorro de energía",
"hardware_time_10_minutes": "10 minutos", "hardware_time_10_minutes": "10 minutos",
"hardware_time_1_hour": "1 hora", "hardware_time_1_hour": "1 hora",
"hardware_time_1_minute": "1 minuto", "hardware_time_1_minute": "1 minuto",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Se désinscrire du Cloud",
"deregister_headline": "Désinscrivez {device} de votre compte cloud", "deregister_headline": "Désinscrivez {device} de votre compte cloud",
"detach": "Détacher", "detach": "Détacher",
"dhcp_empty_lease_description": "Nous n'avons pas encore reçu d'informations sur le bail DHCP de l'appareil.", "dhcp_empty_lease_description": "Nous n'avons pas encore reçu d'informations sur le bail DHCP de l'appareil.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Orientation de l'affichage", "hardware_display_orientation_title": "Orientation de l'affichage",
"hardware_display_wake_up_note": "L'écran se réveille lorsque l'état de connexion change ou lorsqu'il est touché.", "hardware_display_wake_up_note": "L'écran se réveille lorsque l'état de connexion change ou lorsqu'il est touché.",
"hardware_page_description": "Configurer les paramètres d'affichage et les options matérielles de votre périphérique JetKVM", "hardware_page_description": "Configurer les paramètres d'affichage et les options matérielles de votre périphérique JetKVM",
"hardware_power_saving_description": "Réduisez la consommation d'énergie lorsque vous ne l'utilisez pas",
"hardware_power_saving_disabled": "Mode d'économie d'énergie désactivé",
"hardware_power_saving_enabled": "Mode d'économie d'énergie activé",
"hardware_power_saving_failed_error": "Échec de la définition du mode d'économie d'énergie : {error}",
"hardware_power_saving_hdmi_sleep_description": "Désactiver la capture après 90 secondes d'inactivité",
"hardware_power_saving_hdmi_sleep_title": "Mode veille HDMI",
"hardware_power_saving_title": "Économie d'énergie",
"hardware_time_10_minutes": "10 minutes", "hardware_time_10_minutes": "10 minutes",
"hardware_time_1_hour": "1 heure", "hardware_time_1_hour": "1 heure",
"hardware_time_1_minute": "1 minute", "hardware_time_1_minute": "1 minute",
@ -588,7 +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_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",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Annulla registrazione dal cloud",
"deregister_headline": "Annulla la registrazione di {device} dal tuo account cloud", "deregister_headline": "Annulla la registrazione di {device} dal tuo account cloud",
"detach": "Staccare", "detach": "Staccare",
"dhcp_empty_lease_description": "Non abbiamo ancora ricevuto alcuna informazione di lease DHCP dal dispositivo.", "dhcp_empty_lease_description": "Non abbiamo ancora ricevuto alcuna informazione di lease DHCP dal dispositivo.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Orientamento dello schermo", "hardware_display_orientation_title": "Orientamento dello schermo",
"hardware_display_wake_up_note": "Il display si riattiverà quando cambia lo stato della connessione o quando viene toccato.", "hardware_display_wake_up_note": "Il display si riattiverà quando cambia lo stato della connessione o quando viene toccato.",
"hardware_page_description": "Configura le impostazioni di visualizzazione e le opzioni hardware per il tuo dispositivo JetKVM", "hardware_page_description": "Configura le impostazioni di visualizzazione e le opzioni hardware per il tuo dispositivo JetKVM",
"hardware_power_saving_description": "Ridurre il consumo energetico quando non in uso",
"hardware_power_saving_disabled": "Modalità di risparmio energetico disabilitata",
"hardware_power_saving_enabled": "Modalità di risparmio energetico abilitata",
"hardware_power_saving_failed_error": "Impossibile impostare la modalità di risparmio energetico: {error}",
"hardware_power_saving_hdmi_sleep_description": "Disattiva l'acquisizione dopo 90 secondi di inattività",
"hardware_power_saving_hdmi_sleep_title": "Modalità sospensione HDMI",
"hardware_power_saving_title": "Risparmio energetico",
"hardware_time_10_minutes": "10 minuti", "hardware_time_10_minutes": "10 minuti",
"hardware_time_1_hour": "1 ora", "hardware_time_1_hour": "1 ora",
"hardware_time_1_minute": "1 minuto", "hardware_time_1_minute": "1 minuto",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Avregistrer deg fra skyen",
"deregister_headline": "Avregistrer {device} fra skykontoen din", "deregister_headline": "Avregistrer {device} fra skykontoen din",
"detach": "Løsne", "detach": "Løsne",
"dhcp_empty_lease_description": "Vi har ikke mottatt noen DHCP-leaseinformasjon fra enheten ennå.", "dhcp_empty_lease_description": "Vi har ikke mottatt noen DHCP-leaseinformasjon fra enheten ennå.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Skjermretning", "hardware_display_orientation_title": "Skjermretning",
"hardware_display_wake_up_note": "Skjermen vil våkne når tilkoblingsstatusen endres, eller når den berøres.", "hardware_display_wake_up_note": "Skjermen vil våkne når tilkoblingsstatusen endres, eller når den berøres.",
"hardware_page_description": "Konfigurer skjerminnstillinger og maskinvarealternativer for JetKVM-enheten din", "hardware_page_description": "Konfigurer skjerminnstillinger og maskinvarealternativer for JetKVM-enheten din",
"hardware_power_saving_description": "Reduser strømforbruket når det ikke er i bruk",
"hardware_power_saving_disabled": "Strømsparingsmodus deaktivert",
"hardware_power_saving_enabled": "Strømsparingsmodus aktivert",
"hardware_power_saving_failed_error": "Kunne ikke angi strømsparingsmodus: {error}",
"hardware_power_saving_hdmi_sleep_description": "Slå av opptak etter 90 sekunder med inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-hvilemodus",
"hardware_power_saving_title": "Strømsparing",
"hardware_time_10_minutes": "10 minutter", "hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time", "hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minutt", "hardware_time_1_minute": "1 minutt",

View File

@ -202,10 +202,10 @@
"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_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.",
"deregister_from_cloud": "Avregistrera dig från molnet",
"deregister_headline": "Avregistrera {device} från ditt molnkonto", "deregister_headline": "Avregistrera {device} från ditt molnkonto",
"detach": "Lösgöra", "detach": "Lösgöra",
"dhcp_empty_lease_description": "Vi har inte mottagit någon DHCP-leaseinformation från enheten ännu.", "dhcp_empty_lease_description": "Vi har inte mottagit någon DHCP-leaseinformation från enheten ännu.",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "Skärmorientering", "hardware_display_orientation_title": "Skärmorientering",
"hardware_display_wake_up_note": "Skärmen vaknar när anslutningsstatusen ändras eller när den berörs.", "hardware_display_wake_up_note": "Skärmen vaknar när anslutningsstatusen ändras eller när den berörs.",
"hardware_page_description": "Konfigurera skärminställningar och maskinvarualternativ för din JetKVM-enhet", "hardware_page_description": "Konfigurera skärminställningar och maskinvarualternativ för din JetKVM-enhet",
"hardware_power_saving_description": "Minska strömförbrukningen när den inte används",
"hardware_power_saving_disabled": "Energisparläge inaktiverat",
"hardware_power_saving_enabled": "Energisparläge aktiverat",
"hardware_power_saving_failed_error": "Misslyckades med att ställa in energisparläge: {error}",
"hardware_power_saving_hdmi_sleep_description": "Stäng av inspelning efter 90 sekunders inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-viloläge",
"hardware_power_saving_title": "Energisparande",
"hardware_time_10_minutes": "10 minuter", "hardware_time_10_minutes": "10 minuter",
"hardware_time_1_hour": "1 timme", "hardware_time_1_hour": "1 timme",
"hardware_time_1_minute": "1 minut", "hardware_time_1_minute": "1 minut",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "电压", "dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"delete": "删除", "delete": "删除",
"deregister_from_cloud": "从云端注销",
"deregister_cloud_devices": "云设备", "deregister_cloud_devices": "云设备",
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问", "deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
"deregister_error": "注销您的设备时出现错误{status} 。请重试。", "deregister_error": "注销您的设备时出现错误{status} 。请重试。",
"deregister_from_cloud": "从云端注销",
"deregister_headline": "从您的云帐户中取消注册{device}", "deregister_headline": "从您的云帐户中取消注册{device}",
"detach": "分离", "detach": "分离",
"dhcp_empty_lease_description": "我们尚未收到来自该设备的任何 DHCP 租约信息。", "dhcp_empty_lease_description": "我们尚未收到来自该设备的任何 DHCP 租约信息。",
@ -298,13 +298,6 @@
"hardware_display_orientation_title": "显示方向", "hardware_display_orientation_title": "显示方向",
"hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。", "hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。",
"hardware_page_description": "为您的 JetKVM 设备配置显示设置和硬件选项", "hardware_page_description": "为您的 JetKVM 设备配置显示设置和硬件选项",
"hardware_power_saving_description": "不使用时减少功耗",
"hardware_power_saving_disabled": "省电模式已禁用",
"hardware_power_saving_enabled": "启用省电模式",
"hardware_power_saving_failed_error": "无法设置省电模式: {error}",
"hardware_power_saving_hdmi_sleep_description": "90 秒不活动后关闭捕获",
"hardware_power_saving_hdmi_sleep_title": "HDMI睡眠模式",
"hardware_power_saving_title": "节能",
"hardware_time_10_minutes": "10分钟", "hardware_time_10_minutes": "10分钟",
"hardware_time_1_hour": "1小时", "hardware_time_1_hour": "1小时",
"hardware_time_1_minute": "1分钟", "hardware_time_1_minute": "1分钟",

64
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.18.0100", "version": "2025.10.15.1700",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.18.0100", "version": "2025.10.15.1700",
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2", "@headlessui/tailwindcss": "^0.2.2",
@ -35,7 +35,7 @@
"react-simple-keyboard": "^3.8.130", "react-simple-keyboard": "^3.8.130",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.3.0", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.15",
@ -44,7 +44,7 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.37.0",
"@inlang/cli": "^3.0.12", "@inlang/cli": "^3.0.12",
"@inlang/paraglide-js": "^2.4.0", "@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-m-function-matcher": "^2.1.0",
@ -62,7 +62,7 @@
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.1",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.38.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
@ -72,7 +72,7 @@
"globals": "^16.4.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1", "prettier-plugin-tailwindcss": "^0.7.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.10", "vite": "^7.1.10",
@ -126,6 +126,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@ -820,12 +821,12 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.21.1", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.7", "@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1", "debug": "^4.3.1",
"minimatch": "^3.1.2" "minimatch": "^3.1.2"
}, },
@ -834,9 +835,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.4.1", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.16.0" "@eslint/core": "^0.16.0"
@ -893,9 +894,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.38.0", "version": "9.37.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -905,9 +906,9 @@
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
"version": "2.1.7", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3970,24 +3971,25 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.38.0", "version": "9.37.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.1", "@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.4.1", "@eslint/config-helpers": "^0.4.0",
"@eslint/core": "^0.16.0", "@eslint/core": "^0.16.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.38.0", "@eslint/js": "9.37.0",
"@eslint/plugin-kit": "^0.4.0", "@eslint/plugin-kit": "^0.4.0",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@ -6328,9 +6330,9 @@
} }
}, },
"node_modules/prettier-plugin-tailwindcss": { "node_modules/prettier-plugin-tailwindcss": {
"version": "0.7.1", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.1.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.0.tgz",
"integrity": "sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==", "integrity": "sha512-zpRZhkfwq1cNmbKhmKzXKuKFdkgXZXlf6p+KttD75v6pGz1FxmcKMc4RKdw97GYBKBbout4113HSLaBJAomFDw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -6612,9 +6614,9 @@
} }
}, },
"node_modules/recharts": { "node_modules/recharts": {
"version": "3.3.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz",
"integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x", "@reduxjs/toolkit": "1.x.x || 2.x.x",

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.18.0100", "version": "2025.10.15.1700",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.20.0" "node": "^22.20.0"
@ -54,7 +54,7 @@
"react-simple-keyboard": "^3.8.130", "react-simple-keyboard": "^3.8.130",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.3.0", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.15",
@ -63,7 +63,7 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.37.0",
"@inlang/cli": "^3.0.12", "@inlang/cli": "^3.0.12",
"@inlang/paraglide-js": "^2.4.0", "@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-m-function-matcher": "^2.1.0",
@ -81,7 +81,7 @@
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.1",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.38.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
@ -91,7 +91,7 @@
"globals": "^16.4.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1", "prettier-plugin-tailwindcss": "^0.7.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.10", "vite": "^7.1.10",

View File

@ -1,13 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { BacklightSettings, useSettingsStore } from "@hooks/stores"; import { BacklightSettings, useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Checkbox } from "@components/Checkbox";
import { FeatureFlag } from "@components/FeatureFlag"; import { FeatureFlag } from "@components/FeatureFlag";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { UsbInfoSetting } from "@components/UsbInfoSetting"; import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications"; import notifications from "@/notifications";
@ -17,7 +15,6 @@ export default function SettingsHardwareRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const settings = useSettingsStore(); const settings = useSettingsStore();
const { displayRotation, setDisplayRotation } = useSettingsStore(); const { displayRotation, setDisplayRotation } = useSettingsStore();
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
const handleDisplayRotationChange = (rotation: string) => { const handleDisplayRotationChange = (rotation: string) => {
setDisplayRotation(rotation); setDisplayRotation(rotation);
@ -76,19 +73,6 @@ export default function SettingsHardwareRoute() {
handleBacklightSettingsChange(settings); handleBacklightSettingsChange(settings);
}; };
const handlePowerSavingChange = (enabled: boolean) => {
setPowerSavingEnabled(enabled);
const duration = enabled ? 90 : -1;
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data ||m.unknown_error() }));
setPowerSavingEnabled(!enabled); // Attempt to revert on error
return;
}
notifications.success(enabled ? m.hardware_power_saving_enabled() : m.hardware_power_saving_disabled());
});
};
useEffect(() => { useEffect(() => {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => { send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
@ -101,17 +85,6 @@ export default function SettingsHardwareRoute() {
}); });
}, [send, setBacklightSettings]); }, [send, setBacklightSettings]);
useEffect(() => {
send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to get power saving mode:", resp.error);
return;
}
const result = resp.result as { enabled: boolean; duration: number };
setPowerSavingEnabled(result.duration >= 0);
});
}, [send]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
@ -151,7 +124,7 @@ export default function SettingsHardwareRoute() {
{ value: "64", label: m.hardware_display_brightness_high() }, { value: "64", label: m.hardware_display_brightness_high() },
]} ]}
onChange={e => { onChange={e => {
handleBacklightMaxBrightnessChange(Number.parseInt(e.target.value)); handleBacklightMaxBrightnessChange(parseInt(e.target.value));
}} }}
/> />
</SettingsItem> </SettingsItem>
@ -174,7 +147,7 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: m.hardware_time_1_hour() }, { value: "3600", label: m.hardware_time_1_hour() },
]} ]}
onChange={e => { onChange={e => {
handleBacklightDimAfterChange(Number.parseInt(e.target.value)); handleBacklightDimAfterChange(parseInt(e.target.value));
}} }}
/> />
</SettingsItem> </SettingsItem>
@ -194,7 +167,7 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: m.hardware_time_1_hour() }, { value: "3600", label: m.hardware_time_1_hour() },
]} ]}
onChange={e => { onChange={e => {
handleBacklightOffAfterChange(Number.parseInt(e.target.value)); handleBacklightOffAfterChange(parseInt(e.target.value));
}} }}
/> />
</SettingsItem> </SettingsItem>
@ -205,26 +178,6 @@ export default function SettingsHardwareRoute() {
</p> </p>
</div> </div>
<FeatureFlag minAppVersion="0.4.9">
<div className="space-y-4">
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsSectionHeader
title={m.hardware_power_saving_title()}
description={m.hardware_power_saving_description()}
/>
<SettingsItem
badge={m.experimental()}
title={m.hardware_power_saving_hdmi_sleep_title()}
description={m.hardware_power_saving_hdmi_sleep_description()}
>
<Checkbox
checked={powerSavingEnabled}
onChange={(e) => handlePowerSavingChange(e.target.checked)}
/>
</SettingsItem>
</div>
</FeatureFlag>
<FeatureFlag minAppVersion="0.3.8"> <FeatureFlag minAppVersion="0.3.8">
<UsbDeviceSetting /> <UsbDeviceSetting />
</FeatureFlag> </FeatureFlag>

View File

@ -261,7 +261,7 @@ export default function SettingsNetworkRoute() {
}); });
} }
if (dirty.ipv6_static?.gateway) { if (dirty.ipv4_static?.gateway) {
changes.push({ changes.push({
label: m.network_ipv6_gateway(), label: m.network_ipv6_gateway(),
from: initialSettingsRef.current?.ipv6_static?.gateway as string, from: initialSettingsRef.current?.ipv6_static?.gateway as string,
@ -273,7 +273,7 @@ export default function SettingsNetworkRoute() {
changes.push({ changes.push({
label: m.network_ipv6_dns(), label: m.network_ipv6_dns(),
from: initialSettingsRef.current?.ipv6_static?.dns.join(", ").toString() ?? "", from: initialSettingsRef.current?.ipv6_static?.dns.join(", ").toString() ?? "",
to: data.ipv6_static?.dns.join(", ").toString() ?? "", to: data.ipv4_static?.dns.join(", ").toString() ?? "",
}); });
} }