169169 font-style : italic;
170170}
171171
172+ .viewerIcons {
173+ background-position : left;
174+ background-repeat : no-repeat;
175+ background-size : contain;
176+ padding : 5px ;
177+ margin : 0 5px 0 0 ;
178+ cursor : help;
179+ padding-left : 34px ;
180+ border-radius : 25% ;
181+ }
182+
183+ # topbar {
184+ display : flex;
185+ margin : 0 auto;
186+ text-align : center;
187+ position : fixed;
188+ right : 0 ;
189+ z-index : 999 ;
190+ background-color : # 0009 ;
191+ border-radius : 0 0 0 13px ;
192+ padding-left : 3px ;
193+ }
194+
195+ # topbar : empty {
196+ display : none;
197+ }
198+
172199body .obsstudio-mode {
173200 --bg-color : # 13141A ;
174201 background-color : # 13141A ;
424451body .light-mode .meta-value .badge {
425452 background : rgba (0 , 0 , 0 , 0.08 );
426453}
454+
455+ @media only screen and (max-width : 1200px ) {
456+ # header h1 .large {
457+ display : none;
458+ }
459+ }
460+ @media only screen and (min-width : 1200px ) {
461+ # header h1 .small {
462+ display : none;
463+ }
464+ }
465+ @media only screen and (max-width : 700px ) {
466+ # header h1 .small {
467+ display : none;
468+ }
469+ }
470+
427471</ style >
428472</ head >
429473< body >
474+ < div id ="topbar "> </ div >
430475< div id ="header ">
431- < h1 > Stream Events & Donations Dashboard</ h1 >
476+ < h1 class ="large "> Stream Events & Donations Dashboard</ h1 >
477+ < h1 class ="small "> Events Dashboard</ h1 >
432478</ div >
433479< div id ="chat-container ">
434480 < div class ="no-events "> Waiting for stream events and donations...</ div >
@@ -447,12 +493,13 @@ <h1>Stream Events & Donations Dashboard</h1>
447493 console . log ( msg ) ;
448494 }
449495 }
450-
496+
451497 var password = "false" ;
452498 var featuredMode = false ;
453499
454500 const chatContainer = document . getElementById ( 'chat-container' ) ;
455501 let noEventsMessage = document . querySelector ( '.no-events' ) ;
502+ const topBarElement = document . getElementById ( 'topbar' ) ;
456503
457504 var sources = false ; // Default sources
458505 if ( urlParams . get ( "sources" ) ) {
@@ -470,6 +517,7 @@ <h1>Stream Events & Donations Dashboard</h1>
470517 var maxEvents = urlParams . has ( "maxevents" ) ? parseInt ( urlParams . get ( "maxevents" ) ) : 200 ;
471518 var fontFamily = urlParams . has ( "font" ) ? urlParams . get ( "font" ) : "Roboto" ;
472519 var googleFont = urlParams . has ( "googlefont" ) ? urlParams . get ( "googlefont" ) : "" ;
520+ const showViewerCount = urlParams . has ( "showviewercount" ) ;
473521
474522 // Apply theme immediately
475523 if ( lightMode ) {
@@ -581,6 +629,8 @@ <h1>Stream Events & Donations Dashboard</h1>
581629
582630 const DEDUPLICATED_STATUS_EVENTS = new Set ( [ 'follower_update' , 'subscriber_update' ] ) ;
583631 const statusLastValues = Object . create ( null ) ;
632+ const viewerCounts = Object . create ( null ) ;
633+ const VIEWER_ICON_TTL = 130000 ;
584634
585635 function getStatusCacheKey ( normalizedEvent , data ) {
586636 const type = data && typeof data . type === 'string' ? data . type . toLowerCase ( ) : 'unknown' ;
@@ -761,6 +811,56 @@ <h1>Stream Events & Donations Dashboard</h1>
761811 return luminance > 160 ? '#111111' : '#f5f5f5' ;
762812 }
763813
814+ function upsertViewerIcon ( type , count ) {
815+ if ( ! topBarElement ) return ;
816+ let ele = topBarElement . querySelector ( "[data-type='" + type + "']" ) ;
817+ if ( ! ele ) {
818+ ele = document . createElement ( "div" ) ;
819+ ele . dataset . type = type ;
820+ ele . classList . add ( "viewerIcons" ) ;
821+ ele . title = "Viewer count for " + type ;
822+ ele . style . backgroundImage = "url(./sources/images/" + type + ".png)" ;
823+ topBarElement . prepend ( ele ) ;
824+ }
825+ ele . innerText = Number . isFinite ( count ) ? count . toLocaleString ( ) : String ( count || 0 ) ;
826+ clearTimeout ( ele . timeout ) ;
827+ ele . timeout = setTimeout ( function ( node ) {
828+ if ( node ) {
829+ delete viewerCounts [ node . dataset . type ] ;
830+ node . remove ( ) ;
831+ }
832+ } , VIEWER_ICON_TTL , ele ) ;
833+ }
834+
835+ function updateViewerTopBar ( data ) {
836+ if ( ! showViewerCount || ! data || ! data . event || ! topBarElement ) {
837+ return ;
838+ }
839+ const normalizedEvent = normalizeEventKey ( data . event ) ;
840+ if ( normalizedEvent === 'viewer_update' && data . type ) {
841+ const count = resolveMetricNumber ( data ) ;
842+ if ( count === null ) return ;
843+ viewerCounts [ data . type ] = count ;
844+ upsertViewerIcon ( data . type , count ) ;
845+ } else if ( normalizedEvent === 'viewer_updates' && data . meta && typeof data . meta === 'object' ) {
846+ const incomingTypes = Object . keys ( data . meta ) ;
847+ const allowedTypes = new Set ( incomingTypes ) ;
848+ topBarElement . querySelectorAll ( ".viewerIcons" ) . forEach ( ele => {
849+ if ( ! allowedTypes . has ( ele . dataset . type ) ) {
850+ clearTimeout ( ele . timeout ) ;
851+ ele . remove ( ) ;
852+ delete viewerCounts [ ele . dataset . type ] ;
853+ }
854+ } ) ;
855+ incomingTypes . forEach ( type => {
856+ const numericCount = extractNumeric ( data . meta [ type ] ) ;
857+ if ( numericCount === null ) return ;
858+ viewerCounts [ type ] = numericCount ;
859+ upsertViewerIcon ( type , numericCount ) ;
860+ } ) ;
861+ }
862+ }
863+
764864 function isMetaOnlyPayload ( data ) {
765865 if ( ! data || typeof data !== 'object' ) return false ;
766866 if ( ! data . meta ) return false ; // Treat any truthy meta (object/string/number) as meta-only
@@ -1255,6 +1355,7 @@ <h1>Stream Events & Donations Dashboard</h1>
12551355 }
12561356
12571357 function addMessageToOverlay ( data ) {
1358+ updateViewerTopBar ( data ) ;
12581359 if ( isMetaOnlyPayload ( data ) ) {
12591360 return ;
12601361 }
0 commit comments