From 78cef12c97982389ce1a8b4b83a6ea9b65ea7d9c Mon Sep 17 00:00:00 2001 From: Nitish Agarwal <1592163+nitishagar@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:25:24 +0530 Subject: [PATCH 01/11] fix: mobile viewport cropping on video element (#985) --- ui/src/components/WebRTCVideo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index c54d2b8d..1419deb8 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -529,7 +529,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu controlsList="nofullscreen" style={videoStyle} className={cx( - "max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000", + "max-h-full max-w-full sm:min-h-[384px] sm:min-w-[512px] bg-black/50 object-contain transition-all duration-1000", { "cursor-none": settings.isCursorHidden, "!opacity-0": From 07935add15f0713b3e484e97bcb4f0616e704f66 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 20 Nov 2025 14:56:17 +0100 Subject: [PATCH 02/11] refactor: remove redundant initialization of native and display components in Main function (#987) --- main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/main.go b/main.go index a4d80fb7..88d2dec7 100644 --- a/main.go +++ b/main.go @@ -70,9 +70,6 @@ func Main() { initOta() - initNative(systemVersionLocal, appVersionLocal) - initDisplay() - http.DefaultClient.Timeout = 1 * time.Minute // Initialize network From 85eb4babdf7f6faad638f951170775eff1a48642 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 20 Nov 2025 16:07:50 +0100 Subject: [PATCH 03/11] feat: handle grpc events (#986) Co-authored-by: Siyuan --- internal/native/grpc_client.go | 26 ++++++------ internal/native/grpc_server.go | 75 +++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/internal/native/grpc_client.go b/internal/native/grpc_client.go index 300a2284..85a3201b 100644 --- a/internal/native/grpc_client.go +++ b/internal/native/grpc_client.go @@ -79,6 +79,18 @@ func NewGRPCClient(opts grpcClientOptions) (*GRPCClient, error) { // Start event stream go grpcClient.startEventStream() + // Start event handler to process events from the channel + go func() { + for { + select { + case event := <-grpcClient.eventCh: + grpcClient.handleEvent(event) + case <-grpcClient.eventDone: + return + } + } + }() + return grpcClient, nil } @@ -234,20 +246,6 @@ func (c *GRPCClient) handleEvent(event *pb.Event) { } } -// OnEvent registers an event handler -func (c *GRPCClient) OnEvent(eventType string, handler func(data interface{})) { - go func() { - for { - select { - case event := <-c.eventCh: - c.handleEvent(event) - case <-c.eventDone: - return - } - } - }() -} - // Close closes the gRPC client func (c *GRPCClient) Close() error { c.closeM.Lock() diff --git a/internal/native/grpc_server.go b/internal/native/grpc_server.go index 304203ce..9b54fb5b 100644 --- a/internal/native/grpc_server.go +++ b/internal/native/grpc_server.go @@ -15,18 +15,20 @@ import ( // grpcServer wraps the Native instance and implements the gRPC service type grpcServer struct { pb.UnimplementedNativeServiceServer - native *Native - logger *zerolog.Logger - eventChs []chan *pb.Event - eventM sync.Mutex + native *Native + logger *zerolog.Logger + eventStreamChan chan *pb.Event + eventStreamMu sync.Mutex + eventStreamCtx context.Context + eventStreamCancel context.CancelFunc } // NewGRPCServer creates a new gRPC server for the native service func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer { s := &grpcServer{ - native: n, - logger: logger, - eventChs: make([]chan *pb.Event, 0), + native: n, + logger: logger, + eventStreamChan: make(chan *pb.Event, 100), } // Store original callbacks and wrap them to also broadcast events @@ -82,16 +84,7 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer { } func (s *grpcServer) broadcastEvent(event *pb.Event) { - s.eventM.Lock() - defer s.eventM.Unlock() - - for _, ch := range s.eventChs { - select { - case ch <- event: - default: - // Channel full, skip - } - } + s.eventStreamChan <- event } func (s *grpcServer) IsReady(ctx context.Context, req *pb.IsReadyRequest) (*pb.IsReadyResponse, error) { @@ -103,35 +96,49 @@ func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamE setProcTitle("connected") defer setProcTitle("waiting") - eventCh := make(chan *pb.Event, 100) + // Cancel previous stream if exists + s.eventStreamMu.Lock() + if s.eventStreamCancel != nil { + s.logger.Debug().Msg("cancelling previous StreamEvents call") + s.eventStreamCancel() + } - // Register this channel for events - s.eventM.Lock() - s.eventChs = append(s.eventChs, eventCh) - s.eventM.Unlock() + // Create a cancellable context for this stream + ctx, cancel := context.WithCancel(stream.Context()) + s.eventStreamCtx = ctx + s.eventStreamCancel = cancel + s.eventStreamMu.Unlock() - // Unregister on exit + // Clean up when this stream ends defer func() { - s.eventM.Lock() - defer s.eventM.Unlock() - for i, ch := range s.eventChs { - if ch == eventCh { - s.eventChs = append(s.eventChs[:i], s.eventChs[i+1:]...) - break - } + s.eventStreamMu.Lock() + defer s.eventStreamMu.Unlock() + if s.eventStreamCtx == ctx { + s.eventStreamCancel = nil + s.eventStreamCtx = nil } - close(eventCh) + cancel() }() // Stream events for { select { - case event := <-eventCh: + case event := <-s.eventStreamChan: + // Check if this stream is still the active one + s.eventStreamMu.Lock() + isActive := s.eventStreamCtx == ctx + s.eventStreamMu.Unlock() + + if !isActive { + s.logger.Debug().Msg("stream replaced by new call, exiting") + return context.Canceled + } + if err := stream.Send(event); err != nil { return err } - case <-stream.Context().Done(): - return stream.Context().Err() + case <-ctx.Done(): + return ctx.Err() } } } From 0952c6abf2ff840d113ae9ecd66b6a543a3254d2 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 20 Nov 2025 16:34:02 +0100 Subject: [PATCH 04/11] chore: use en by default (#988) --- ui/src/components/SettingsItem.tsx | 13 +++++++++++-- .../routes/devices.$id.settings.general._index.tsx | 2 ++ ui/vite.config.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ui/src/components/SettingsItem.tsx b/ui/src/components/SettingsItem.tsx index e2cd9489..f37c554a 100644 --- a/ui/src/components/SettingsItem.tsx +++ b/ui/src/components/SettingsItem.tsx @@ -5,13 +5,22 @@ interface SettingsItemProps { readonly title: string; readonly description: string | React.ReactNode; readonly badge?: string; + readonly badgeTheme?: keyof typeof badgeTheme; readonly className?: string; readonly loading?: boolean; readonly children?: React.ReactNode; } +const badgeTheme = { + info: "bg-blue-500 text-white", + success: "bg-green-500 text-white", + warning: "bg-yellow-500 text-white", + danger: "bg-red-500 text-white", +}; + export function SettingsItem(props: SettingsItemProps) { - const { title, description, badge, children, className, loading } = props; + const { title, description, badge, badgeTheme: badgeThemeProp = "danger", children, className, loading } = props; + const badgeThemeClass = badgeTheme[badgeThemeProp]; return (