Compare commits

..

8 Commits

Author SHA1 Message Date
Marc Brooks fe277517c1 Fix update status rendering 2025-10-15 04:51:49 +00:00
Marc Brooks b74f779daa Merged in dev hotfix 2025-10-15 04:18:00 +00:00
Marc Brooks 748155d815
Added setting to choose locale 2025-10-14 22:35:05 -05:00
Marc Brooks 423bf1a53f
Delete unused messages 2025-10-14 18:50:21 -05:00
Marc Brooks 9e822850e0
Fix eslint 2025-10-14 17:15:25 -05:00
Marc Brooks c6a12588f5
Restore some missing blanks 2025-10-14 17:06:49 -05:00
Marc Brooks 75716405d5
Use getLocale for date, relative time, and money formatting 2025-10-14 14:49:45 -05:00
Marc Brooks 5363baa37a
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
2025-10-14 06:41:12 -05:00
8 changed files with 8 additions and 214 deletions

View File

@ -104,7 +104,6 @@ type Config struct {
UsbDevices *usbgadget.Devices `json:"usb_devices"` UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *network.NetworkConfig `json:"network_config"` NetworkConfig *network.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
} }
func (c *Config) GetDisplayRotation() uint16 { func (c *Config) GetDisplayRotation() uint16 {

View File

@ -19,7 +19,6 @@ type Native struct {
onVideoFrameReceived func(frame []byte, duration time.Duration) onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string) onIndevEvent func(event string)
onRpcEvent func(event string) onRpcEvent func(event string)
sleepModeSupported bool
videoLock sync.Mutex videoLock sync.Mutex
screenLock sync.Mutex screenLock sync.Mutex
} }
@ -63,8 +62,6 @@ func NewNative(opts NativeOptions) *Native {
} }
} }
sleepModeSupported := isSleepModeSupported()
return &Native{ return &Native{
ready: make(chan struct{}), ready: make(chan struct{}),
l: nativeLogger, l: nativeLogger,
@ -76,7 +73,6 @@ func NewNative(opts NativeOptions) *Native {
onVideoFrameReceived: onVideoFrameReceived, onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent, onIndevEvent: onIndevEvent,
onRpcEvent: onRpcEvent, onRpcEvent: onRpcEvent,
sleepModeSupported: sleepModeSupported,
videoLock: sync.Mutex{}, videoLock: sync.Mutex{},
screenLock: sync.Mutex{}, screenLock: sync.Mutex{},
} }

View File

@ -1,12 +1,5 @@
package native 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 { type VideoState struct {
Ready bool `json:"ready"` Ready bool `json:"ready"`
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
@ -15,58 +8,6 @@ type VideoState struct {
FramePerSecond float64 `json:"fps"` 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 { func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -74,7 +15,6 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
return videoSetStreamQualityFactor(factor) return videoSetStreamQualityFactor(factor)
} }
// VideoGetQualityFactor gets the quality factor for the video stream.
func (n *Native) VideoGetQualityFactor() (float64, error) { func (n *Native) VideoGetQualityFactor() (float64, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -82,7 +22,6 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
return videoGetStreamQualityFactor() return videoGetStreamQualityFactor()
} }
// VideoSetEDID sets the EDID for the video stream.
func (n *Native) VideoSetEDID(edid string) error { func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -90,7 +29,6 @@ func (n *Native) VideoSetEDID(edid string) error {
return videoSetEDID(edid) return videoSetEDID(edid)
} }
// VideoGetEDID gets the EDID for the video stream.
func (n *Native) VideoGetEDID() (string, error) { func (n *Native) VideoGetEDID() (string, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -98,7 +36,6 @@ func (n *Native) VideoGetEDID() (string, error) {
return videoGetEDID() return videoGetEDID()
} }
// VideoLogStatus gets the log status for the video stream.
func (n *Native) VideoLogStatus() (string, error) { func (n *Native) VideoLogStatus() (string, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -106,7 +43,6 @@ func (n *Native) VideoLogStatus() (string, error) {
return videoLogStatus(), nil return videoLogStatus(), nil
} }
// VideoStop stops the video stream.
func (n *Native) VideoStop() error { func (n *Native) VideoStop() error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -115,14 +51,10 @@ func (n *Native) VideoStop() error {
return nil return nil
} }
// VideoStart starts the video stream.
func (n *Native) VideoStart() error { func (n *Native) VideoStart() error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
// disable sleep mode before starting video
_ = n.setSleepMode(false)
videoStart() videoStart()
return nil return nil
} }

View File

@ -1215,8 +1215,6 @@ var rpcHandlers = map[string]RPCHandler{
"getEDID": {Func: rpcGetEDID}, "getEDID": {Func: rpcGetEDID},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getVideoLogStatus": {Func: rpcGetVideoLogStatus}, "getVideoLogStatus": {Func: rpcGetVideoLogStatus},
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
"getDevChannelState": {Func: rpcGetDevChannelState}, "getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion}, "getLocalVersion": {Func: rpcGetLocalVersion},

View File

@ -77,9 +77,6 @@ func Main() {
// initialize display // initialize display
initDisplay() initDisplay()
// start video sleep mode timer
startVideoSleepModeTicker()
go func() { go func() {
time.Sleep(15 * time.Minute) time.Sleep(15 * time.Minute)
for { for {

View File

@ -179,7 +179,6 @@ export function MacroStepCard({
</div> </div>
</div> </div>
<div className="w-full flex flex-col gap-1"> <div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FieldLabel label={m.macro_step_keys_label()} description={m.macro_step_keys_description({ max: MAX_KEYS_PER_STEP })} /> <FieldLabel label={m.macro_step_keys_label()} description={m.macro_step_keys_description({ max: MAX_KEYS_PER_STEP })} />
@ -245,4 +244,4 @@ export function MacroStepCard({
</div> </div>
</Card> </Card>
); );
} }

103
video.go
View File

@ -1,22 +1,10 @@
package kvm package kvm
import ( import (
"context"
"fmt"
"time"
"github.com/jetkvm/kvm/internal/native" "github.com/jetkvm/kvm/internal/native"
) )
var ( var lastVideoState native.VideoState
lastVideoState native.VideoState
videoSleepModeCtx context.Context
videoSleepModeCancel context.CancelFunc
)
const (
defaultVideoSleepModeDuration = 1 * time.Minute
)
func triggerVideoStateUpdate() { func triggerVideoStateUpdate() {
go func() { go func() {
@ -29,92 +17,3 @@ func triggerVideoStateUpdate() {
func rpcGetVideoState() (native.VideoState, error) { func rpcGetVideoState() (native.VideoState, error) {
return lastVideoState, nil 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
}
}
}

View File

@ -39,34 +39,6 @@ type Session struct {
keysDownStateQueue chan usbgadget.KeysDownState 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() { func (s *Session) resetKeepAliveTime() {
s.keepAliveJitterLock.Lock() s.keepAliveJitterLock.Lock()
defer s.keepAliveJitterLock.Unlock() defer s.keepAliveJitterLock.Unlock()
@ -340,8 +312,9 @@ func newSession(config SessionConfig) (*Session, error) {
if connectionState == webrtc.ICEConnectionStateConnected { if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected { if !isConnected {
isConnected = true isConnected = true
actionSessions++
onActiveSessionsChanged() onActiveSessionsChanged()
if incrActiveSessions() == 1 { if actionSessions == 1 {
onFirstSessionConnected() onFirstSessionConnected()
} }
} }
@ -380,8 +353,9 @@ func newSession(config SessionConfig) (*Session, error) {
} }
if isConnected { if isConnected {
isConnected = false isConnected = false
actionSessions--
onActiveSessionsChanged() onActiveSessionsChanged()
if decrActiveSessions() == 0 { if actionSessions == 0 {
onLastSessionDisconnected() onLastSessionDisconnected()
} }
} }
@ -390,16 +364,16 @@ func newSession(config SessionConfig) (*Session, error) {
return session, nil return session, nil
} }
var actionSessions = 0
func onActiveSessionsChanged() { func onActiveSessionsChanged() {
requestDisplayUpdate(true, "active_sessions_changed") requestDisplayUpdate(true, "active_sessions_changed")
} }
func onFirstSessionConnected() { func onFirstSessionConnected() {
_ = nativeInstance.VideoStart() _ = nativeInstance.VideoStart()
stopVideoSleepModeTicker()
} }
func onLastSessionDisconnected() { func onLastSessionDisconnected() {
_ = nativeInstance.VideoStop() _ = nativeInstance.VideoStop()
startVideoSleepModeTicker()
} }