Compare commits

..

6 Commits

Author SHA1 Message Date
Marc Brooks 83b7483cfd
Merge e3af69815b into 2444817455 2025-10-18 03:26:25 +02:00
Marc Brooks e3af69815b
Fix copy-pasta errors
Thanks CoPilot!
2025-10-17 20:26:14 -05:00
Marc Brooks 3391fa6180
Improve developer guidance 2025-10-17 20:16:34 -05:00
Marc Brooks e2321b6f38
Add note to do back-translation 2025-10-17 20:16:34 -05:00
Marc Brooks 81b305d625
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 20:16:34 -05:00
Aveline 2444817455
chore: disable sleep mode when detecting video format (#887)
Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
Co-authored-by: Adam Shiervani <adamshiervani@fastmail.com>
2025-10-17 17:51:02 +02:00
24 changed files with 373 additions and 125 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
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. 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.
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()`
- **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 })`
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.
### Adding a new language

View File

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

View File

@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status();
}
int jetkvm_video_init() {
return video_init();
int jetkvm_video_init(float factor) {
return video_init(factor);
}
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);
int jetkvm_video_init();
int jetkvm_video_init(float quality_factor);
void jetkvm_video_shutdown();
void jetkvm_video_start();
void jetkvm_video_stop();

View File

@ -29,6 +29,7 @@
#define VIDEO_DEV "/dev/video0"
#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_2(x) RK_ALIGN(x, 2)
@ -39,6 +40,7 @@ int sub_dev_fd = -1;
#define VENC_CHANNEL 0
MB_POOL memPool = MB_INVALID_POOLID;
bool sleep_mode_available = false;
bool should_exit = false;
float quality_factor = 1.0f;
@ -51,6 +53,45 @@ RK_U64 get_us()
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)
{
const int32_t base_bitrate_high = 2000;
@ -190,8 +231,15 @@ static int32_t buf_init()
pthread_t *format_thread = NULL;
int video_init()
int video_init(float factor)
{
detect_sleep_mode();
if (factor < 0 || factor > 1) {
factor = 1.0f;
}
quality_factor = factor;
if (RK_MPI_SYS_Init() != RK_SUCCESS)
{
log_error("RK_MPI_SYS_Init failed");
@ -301,11 +349,29 @@ static void *venc_read_stream(void *arg)
}
uint32_t detected_width, detected_height;
bool detected_signal = false, streaming_flag = false;
bool detected_signal = false, streaming_flag = false, streaming_stopped = true;
pthread_t *streaming_thread = NULL;
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)
{
FILE *file = fopen(filename, "wb");
@ -319,6 +385,8 @@ void *run_video_stream(void *arg)
log_info("running video stream");
streaming_stopped = false;
while (streaming_flag)
{
if (detected_signal == false)
@ -401,7 +469,7 @@ void *run_video_stream(void *arg)
{
log_error("get mb blk failed!");
close(video_dev_fd);
return ;
return (void *)errno;
}
log_info("Got memory block for buffer %d", i);
@ -538,6 +606,18 @@ void *run_video_stream(void *arg)
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();
for (int i = 0; i < input_buffer_count; i++)
@ -553,6 +633,9 @@ void *run_video_stream(void *arg)
}
log_info("video stream thread exiting");
streaming_stopped = true;
return NULL;
}
@ -577,61 +660,80 @@ void video_shutdown()
RK_MPI_MB_DestroyPool(memPool);
}
log_info("Destroyed memory pool");
pthread_mutex_destroy(&streaming_mutex);
log_info("Destroyed streaming mutex");
}
void video_start_streaming()
{
pthread_mutex_lock(&streaming_mutex);
log_info("starting video streaming");
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");
goto cleanup;
return;
}
pthread_t *new_thread = malloc(sizeof(pthread_t));
if (new_thread == NULL)
{
log_error("Failed to allocate memory for streaming thread");
goto cleanup;
return;
}
streaming_flag = true;
set_streaming_flag(true);
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
if (result != 0)
{
log_error("Failed to create streaming thread: %s", strerror(result));
streaming_flag = false;
set_streaming_flag(false);
free(new_thread);
goto cleanup;
return;
}
// Only set streaming_thread after successful creation, and before unlocking the mutex
// Only set streaming_thread after successful creation
streaming_thread = new_thread;
cleanup:
pthread_mutex_unlock(&streaming_mutex);
return;
}
void video_stop_streaming()
{
pthread_mutex_lock(&streaming_mutex);
if (streaming_thread != NULL)
{
streaming_flag = false;
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");
if (streaming_thread == NULL) {
log_info("video streaming already stopped");
return;
}
pthread_mutex_unlock(&streaming_mutex);
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");
video_stop_streaming();
}
video_start_streaming();
}
void *run_detect_format(void *arg)
@ -650,6 +752,8 @@ void *run_detect_format(void *arg)
while (!should_exit)
{
ensure_sleep_mode_disabled();
memset(&dv_timings, 0, sizeof(dv_timings));
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
{
@ -689,21 +793,17 @@ void *run_detect_format(void *arg)
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
dv_timings.bt.hbackporch));
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_height = dv_timings.bt.height;
detected_signal = true;
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
pthread_mutex_lock(&streaming_mutex);
if (streaming_flag == true)
{
pthread_mutex_unlock(&streaming_mutex);
log_info("restarting on going video streaming");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
if (should_restart) {
log_info("restarting video streaming due to format change");
video_restart_streaming();
}
}
@ -731,21 +831,9 @@ void video_set_quality_factor(float factor)
quality_factor = factor;
// TODO: update venc bitrate without stopping 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);
}
video_restart_streaming();
}
float video_get_quality_factor() {
return quality_factor;
}
}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,18 @@
package native
import (
"fmt"
"os"
"time"
)
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.
type VideoState struct {
Ready bool `json:"ready"`
@ -66,12 +73,32 @@ func (n *Native) VideoSleepModeSupported() bool {
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.
func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return videoSetStreamQualityFactor(factor)
return n.useExtraLock(func() error {
return videoSetStreamQualityFactor(factor)
})
}
// VideoGetQualityFactor gets the quality factor for the video stream.
@ -87,7 +114,13 @@ func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return videoSetEDID(edid)
if edid == "" {
edid = DefaultEDID
}
return n.useExtraLock(func() error {
return videoSetEDID(edid)
})
}
// VideoGetEDID gets the EDID for the video stream.

