mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
b8112995fc
...
13aff1109a
| Author | SHA1 | Date |
|---|---|---|
|
|
13aff1109a | |
|
|
748155d815 | |
|
|
cc9ff74276 | |
|
|
b144d9926f |
|
|
@ -104,6 +104,7 @@ type Config struct {
|
|||
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||
NetworkConfig *network.NetworkConfig `json:"network_config"`
|
||||
DefaultLogLevel string `json:"default_log_level"`
|
||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type Native struct {
|
|||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
onIndevEvent func(event string)
|
||||
onRpcEvent func(event string)
|
||||
sleepModeSupported bool
|
||||
videoLock sync.Mutex
|
||||
screenLock sync.Mutex
|
||||
}
|
||||
|
|
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
|
|||
}
|
||||
}
|
||||
|
||||
sleepModeSupported := isSleepModeSupported()
|
||||
|
||||
return &Native{
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
|
|
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
onVideoFrameReceived: onVideoFrameReceived,
|
||||
onIndevEvent: onIndevEvent,
|
||||
onRpcEvent: onRpcEvent,
|
||||
sleepModeSupported: sleepModeSupported,
|
||||
videoLock: sync.Mutex{},
|
||||
screenLock: sync.Mutex{},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||
|
||||
// VideoState is the state of the video stream.
|
||||
type VideoState struct {
|
||||
Ready bool `json:"ready"`
|
||||
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
|
||||
|
|
@ -8,6 +15,58 @@ type VideoState struct {
|
|||
FramePerSecond float64 `json:"fps"`
|
||||
}
|
||||
|
||||
func isSleepModeSupported() bool {
|
||||
_, err := os.Stat(sleepModeFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (n *Native) setSleepMode(enabled bool) error {
|
||||
if !n.sleepModeSupported {
|
||||
return nil
|
||||
}
|
||||
|
||||
bEnabled := "0"
|
||||
if enabled {
|
||||
bEnabled = "1"
|
||||
}
|
||||
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
|
||||
}
|
||||
|
||||
func (n *Native) getSleepMode() (bool, error) {
|
||||
if !n.sleepModeSupported {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(sleepModeFile)
|
||||
if err == nil {
|
||||
return string(data) == "1", nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// VideoSetSleepMode sets the sleep mode for the video stream.
|
||||
func (n *Native) VideoSetSleepMode(enabled bool) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.setSleepMode(enabled)
|
||||
}
|
||||
|
||||
// VideoGetSleepMode gets the sleep mode for the video stream.
|
||||
func (n *Native) VideoGetSleepMode() (bool, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.getSleepMode()
|
||||
}
|
||||
|
||||
// VideoSleepModeSupported checks if the sleep mode is supported.
|
||||
func (n *Native) VideoSleepModeSupported() bool {
|
||||
return n.sleepModeSupported
|
||||
}
|
||||
|
||||
// VideoSetQualityFactor sets the quality factor for the video stream.
|
||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
|
|||
return videoSetStreamQualityFactor(factor)
|
||||
}
|
||||
|
||||
// VideoGetQualityFactor gets the quality factor for the video stream.
|
||||
func (n *Native) VideoGetQualityFactor() (float64, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
|
|||
return videoGetStreamQualityFactor()
|
||||
}
|
||||
|
||||
// VideoSetEDID sets the EDID for the video stream.
|
||||
func (n *Native) VideoSetEDID(edid string) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
|
|||
return videoSetEDID(edid)
|
||||
}
|
||||
|
||||
// VideoGetEDID gets the EDID for the video stream.
|
||||
func (n *Native) VideoGetEDID() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
|
|||
return videoGetEDID()
|
||||
}
|
||||
|
||||
// VideoLogStatus gets the log status for the video stream.
|
||||
func (n *Native) VideoLogStatus() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
|
|||
return videoLogStatus(), nil
|
||||
}
|
||||
|
||||
// VideoStop stops the video stream.
|
||||
func (n *Native) VideoStop() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// VideoStart starts the video stream.
|
||||
func (n *Native) VideoStart() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
// disable sleep mode before starting video
|
||||
_ = n.setSleepMode(false)
|
||||
|
||||
videoStart()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getEDID": {Func: rpcGetEDID},
|
||||
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
|
||||
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
|
||||
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
|
||||
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
|
||||
"getDevChannelState": {Func: rpcGetDevChannelState},
|
||||
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
||||
"getLocalVersion": {Func: rpcGetLocalVersion},
|
||||
|
|
|
|||
3
main.go
3
main.go
|
|
@ -77,6 +77,9 @@ func Main() {
|
|||
// initialize display
|
||||
initDisplay()
|
||||
|
||||
// start video sleep mode timer
|
||||
startVideoSleepModeTicker()
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,17 @@
|
|||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
},
|
||||
"plugin.inlang.mFunctionMatcher": {
|
||||
"matchers": [
|
||||
{
|
||||
"type": "m-function",
|
||||
"function": "plural",
|
||||
"parameter": "count"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strategy": [
|
||||
"cookie",
|
||||
"localStorage",
|
||||
"preferredLanguage",
|
||||
"baseLocale"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Du har ændret din adgangskode til beskyttelse af din lokale enhed. Husk din nye adgangskode til senere brug.",
|
||||
"local_auth_success_password_updated_title": "Adgangskode opdateret",
|
||||
"local_auth_update_password_button": "Opdater adgangskode",
|
||||
"locale_auto": "Bil",
|
||||
"locale_change_success": "Sproget er ændret til {locale}",
|
||||
"locale_da": "Dansk",
|
||||
"locale_de": "Tysk",
|
||||
"locale_en": "Engelsk",
|
||||
"locale_es": "Spansk",
|
||||
"locale_fr": "Fransk",
|
||||
"locale_it": "Italiensk",
|
||||
"locale_nb": "Norsk (bokmål)",
|
||||
"locale_sv": "Svensk",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Log ind",
|
||||
"log_out": "Log ud",
|
||||
"logged_in_as": "Logget ind som",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Afbrudt",
|
||||
"usb_state_low_power_mode": "Lavstrømstilstand",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Vælg det sprog, der skal bruges i JetKVM-brugergrænsefladen",
|
||||
"user_interface_language_title": "Grænsefladesprog",
|
||||
"video_brightness_description": "Lysstyrkeniveau ( {value} x)",
|
||||
"video_brightness_title": "Lysstyrke",
|
||||
"video_contrast_description": "Kontrastniveau ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Sie haben Ihr lokales Geräteschutzkennwort erfolgreich geändert. Merken Sie sich das neue Kennwort für zukünftige Zugriffe.",
|
||||
"local_auth_success_password_updated_title": "Passwort erfolgreich aktualisiert",
|
||||
"local_auth_update_password_button": "Kennwort aktualisieren",
|
||||
"locale_auto": "Auto",
|
||||
"locale_change_success": "Die Sprache wurde erfolgreich in {locale} geändert.",
|
||||
"locale_da": "Dänisch",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "Englisch",
|
||||
"locale_es": "Spanisch",
|
||||
"locale_fr": "Deutsch",
|
||||
"locale_it": "Italienisch",
|
||||
"locale_nb": "Norwegisch (bokmål)",
|
||||
"locale_sv": "Schwedisch",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Einloggen",
|
||||
"log_out": "Ausloggen",
|
||||
"logged_in_as": "Angemeldet als",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Getrennt",
|
||||
"usb_state_low_power_mode": "Energiesparmodus",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Wählen Sie die Sprache aus, die in der JetKVM-Benutzeroberfläche verwendet werden soll",
|
||||
"user_interface_language_title": "Schnittstellensprache",
|
||||
"video_brightness_description": "Helligkeitsstufe ( {value} x)",
|
||||
"video_brightness_title": "Helligkeit",
|
||||
"video_contrast_description": "Kontraststufe ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
|
||||
"local_auth_success_password_updated_title": "Password Updated Successfully",
|
||||
"local_auth_update_password_button": "Update Password",
|
||||
"locale_auto": "Auto",
|
||||
"locale_change_success": "Language changed successfully to {locale}",
|
||||
"locale_da": "Dansk",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "English",
|
||||
"locale_es": "Español",
|
||||
"locale_fr": "Français",
|
||||
"locale_it": "Italiano",
|
||||
"locale_nb": "Norsk (bokmål)",
|
||||
"locale_sv": "Svenska",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Log In",
|
||||
"log_out": "Log out",
|
||||
"logged_in_as": "Logged in as",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Disconnected",
|
||||
"usb_state_low_power_mode": "Low power mode",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Select the language to use in the JetKVM user interface",
|
||||
"user_interface_language_title": "Interface Language",
|
||||
"video_brightness_description": "Brightness level ({value}x)",
|
||||
"video_brightness_title": "Brightness",
|
||||
"video_contrast_description": "Contrast level ({value}x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Has cambiado correctamente la contraseña de protección de tu dispositivo local. Recuerda la nueva contraseña para acceder en el futuro.",
|
||||
"local_auth_success_password_updated_title": "Contraseña actualizada exitosamente",
|
||||
"local_auth_update_password_button": "Actualizar contraseña",
|
||||
"locale_auto": "Auto",
|
||||
"locale_change_success": "El idioma se cambió correctamente a {locale}",
|
||||
"locale_da": "Danés",
|
||||
"locale_de": "Alemán",
|
||||
"locale_en": "Inglés",
|
||||
"locale_es": "Español",
|
||||
"locale_fr": "Francés",
|
||||
"locale_it": "Italiano",
|
||||
"locale_nb": "Noruego (bokmål)",
|
||||
"locale_sv": "Sueco",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Acceso",
|
||||
"log_out": "Finalizar la sesión",
|
||||
"logged_in_as": "Inició sesión como",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Desconectado",
|
||||
"usb_state_low_power_mode": "Modo de bajo consumo",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Seleccione el idioma que se utilizará en la interfaz de usuario de JetKVM",
|
||||
"user_interface_language_title": "Lenguaje de interfaz",
|
||||
"video_brightness_description": "Nivel de brillo ( {value} x)",
|
||||
"video_brightness_title": "Brillo",
|
||||
"video_contrast_description": "Nivel de contraste ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Vous avez modifié avec succès le mot de passe de protection de votre appareil local. N'oubliez pas de le mémoriser pour y accéder ultérieurement.",
|
||||
"local_auth_success_password_updated_title": "Mot de passe mis à jour avec succès",
|
||||
"local_auth_update_password_button": "Mettre à jour le mot de passe",
|
||||
"locale_auto": "Auto",
|
||||
"locale_change_success": "La langue a été modifiée avec succès en {locale}",
|
||||
"locale_da": "danois",
|
||||
"locale_de": "Allemand",
|
||||
"locale_en": "Anglais",
|
||||
"locale_es": "Espagnol",
|
||||
"locale_fr": "Français",
|
||||
"locale_it": "italien",
|
||||
"locale_nb": "Norvégien (bokmål)",
|
||||
"locale_sv": "suédois",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Se connecter",
|
||||
"log_out": "Se déconnecter",
|
||||
"logged_in_as": "Connecté en tant que",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Déconnecté",
|
||||
"usb_state_low_power_mode": "Mode basse consommation",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Sélectionnez la langue à utiliser dans l'interface utilisateur de JetKVM",
|
||||
"user_interface_language_title": "Langue de l'interface",
|
||||
"video_brightness_description": "Niveau de luminosité ( {value} x)",
|
||||
"video_brightness_title": "Luminosité",
|
||||
"video_contrast_description": "Niveau de contraste ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Hai modificato correttamente la password di protezione del tuo dispositivo locale. Assicurati di ricordare la nuova password per gli accessi futuri.",
|
||||
"local_auth_success_password_updated_title": "Password aggiornata con successo",
|
||||
"local_auth_update_password_button": "Aggiorna password",
|
||||
"locale_auto": "Auto",
|
||||
"locale_change_success": "Lingua modificata correttamente in {locale}",
|
||||
"locale_da": "Danese",
|
||||
"locale_de": "Tedesco",
|
||||
"locale_en": "Inglese",
|
||||
"locale_es": "Spagnolo",
|
||||
"locale_fr": "Francese",
|
||||
"locale_it": "Italiano",
|
||||
"locale_nb": "Norvegese (bokmål)",
|
||||
"locale_sv": "Svedese",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Login",
|
||||
"log_out": "Disconnetti",
|
||||
"logged_in_as": "Accedi come",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Disconnesso",
|
||||
"usb_state_low_power_mode": "Modalità a basso consumo",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Seleziona la lingua da utilizzare nell'interfaccia utente JetKVM",
|
||||
"user_interface_language_title": "Lingua dell'interfaccia",
|
||||
"video_brightness_description": "Livello di luminosità ( {value} x)",
|
||||
"video_brightness_title": "Luminosità",
|
||||
"video_contrast_description": "Livello di contrasto ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Du har endret passordet for beskyttelse av den lokale enheten. Husk det nye passordet for fremtidig tilgang.",
|
||||
"local_auth_success_password_updated_title": "Passord oppdatert",
|
||||
"local_auth_update_password_button": "Oppdater passord",
|
||||
"locale_auto": "Bil",
|
||||
"locale_change_success": "Språket er endret til {locale}",
|
||||
"locale_da": "Dansk",
|
||||
"locale_de": "Tysk",
|
||||
"locale_en": "Engelsk",
|
||||
"locale_es": "Spansk",
|
||||
"locale_fr": "Fransk",
|
||||
"locale_it": "Italiensk",
|
||||
"locale_nb": "Norsk (bokmål)",
|
||||
"locale_sv": "Svensk",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Logg inn",
|
||||
"log_out": "Logg ut",
|
||||
"logged_in_as": "Logget inn som",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Frakoblet",
|
||||
"usb_state_low_power_mode": "Lavstrømsmodus",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Velg språket som skal brukes i JetKVM-brukergrensesnittet",
|
||||
"user_interface_language_title": "Grensesnittspråk",
|
||||
"video_brightness_description": "Lysstyrkenivå ( {value} x)",
|
||||
"video_brightness_title": "Lysstyrke",
|
||||
"video_contrast_description": "Kontrastnivå ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "Du har ändrat ditt lösenord för lokal enhetsskydd. Se till att komma ihåg ditt nya lösenord för framtida åtkomst.",
|
||||
"local_auth_success_password_updated_title": "Lösenordet har uppdaterats",
|
||||
"local_auth_update_password_button": "Uppdatera lösenord",
|
||||
"locale_auto": "Bil",
|
||||
"locale_change_success": "Språket har ändrats till {locale}",
|
||||
"locale_da": "Danska",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "Engelska",
|
||||
"locale_es": "Spanska",
|
||||
"locale_fr": "Franska",
|
||||
"locale_it": "italiensk",
|
||||
"locale_nb": "Norska (bokmål)",
|
||||
"locale_sv": "Svenska",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "Logga in",
|
||||
"log_out": "Logga ut",
|
||||
"logged_in_as": "Inloggad som",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "Osammanhängande",
|
||||
"usb_state_low_power_mode": "Lågströmsläge",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "Välj språket som ska användas i JetKVM-användargränssnittet",
|
||||
"user_interface_language_title": "Gränssnittsspråk",
|
||||
"video_brightness_description": "Ljusstyrka ( {value} x)",
|
||||
"video_brightness_title": "Ljusstyrka",
|
||||
"video_contrast_description": "Kontrastnivå ( {value} x)",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,17 @@
|
|||
"local_auth_success_password_updated_description": "您已成功更改本地设备保护密码。请务必记住新密码,以便日后访问。",
|
||||
"local_auth_success_password_updated_title": "密码更新成功",
|
||||
"local_auth_update_password_button": "更新密码",
|
||||
"locale_auto": "汽车",
|
||||
"locale_change_success": "语言已成功更改为{locale}",
|
||||
"locale_da": "丹麦语",
|
||||
"locale_de": "德语",
|
||||
"locale_en": "英语",
|
||||
"locale_es": "西班牙语",
|
||||
"locale_fr": "法语",
|
||||
"locale_it": "意大利语",
|
||||
"locale_nb": "挪威语(博克马尔语)",
|
||||
"locale_sv": "瑞典语",
|
||||
"locale_zh": "中文 (简体)",
|
||||
"log_in": "登录",
|
||||
"log_out": "登出",
|
||||
"logged_in_as": "登录身份",
|
||||
|
|
@ -748,6 +759,8 @@
|
|||
"usb_state_disconnected": "断开连接",
|
||||
"usb_state_low_power_mode": "低功耗模式",
|
||||
"usb": "USB",
|
||||
"user_interface_language_description": "选择 JetKVM 用户界面使用的语言",
|
||||
"user_interface_language_title": "界面语言",
|
||||
"video_brightness_description": "亮度级别( {value} x)",
|
||||
"video_brightness_title": "亮度",
|
||||
"video_contrast_description": "对比度级别( {value} x)",
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ export interface MouseMove {
|
|||
y: number;
|
||||
buttons: number;
|
||||
}
|
||||
|
||||
export interface MouseState {
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
|
|
@ -347,8 +348,10 @@ export interface SettingsState {
|
|||
// Video enhancement settings
|
||||
videoSaturation: number;
|
||||
setVideoSaturation: (value: number) => void;
|
||||
|
||||
videoBrightness: number;
|
||||
setVideoBrightness: (value: number) => void;
|
||||
|
||||
videoContrast: number;
|
||||
setVideoContrast: (value: number) => void;
|
||||
}
|
||||
|
|
@ -392,8 +395,10 @@ export const useSettingsStore = create(
|
|||
// Video enhancement settings with default values (1.0 = normal)
|
||||
videoSaturation: 1.0,
|
||||
setVideoSaturation: (value: number) => set({ videoSaturation: value }),
|
||||
|
||||
videoBrightness: 1.0,
|
||||
setVideoBrightness: (value: number) => set({ videoBrightness: value }),
|
||||
|
||||
videoContrast: 1.0,
|
||||
setVideoContrast: (value: number) => set({ videoContrast: value }),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox from "@components/Checkbox";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import notifications from "@/notifications";
|
||||
import { getLocale, setLocale, locales, baseLocale } from '@localizations/runtime.js';
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { deleteCookie, map_locale_code_to_name } from "@/utils";
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -40,6 +43,37 @@ export default function SettingsGeneralRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
const [currentLocale, setCurrentLocale] = useState(getLocale());
|
||||
|
||||
const localeOptions = useMemo(() => {
|
||||
return ["", ...locales]
|
||||
.map((code) => {
|
||||
const [localizedName, nativeName] = map_locale_code_to_name(code);
|
||||
// don't repeat the name if it's the same in both locales (or blank)
|
||||
const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName;
|
||||
return { value: code, label: label }
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleLocaleChange = (newLocale: string) => {
|
||||
if (newLocale === currentLocale) return;
|
||||
|
||||
let validLocale = newLocale as typeof locales[number];
|
||||
|
||||
if (newLocale !== "") {
|
||||
if (!locales.includes(validLocale)) {
|
||||
validLocale = baseLocale;
|
||||
}
|
||||
|
||||
setLocale(validLocale); // tell the i18n system to change locale
|
||||
} else {
|
||||
deleteCookie("JETKVM_LOCALE", "", "/"); // delete the cookie that the i18n system uses to store the locale
|
||||
}
|
||||
|
||||
setCurrentLocale(validLocale);
|
||||
notifications.success(m.locale_change_success({ locale: validLocale || m.locale_auto() }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
|
@ -49,6 +83,20 @@ export default function SettingsGeneralRoute() {
|
|||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.user_interface_language_title()}
|
||||
description={m.user_interface_language_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={currentLocale}
|
||||
options={localeOptions}
|
||||
onChange={e => { handleLocaleChange(e.target.value); }}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title={m.general_check_for_updates()}
|
||||
|
|
@ -82,7 +130,6 @@ export default function SettingsGeneralRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title={m.general_reboot_device()}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { KeySequence } from "@hooks/stores";
|
||||
import { getLocale } from '@localizations/runtime.js';
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export const formatters = {
|
||||
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
|
||||
|
|
@ -254,3 +255,29 @@ export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
|
|||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
export function map_locale_code_to_name(locale: string): [string, string] {
|
||||
// the first is the name in the current app locale (e.g. Inglese),
|
||||
// the second is the name in the language of the locale itself (e.g. English)
|
||||
switch (locale) {
|
||||
case '': return [m.locale_auto(), ""];
|
||||
case 'en': return [m.locale_en(), m.locale_en({}, { locale })];
|
||||
case 'da': return [m.locale_da(), m.locale_da({}, { locale })];
|
||||
case 'de': return [m.locale_de(), m.locale_de({}, { locale })];
|
||||
case 'es': return [m.locale_es(), m.locale_es({}, { locale })];
|
||||
case 'fr': return [m.locale_fr(), m.locale_fr({}, { locale })];
|
||||
case 'it': return [m.locale_it(), m.locale_it({}, { locale })];
|
||||
case 'nb': return [m.locale_nb(), m.locale_nb({}, { locale })];
|
||||
case 'sv': return [m.locale_sv(), m.locale_sv({}, { locale })];
|
||||
case 'zh': return [m.locale_zh(), m.locale_zh({}, { locale })];
|
||||
default: return [locale, ""];
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteCookie(name: string, domain?: string, path = "/") {
|
||||
const domainPart = domain ? `; domain=${domain}` : "";
|
||||
// max-age=0 removes the cookie immediately in modern browsers
|
||||
document.cookie = `${name}=; path=${path}; max-age=0${domainPart}`;
|
||||
// fallback: set an expires in the past for older agents
|
||||
document.cookie = `${name}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT${domainPart}`;
|
||||
}
|
||||
|
|
@ -33,8 +33,7 @@ export default defineConfig(({ mode, command }) => {
|
|||
outdir: "./localization/paraglide",
|
||||
outputStructure: 'message-modules',
|
||||
cookieName: 'JETKVM_LOCALE',
|
||||
localStorageKey: 'JETKVM_LOCALE',
|
||||
strategy: ['cookie', 'localStorage', 'preferredLanguage', 'baseLocale'],
|
||||
strategy: ['cookie', 'preferredLanguage', 'baseLocale'],
|
||||
}))
|
||||
|
||||
return {
|
||||
|
|
|
|||
103
video.go
103
video.go
|
|
@ -1,10 +1,22 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/native"
|
||||
)
|
||||
|
||||
var lastVideoState native.VideoState
|
||||
var (
|
||||
lastVideoState native.VideoState
|
||||
videoSleepModeCtx context.Context
|
||||
videoSleepModeCancel context.CancelFunc
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVideoSleepModeDuration = 1 * time.Minute
|
||||
)
|
||||
|
||||
func triggerVideoStateUpdate() {
|
||||
go func() {
|
||||
|
|
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
|
|||
func rpcGetVideoState() (native.VideoState, error) {
|
||||
return lastVideoState, nil
|
||||
}
|
||||
|
||||
type rpcVideoSleepModeResponse struct {
|
||||
Supported bool `json:"supported"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
|
||||
sleepMode, _ := nativeInstance.VideoGetSleepMode()
|
||||
return rpcVideoSleepModeResponse{
|
||||
Supported: nativeInstance.VideoSleepModeSupported(),
|
||||
Enabled: sleepMode,
|
||||
Duration: config.VideoSleepAfterSec,
|
||||
}
|
||||
}
|
||||
|
||||
func rpcSetVideoSleepMode(duration int) error {
|
||||
if duration < 0 {
|
||||
duration = -1 // disable
|
||||
}
|
||||
|
||||
config.VideoSleepAfterSec = duration
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
// we won't restart the ticker here,
|
||||
// as the session can't be inactive when this function is called
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopVideoSleepModeTicker() {
|
||||
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
|
||||
|
||||
if videoSleepModeCancel != nil {
|
||||
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
|
||||
videoSleepModeCancel()
|
||||
videoSleepModeCancel = nil
|
||||
videoSleepModeCtx = nil
|
||||
}
|
||||
}
|
||||
|
||||
func startVideoSleepModeTicker() {
|
||||
if !nativeInstance.VideoSleepModeSupported() {
|
||||
return
|
||||
}
|
||||
|
||||
var duration time.Duration
|
||||
|
||||
if config.VideoSleepAfterSec == 0 {
|
||||
duration = defaultVideoSleepModeDuration
|
||||
} else if config.VideoSleepAfterSec > 0 {
|
||||
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
|
||||
} else {
|
||||
stopVideoSleepModeTicker()
|
||||
return
|
||||
}
|
||||
|
||||
// Stop any existing timer and goroutine
|
||||
stopVideoSleepModeTicker()
|
||||
|
||||
// Create new context for this ticker
|
||||
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
|
||||
|
||||
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
|
||||
}
|
||||
|
||||
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if getActiveSessions() > 0 {
|
||||
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
|
||||
continue
|
||||
}
|
||||
|
||||
nativeLogger.Trace().Msg("entering HDMI sleep mode")
|
||||
_ = nativeInstance.VideoSetSleepMode(true)
|
||||
case <-ctx.Done():
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
webrtc.go
38
webrtc.go
|
|
@ -39,6 +39,34 @@ type Session struct {
|
|||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
var (
|
||||
actionSessions int = 0
|
||||
activeSessionsMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func incrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions++
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func decrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions--
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func getActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func (s *Session) resetKeepAliveTime() {
|
||||
s.keepAliveJitterLock.Lock()
|
||||
defer s.keepAliveJitterLock.Unlock()
|
||||
|
|
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
if connectionState == webrtc.ICEConnectionStateConnected {
|
||||
if !isConnected {
|
||||
isConnected = true
|
||||
actionSessions++
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 1 {
|
||||
if incrActiveSessions() == 1 {
|
||||
onFirstSessionConnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
}
|
||||
if isConnected {
|
||||
isConnected = false
|
||||
actionSessions--
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 0 {
|
||||
if decrActiveSessions() == 0 {
|
||||
onLastSessionDisconnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
return session, nil
|
||||
}
|
||||
|
||||
var actionSessions = 0
|
||||
|
||||
func onActiveSessionsChanged() {
|
||||
requestDisplayUpdate(true, "active_sessions_changed")
|
||||
}
|
||||
|
||||
func onFirstSessionConnected() {
|
||||
_ = nativeInstance.VideoStart()
|
||||
stopVideoSleepModeTicker()
|
||||
}
|
||||
|
||||
func onLastSessionDisconnected() {
|
||||
_ = nativeInstance.VideoStop()
|
||||
startVideoSleepModeTicker()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue