1/** 2 * Renders global VC chrome (top-right roster + toolbar) from ChatroomVoiceChat state. 3 */ 4(function (global) { 5 'use strict'; 6 7 var CSS_HREF = '/css/spaVcOverlay.css'; 8 var bound = false; 9 var lastState = null; 10 11 function ensureStylesheet() { 12 if (document.querySelector('link[data-spa-vc-overlay-css]')) return; 13 var link = document.createElement('link'); 14 link.rel = 'stylesheet'; 15 link.href = CSS_HREF; 16 link.setAttribute('data-spa-vc-overlay-css', '1'); 17 document.head.appendChild(link); 18 } 19 20 function isOnChatroomRoute() { 21 return !!(document.body && document.body.classList.contains('page-chatroom')); 22 } 23 24 function isVoiceConnected() { 25 if (!global.ChatroomVoiceChat || typeof global.ChatroomVoiceChat.getStatus !== 'function') { 26 return false; 27 } 28 var status = global.ChatroomVoiceChat.getStatus(); 29 return !!(status && status.connected === true); 30 } 31 32 function shouldShowChrome(state) { 33 return !!(state && state.connected === true && !isOnChatroomRoute()); 34 } 35 36 function hideDisconnected() { 37 render({ connected: false }); 38 } 39 40 function escapeHtml(str) { 41 return String(str || '') 42 .replace(/&/g, '&') 43 .replace(/</g, '<') 44 .replace(/>/g, '>') 45 .replace(/"/g, '"'); 46 } 47 48 function renderUserRow(username, isTalking) { 49 var row = document.createElement('div'); 50 row.className = 'spa-vc-chrome__user' + (isTalking ? ' spa-vc-chrome__user--talking' : ''); 51 row.setAttribute('role', 'listitem'); 52 row.title = username; 53 54 var name = document.createElement('span'); 55 name.className = 'spa-vc-chrome__name'; 56 name.textContent = username; 57 58 var av = document.createElement('span'); 59 av.className = 'spa-vc-chrome__avatar'; 60 av.setAttribute('aria-hidden', 'true'); 61 62 var pic = global.ChatroomProfilePicture && global.ChatroomProfilePicture.getProfilePicture(username); 63 if (pic) { 64 var img = document.createElement('img'); 65 img.className = 'spa-vc-chrome__avatar-img'; 66 img.src = pic; 67 img.alt = ''; 68 img.loading = 'lazy'; 69 av.appendChild(img); 70 } else { 71 var fb = document.createElement('span'); 72 fb.className = 'spa-vc-chrome__avatar-fallback'; 73 fb.textContent = (username || '?').charAt(0).toUpperCase(); 74 av.appendChild(fb); 75 } 76 77 row.appendChild(name); 78 row.appendChild(av); 79 return row; 80 } 81 82 function renderRoster(participants, talkingUsers) { 83 var roster = document.getElementById('spa-vc-chrome-roster'); 84 if (!roster) return; 85 86 roster.innerHTML = ''; 87 88 if (!participants || !participants.length) { 89 return; 90 } 91
92 participants.forEach(function (p) { 93 roster.appendChild(renderUserRow( 94 p.username, 95 talkingUsers.indexOf(p.username) !== -1 96 )); 97 }); 98 } 99 100 function updateControlButtons(state) { 101 var muteBtn = document.getElementById('spa-vc-chrome-mute'); 102 var deafenBtn = document.getElementById('spa-vc-chrome-deafen'); 103 if (muteBtn && global.ChatroomIcons) { 104 global.ChatroomIcons.setVoiceMute(muteBtn, !!state.muted); 105 muteBtn.title = state.muted ? 'Unmute' : 'Mute'; 106 muteBtn.setAttribute('aria-label', muteBtn.title); 107 muteBtn.classList.toggle('is-active', !!state.muted); 108 } 109 if (deafenBtn && global.ChatroomIcons) { 110 global.ChatroomIcons.setVoiceDeafen(deafenBtn, !!state.deafened); 111 deafenBtn.title = state.deafened ? 'Undeafen' : 'Deafen'; 112 deafenBtn.setAttribute('aria-label', deafenBtn.title); 113 deafenBtn.classList.toggle('is-active', !!state.deafened); 114 } 115 } 116 117 function resolveListeningPartyState(state) { 118 var lp = state && state.listeningParty; 119 if (lp && (lp.active || lp.canStart)) { 120 return lp; 121 } 122 if (state && state.connected === true && (!lp || !lp.active)) { 123 return { active: false, canStart: true }; 124 } 125 return lp || { active: false, canStart: false }; 126 } 127 128 function renderListeningRow(listening) { 129 var row = document.getElementById('spa-vc-chrome-listening'); 130 if (!row) return; 131 if (!listening || (!listening.active && !listening.canStart)) { 132 row.hidden = true; 133 row.innerHTML = ''; 134 return; 135 } 136 row.hidden = false; 137 138 var track = listening.trackLabel || 'Listening party'; 139 var labelText; 140 var actionsHtml = ''; 141 142 if (listening.active && listening.host) { 143 if (listening.isHost) { 144 labelText = 'Hosting · ' + track; 145 actionsHtml = 146 '<button type="button" class="spa-vc-chrome-listening-btn spa-vc-chrome-listening-btn--stop" id="spa-vc-chrome-listening-stop">Stop</button>' 147 + '<button type="button" class="spa-vc-chrome-listening-btn spa-vc-chrome-listening-btn--invite" id="spa-vc-chrome-listening-invite">Invite</button>'; 148 } else if (listening.joined) { 149 labelText = 'Synced · ' + track; 150 actionsHtml = '<button type="button" class="spa-vc-chrome-listening-btn spa-vc-chrome-listening-btn--leave" id="spa-vc-chrome-listening-leave">Leave</button>'; 151 } else { 152 labelText = (listening.host ? listening.host + ' · ' : '') + track; 153 actionsHtml = '<button type="button" class="spa-vc-chrome-listening-btn spa-vc-chrome-listening-btn--join" id="spa-vc-chrome-listening-join">Join</button>'; 154 } 155 } else if (listening.canStart) { 156 labelText = 'Synced listening party'; 157 actionsHtml = '<button type="button" class="spa-vc-chrome-listening-btn spa-vc-chrome-listening-btn--start" id="spa-vc-chrome-listening-start">Start party</button>'; 158 } else { 159 row.hidden = true; 160 row.innerHTML = ''; 161 return; 162 } 163 164 row.innerHTML = 165 '<p class="spa-vc-chrome__listening-label">' 166 + '<i class="fa-solid fa-compact-disc" aria-hidden="true"></i> ' 167 + escapeHtml(labelText) 168 + '</p>' 169 + (actionsHtml ? '<div class="spa-vc-chrome__listening-actions">' + actionsHtml + '</div>' : ''); 170 } 171 172 function setChromeVisible(chrome, show) { 173 chrome.hidden = !show; 174 chrome.setAttribute('aria-hidden', show ? 'false' : 'true'); 175 chrome.classList.toggle('spa-vc-chrome--hidden', !show); 176 if (document.body) { 177 document.body.classList.toggle('vc-chrome-active', show); 178 } 179 } 180 181 function render(state) { 182 if (!state || state.connected !== true) { 183 state = { connected: false }; 184 } 185 186 lastState = state; 187 ensureStylesheet(); 188 if (typeof global.ensureSpaVcOverlayShell === 'function') { 189 global.ensureSpaVcOverlayShell(); 190 } 191 192 var chrome = document.getElementById('spa-vc-chrome'); 193 if (!chrome) return; 194 195 var show = shouldShowChrome(state); 196 setChromeVisible(chrome, show); 197 if (!show) return; 198 199 var channelEl = document.getElementById('spa-vc-chrome-channel'); 200 if (channelEl) { 201 channelEl.textContent = state.channelLabel || 'Whimsy Zone'; 202 } 203 204 renderRoster(state.participants || [], state.talkingUsers || []); 205 renderListeningRow(resolveListeningPartyState(state)); 206 updateControlButtons(state); 207 } 208 209 function syncWithVoiceStatus() { 210 if (!isVoiceConnected()) { 211 hideDisconnected(); 212 return; 213 } 214 if (global.ChatroomVoiceChat && typeof global.ChatroomVoiceChat.publishOverlayState === 'function') { 215 global.ChatroomVoiceChat.publishOverlayState(); 216 } 217 } 218
219 function bindControlsOnce() { 220 if (bound) return; 221 bound = true; 222 223 global.addEventListener('pupwife:spa-chrome', function () { 224 if (!isVoiceConnected()) { 225 hideDisconnected(); 226 return; 227 } 228 var publish = function () { 229 syncWithVoiceStatus(); 230 }; 231 if (typeof global.ensureSpaVcOverlayLoaded === 'function') { 232 global.ensureSpaVcOverlayLoaded().then(publish).catch(publish); 233 } else { 234 publish(); 235 } 236 }); 237 238 global.addEventListener('click', function (e) { 239 var muteBtn = e.target && e.target.closest ? e.target.closest('#spa-vc-chrome-mute') : null; 240 var deafenBtn = e.target && e.target.closest ? e.target.closest('#spa-vc-chrome-deafen') : null; 241 var leaveBtn = e.target && e.target.closest ? e.target.closest('#spa-vc-chrome-leave') : null; 242 if (!muteBtn && !deafenBtn && !leaveBtn) return; 243 if (!global.ChatroomVoiceChat) return; 244 e.preventDefault(); 245 if (muteBtn && typeof global.ChatroomVoiceChat.toggleMute === 'function') { 246 global.ChatroomVoiceChat.toggleMute(); 247 } else if (deafenBtn && typeof global.ChatroomVoiceChat.toggleDeafen === 'function') { 248 global.ChatroomVoiceChat.toggleDeafen(); 249 } else if (leaveBtn && typeof global.ChatroomVoiceChat.disconnect === 'function') { 250 global.ChatroomVoiceChat.disconnect(); 251 } 252 }); 253 } 254 255 function init() { 256 bindControlsOnce(); 257 ensureStylesheet(); 258 if (typeof global.ensureSpaVcOverlayShell === 'function') { 259 global.ensureSpaVcOverlayShell(); 260 } 261 if (global.ChatroomIcons && typeof global.ChatroomIcons.initStaticButtons === 'function') { 262 global.ChatroomIcons.initStaticButtons(); 263 } 264 hideDisconnected(); 265 } 266 267 global.SpaVcOverlay = { 268 init: init, 269 render: render, 270 hide: hideDisconnected, 271 syncWithVoiceStatus: syncWithVoiceStatus, 272 refresh: syncWithVoiceStatus 273 }; 274})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.