View File

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

View File

@ -17,9 +17,10 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(),
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(),
DefaultQualityFactor: config.VideoQualityFactor,
OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state
triggerVideoStateUpdate()
@ -58,7 +59,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
}
},
})
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" {
nativeInstance.DoNotUseThisIsForCrashTestingOnly()

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Spænding",
"dc_power_control_voltage_unit": "V",
"delete": "Slet",
"deregister_from_cloud": "Afregistrering fra Cloud",
"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_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",
"detach": "Løsrive",
"dhcp_empty_lease_description": "Vi har endnu ikke modtaget nogen DHCP-leaseoplysninger fra enheden.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 time",
"hardware_time_1_minute": "1 minut",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Stromspannung",
"dc_power_control_voltage_unit": "V",
"delete": "Löschen",
"deregister_from_cloud": "Abmelden von der Cloud",
"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_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}",
"detach": "Abtrennen",
"dhcp_empty_lease_description": "Wir haben noch keine DHCP-Lease-Informationen vom Gerät erhalten.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 Stunde",
"hardware_time_1_minute": "1 Minute",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Voltage",
"dc_power_control_voltage_unit": "V",
"delete": "Delete",
"deregister_from_cloud": "Deregister from Cloud",
"deregister_cloud_devices": "Cloud Devices",
"deregister_description": "This will remove the device from your cloud account and revoke remote access to it. Please note that local access will still be possible",
"deregister_error": "There was an error {status} deregistering your device. Please try again.",
"deregister_from_cloud": "Deregister from Cloud",
"deregister_headline": "Deregister {device} from your cloud account",
"detach": "Detach",
"dhcp_empty_lease_description": "We haven't received any DHCP lease information from the device yet.",
@ -298,6 +298,13 @@
"hardware_display_orientation_title": "Display Orientation",
"hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.",
"hardware_page_description": "Configure display settings and hardware options for your JetKVM device",
"hardware_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_1_hour": "1 Hour",
"hardware_time_1_minute": "1 Minute",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Voltaje",
"dc_power_control_voltage_unit": "V",
"delete": "Borrar",
"deregister_from_cloud": "Darse de baja de 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_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",
"detach": "Despegar",
"dhcp_empty_lease_description": "Aún no hemos recibido ninguna información de concesión de DHCP del dispositivo.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 hora",
"hardware_time_1_minute": "1 minuto",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Tension",
"dc_power_control_voltage_unit": "V",
"delete": "Supprimer",
"deregister_from_cloud": "Se désinscrire du 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_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",
"detach": "Détacher",
"dhcp_empty_lease_description": "Nous n'avons pas encore reçu d'informations sur le bail DHCP de l'appareil.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 heure",
"hardware_time_1_minute": "1 minute",
@ -581,6 +588,7 @@
"network_description": "Configurez vos paramètres réseau",
"network_dhcp_client_description": "Configurer le client DHCP à utiliser",
"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_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",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Voltaggio",
"dc_power_control_voltage_unit": "V",
"delete": "Eliminare",
"deregister_from_cloud": "Annulla registrazione dal 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_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",
"detach": "Staccare",
"dhcp_empty_lease_description": "Non abbiamo ancora ricevuto alcuna informazione di lease DHCP dal dispositivo.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 ora",
"hardware_time_1_minute": "1 minuto",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Spenning",
"dc_power_control_voltage_unit": "V",
"delete": "Slett",
"deregister_from_cloud": "Avregistrer deg fra skyen",
"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_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",
"detach": "Løsne",
"dhcp_empty_lease_description": "Vi har ikke mottatt noen DHCP-leaseinformasjon fra enheten ennå.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 time",
"hardware_time_1_minute": "1 minutt",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "Spänning",
"dc_power_control_voltage_unit": "V",
"delete": "Radera",
"deregister_from_cloud": "Avregistrera dig från molnet",
"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_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",
"detach": "Lösgöra",
"dhcp_empty_lease_description": "Vi har inte mottagit någon DHCP-leaseinformation från enheten ännu.",
@ -298,6 +298,13 @@
"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_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_1_hour": "1 timme",
"hardware_time_1_minute": "1 minut",

