PageSourceSearch

https://pupwife.com/scripts/effects/spaVcOverlay.js

js pupwife.com collected 2026-09-30 15:06:13 UTC 10,362 bytes, 274 lines download raw bytes

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, '&lt;')
44            .replace(/>/g, '&gt;')
45            .replace(/"/g, '&quot;');
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.