View File

@ -202,10 +202,10 @@
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "V",
"delete": "删除",
"deregister_from_cloud": "从云端注销",
"deregister_cloud_devices": "云设备",
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
"deregister_error": "注销您的设备时出现错误{status} 。请重试。",
"deregister_from_cloud": "从云端注销",
"deregister_headline": "从您的云帐户中取消注册{device}",
"detach": "分离",
"dhcp_empty_lease_description": "我们尚未收到来自该设备的任何 DHCP 租约信息。",
@ -298,6 +298,13 @@
"hardware_display_orientation_title": "显示方向",
"hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。",
"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_1_hour": "1小时",
"hardware_time_1_minute": "1分钟",

64
ui/package-lock.json generated
View File

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

View File

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

View File

@ -1,11 +1,13 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { BacklightSettings, useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Checkbox } from "@components/Checkbox";
import { FeatureFlag } from "@components/FeatureFlag";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications";
@ -15,6 +17,7 @@ export default function SettingsHardwareRoute() {
const { send } = useJsonRpc();
const settings = useSettingsStore();
const { displayRotation, setDisplayRotation } = useSettingsStore();
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
const handleDisplayRotationChange = (rotation: string) => {
setDisplayRotation(rotation);
@ -73,6 +76,19 @@ export default function SettingsHardwareRoute() {
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(() => {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
@ -85,6 +101,17 @@ export default function SettingsHardwareRoute() {
});
}, [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 (
<div className="space-y-4">
<SettingsPageHeader
@ -124,7 +151,7 @@ export default function SettingsHardwareRoute() {
{ value: "64", label: m.hardware_display_brightness_high() },
]}
onChange={e => {
handleBacklightMaxBrightnessChange(parseInt(e.target.value));
handleBacklightMaxBrightnessChange(Number.parseInt(e.target.value));
}}
/>
</SettingsItem>
@ -147,7 +174,7 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: m.hardware_time_1_hour() },
]}
onChange={e => {
handleBacklightDimAfterChange(parseInt(e.target.value));
handleBacklightDimAfterChange(Number.parseInt(e.target.value));
}}
/>
</SettingsItem>
@ -167,7 +194,7 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: m.hardware_time_1_hour() },
]}
onChange={e => {
handleBacklightOffAfterChange(parseInt(e.target.value));
handleBacklightOffAfterChange(Number.parseInt(e.target.value));
}}
/>
</SettingsItem>
@ -178,6 +205,26 @@ export default function SettingsHardwareRoute() {
</p>
</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">
<UsbDeviceSetting />
</FeatureFlag>

View File

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