Last active
May 19, 2026 09:33
-
-
Save vdeemann/784fcffa75e0b50fab6ac29ab19bb271 to your computer and use it in GitHub Desktop.
Auto-mutes songs from DJs not on your favorites list on deepcut.fm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Deepcut DJ Auto-Muter | |
| // @namespace https://github.com/vdeemann | |
| // @version 2.1.0 | |
| // @description Auto-mutes songs from DJs not on your favorites list on deepcut.fm | |
| // @author vdeemann | |
| // @match https://deepcut.live/* | |
| // @match https://deepcut.fm/* | |
| // @match https://deep-cut.fm/* | |
| // @match https://www.deepcut.live/* | |
| // @match https://www.deepcut.fm/* | |
| // @match https://www.deep-cut.fm/* | |
| // @match https://70s.neader.com/* | |
| // @match https://*.neader.com/* | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // PERSISTENCE | |
| // ═══════════════════════════════════════════════════════════════════ | |
| const store = { | |
| get(key, fallback) { | |
| try { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); } catch (_) {} | |
| try { const v = localStorage.getItem('djm_' + key); return v !== null ? JSON.parse(v) : fallback; } catch (_) { return fallback; } | |
| }, | |
| set(key, val) { | |
| try { if (typeof GM_setValue === 'function') return GM_setValue(key, val); } catch (_) {} | |
| try { localStorage.setItem('djm_' + key, JSON.stringify(val)); } catch (_) {} | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // STATE | |
| // ═══════════════════════════════════════════════════════════════════ | |
| const state = { | |
| enabled: store.get('enabled', true), | |
| favorites: new Set((store.get('favorites', [])).map(n => n.toLowerCase())), | |
| currentDJ: null, | |
| isMuted: false, | |
| myUsername: store.get('myUsername', ''), | |
| prevVolume: null, | |
| log: [], | |
| // Lifecycle | |
| uiMounted: false, | |
| djInterval: null, | |
| enforceInterval: null, | |
| djObserver: null, | |
| iframeObserver: null, | |
| }; | |
| function saveFavorites() { | |
| store.set('favorites', [...state.favorites]); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // ROOM DETECTION | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // | |
| // The UI should ONLY appear inside a room, not on the homepage / room-list / | |
| // settings / profile pages. We detect room context by: | |
| // 1. Presence of room-only DOM (current-DJ element, songboard, room player) | |
| // 2. URL pattern — deepcut room URLs typically look like | |
| // /room/<slug> or /r/<slug> (not "/" or "/favorites" or "/settings") | |
| // | |
| // Either signal is sufficient; DOM is more reliable, URL is the fast-path | |
| // before the room DOM has rendered. | |
| function urlLooksLikeRoom() { | |
| const path = (location.pathname || '').toLowerCase(); | |
| // Homepage and known non-room top-level pages | |
| const nonRoomPaths = [ | |
| '/', '/favorites', '/settings', '/profile', '/login', | |
| '/signup', '/about', '/contact', '/terms', '/privacy', | |
| '/djs-needed', '/all-rooms' | |
| ]; | |
| if (nonRoomPaths.includes(path)) return false; | |
| // Common room URL shapes | |
| if (/^\/(room|r|rooms)\/[^/]+/.test(path)) return true; | |
| // Fall through — let DOM be the deciding factor | |
| return null; // unknown | |
| } | |
| function domLooksLikeRoom() { | |
| // Most reliable: the mini-room current-DJ name element exists | |
| if (document.querySelector('.mini-room-current-dj-name')) return true; | |
| if (document.querySelector('.mini-room-current-dj')) return true; | |
| // Songboard appears in rooms only | |
| if (document.querySelector('.songboard-artist.songboard-main')) return true; | |
| if (document.querySelector('.songboard-song.songboard-main')) return true; | |
| // A YouTube embed in the page is also a strong room signal | |
| if (document.querySelector('iframe[src*="youtube.com/embed"]')) return true; | |
| return false; | |
| } | |
| function isInsideRoom() { | |
| const urlSignal = urlLooksLikeRoom(); | |
| if (urlSignal === false) return false; // explicit non-room path | |
| if (urlSignal === true && domLooksLikeRoom()) return true; | |
| // URL unknown — defer to DOM | |
| return domLooksLikeRoom(); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // DJ DETECTION — uses .mini-room-current-dj-name element | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function getCurrentDJName() { | |
| // Primary: the mini-room current DJ name element | |
| const el = document.querySelector('.mini-room-current-dj-name'); | |
| if (el) { | |
| const name = (el.textContent || '').trim(); | |
| if (name.length > 0) return name; | |
| } | |
| // Fallback: scan chat for most recent "started playing" message | |
| const subjects = document.querySelectorAll('.subject'); | |
| const texts = document.querySelectorAll('.text'); | |
| for (let i = texts.length - 1; i >= 0; i--) { | |
| const t = (texts[i].textContent || '').trim(); | |
| if (t.startsWith('started playing')) { | |
| // Find the matching subject (previous sibling or same-index) | |
| const parent = texts[i].parentElement; | |
| if (parent) { | |
| const subj = parent.querySelector('.subject'); | |
| if (subj) return (subj.textContent || '').trim(); | |
| } | |
| // Try same index | |
| if (subjects[i]) return (subjects[i].textContent || '').trim(); | |
| } | |
| } | |
| return null; | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // MUTE / UNMUTE — YouTube iframe API + any audio elements | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function getYouTubeIframe() { | |
| return document.querySelector('iframe[src*="youtube.com/embed"]'); | |
| } | |
| function postToYT(iframe, func, args) { | |
| if (!iframe || !iframe.contentWindow) return; | |
| try { | |
| iframe.contentWindow.postMessage(JSON.stringify({ | |
| event: 'command', | |
| func: func, | |
| args: args || [] | |
| }), '*'); | |
| } catch (_) {} | |
| } | |
| function setMuted(shouldMute) { | |
| if (state.isMuted === shouldMute) return; | |
| state.isMuted = shouldMute; | |
| const ytIframe = getYouTubeIframe(); | |
| if (shouldMute) { | |
| postToYT(ytIframe, 'mute'); | |
| postToYT(ytIframe, 'setVolume', [0]); | |
| document.querySelectorAll('audio, video').forEach(el => { el.muted = true; }); | |
| const volSlider = document.querySelector('input[type="range"][class*="vol"], .volume input[type="range"], [class*="volume"] input'); | |
| if (volSlider) { | |
| state.prevVolume = volSlider.value; | |
| setNativeValue(volSlider, 0); | |
| } | |
| } else { | |
| postToYT(ytIframe, 'unMute'); | |
| postToYT(ytIframe, 'setVolume', [100]); | |
| document.querySelectorAll('audio, video').forEach(el => { el.muted = false; }); | |
| const volSlider = document.querySelector('input[type="range"][class*="vol"], .volume input[type="range"], [class*="volume"] input'); | |
| if (volSlider && state.prevVolume !== null) { | |
| setNativeValue(volSlider, state.prevVolume); | |
| } | |
| } | |
| addLog(shouldMute | |
| ? '\u{1F507} Muted: ' + state.currentDJ | |
| : '\u{1F50A} Unmuted: ' + (state.currentDJ || '')); | |
| updateUI(); | |
| } | |
| function setNativeValue(el, value) { | |
| const setter = Object.getOwnPropertyDescriptor( | |
| Object.getPrototypeOf(el), 'value' | |
| )?.set || Object.getOwnPropertyDescriptor( | |
| HTMLInputElement.prototype, 'value' | |
| )?.set; | |
| if (setter) setter.call(el, value); | |
| else el.value = value; | |
| el.dispatchEvent(new Event('input', { bubbles: true })); | |
| el.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // CORE LOOP — poll for DJ changes | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function checkDJ() { | |
| if (!state.enabled) return; | |
| const djName = getCurrentDJName(); | |
| if (!djName) return; | |
| if (djName === state.currentDJ) return; | |
| state.currentDJ = djName; | |
| addLog('\u{1F3A7} Now spinning: ' + djName); | |
| const djLower = djName.toLowerCase(); | |
| const isMe = state.myUsername && djLower === state.myUsername.toLowerCase(); | |
| const isFav = state.favorites.has(djLower); | |
| if (isMe || isFav) { | |
| setMuted(false); | |
| } else { | |
| setMuted(true); | |
| } | |
| updateUI(); | |
| } | |
| function enforceMute() { | |
| if (!state.enabled || !state.currentDJ) return; | |
| const djLower = state.currentDJ.toLowerCase(); | |
| const isMe = state.myUsername && djLower === state.myUsername.toLowerCase(); | |
| const isFav = state.favorites.has(djLower); | |
| const shouldBeMuted = !(isMe || isFav); | |
| if (shouldBeMuted) { | |
| const ytIframe = getYouTubeIframe(); | |
| postToYT(ytIframe, 'mute'); | |
| postToYT(ytIframe, 'setVolume', [0]); | |
| document.querySelectorAll('audio, video').forEach(el => { el.muted = true; }); | |
| if (!state.isMuted) { | |
| state.isMuted = true; | |
| addLog('\u{1F507} Re-muted: ' + state.currentDJ + ' (new embed)'); | |
| updateUI(); | |
| } | |
| } | |
| } | |
| function startWatching() { | |
| if (state.djInterval) return; // already running | |
| state.djInterval = setInterval(checkDJ, 1500); | |
| state.enforceInterval = setInterval(enforceMute, 2000); | |
| const djTarget = document.querySelector('.mini-room-current-dj') || | |
| document.querySelector('.mini-room-current-dj-name') || | |
| document.body; | |
| state.djObserver = new MutationObserver(() => { | |
| clearTimeout(state.djObserver._t); | |
| state.djObserver._t = setTimeout(checkDJ, 200); | |
| }); | |
| state.djObserver.observe(djTarget, { | |
| childList: true, subtree: true, characterData: true | |
| }); | |
| state.iframeObserver = new MutationObserver((mutations) => { | |
| for (const m of mutations) { | |
| for (const node of m.addedNodes) { | |
| if (node.nodeType !== 1) continue; | |
| const isYT = (node.tagName === 'IFRAME' && (node.src || '').includes('youtube.com')) || | |
| (node.querySelector && node.querySelector('iframe[src*="youtube.com"]')); | |
| if (isYT) { | |
| setTimeout(enforceMute, 500); | |
| setTimeout(enforceMute, 1500); | |
| setTimeout(enforceMute, 3000); | |
| } | |
| } | |
| } | |
| }); | |
| state.iframeObserver.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| function stopWatching() { | |
| if (state.djInterval) { clearInterval(state.djInterval); state.djInterval = null; } | |
| if (state.enforceInterval) { clearInterval(state.enforceInterval); state.enforceInterval = null; } | |
| if (state.djObserver) { state.djObserver.disconnect(); state.djObserver = null; } | |
| if (state.iframeObserver) { state.iframeObserver.disconnect(); state.iframeObserver = null; } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // LOGGING | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function addLog(msg) { | |
| const t = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| state.log.unshift('[' + t + '] ' + msg); | |
| if (state.log.length > 50) state.log.length = 50; | |
| const el = document.getElementById('djm-log'); | |
| if (el) el.textContent = state.log.slice(0, 15).join('\n'); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // UI | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function esc(s) { | |
| return String(s).replace(/&/g,'&').replace(/</g,'<') | |
| .replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| function createUI() { | |
| if (document.getElementById('djm-root')) return; // already mounted | |
| const root = document.createElement('div'); | |
| root.id = 'djm-root'; | |
| root.innerHTML = ` | |
| <style> | |
| #djm-root { | |
| position: fixed; bottom: 8px; right: 280px; z-index: 999999; | |
| font-family: 'SF Mono','Fira Code','Cascadia Code','Consolas',monospace; | |
| font-size: 12px; user-select: none; | |
| } | |
| #djm-pill { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 8px 14px; background: rgba(10,10,10,0.93); | |
| backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,0.07); | |
| border-radius: 40px; color: #d0d0d0; cursor: pointer; | |
| transition: all 0.2s; box-shadow: 0 4px 24px rgba(0,0,0,0.55); | |
| } | |
| #djm-pill:hover { border-color: rgba(255,255,255,0.16); } | |
| #djm-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| transition: background 0.3s, box-shadow 0.3s; | |
| } | |
| .djm-listening { background: #4ade80; box-shadow: 0 0 6px rgba(74,222,128,0.5); } | |
| .djm-muted { background: #f87171; box-shadow: 0 0 6px rgba(248,113,113,0.5); } | |
| .djm-off { background: #555; box-shadow: none; } | |
| #djm-label { font-size: 11px; letter-spacing: 0.5px; text-transform: uppercase; } | |
| #djm-panel { | |
| display: none; position: absolute; bottom: 48px; right: 0; | |
| width: 300px; max-height: 500px; overflow-y: auto; | |
| background: rgba(12,12,12,0.96); backdrop-filter: blur(16px); | |
| border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; | |
| padding: 16px; color: #bbb; | |
| box-shadow: 0 8px 40px rgba(0,0,0,0.65); | |
| } | |
| #djm-panel.open { display: block; } | |
| #djm-panel::-webkit-scrollbar { width: 4px; } | |
| #djm-panel::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; } | |
| .djm-sec { margin-bottom: 14px; } | |
| .djm-sec-t { | |
| font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px; | |
| color: #666; margin-bottom: 8px; | |
| } | |
| .djm-row { | |
| display: flex; align-items: center; justify-content: space-between; | |
| margin-bottom: 6px; | |
| } | |
| .djm-row label { font-size: 11px; color: #999; } | |
| .djm-sw { | |
| position: relative; width: 34px; height: 18px; | |
| background: #333; border-radius: 9px; cursor: pointer; transition: background 0.2s; | |
| } | |
| .djm-sw.on { background: #4ade80; } | |
| .djm-sw::after { | |
| content: ''; position: absolute; top: 2px; left: 2px; | |
| width: 14px; height: 14px; background: #fff; | |
| border-radius: 50%; transition: transform 0.2s; | |
| } | |
| .djm-sw.on::after { transform: translateX(16px); } | |
| .djm-input { | |
| width: 100%; box-sizing: border-box; padding: 6px 10px; | |
| background: #1a1a1a; border: 1px solid #333; border-radius: 6px; | |
| color: #ddd; font-size: 11px; font-family: inherit; outline: none; | |
| } | |
| .djm-input:focus { border-color: #4ade80; } | |
| .djm-input::placeholder { color: #555; } | |
| .djm-fav-item { | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 4px 8px; margin-top: 3px; | |
| background: #1a1a1a; border-radius: 5px; font-size: 11px; color: #ccc; | |
| } | |
| .djm-fav-rm { | |
| cursor: pointer; color: #666; font-size: 14px; padding: 0 4px; | |
| transition: color 0.15s; | |
| } | |
| .djm-fav-rm:hover { color: #f87171; } | |
| .djm-fav-empty { font-size: 10px; color: #555; font-style: italic; padding: 4px 0; } | |
| #djm-now { | |
| padding: 8px 10px; background: #1a1a1a; border-radius: 6px; | |
| font-size: 11px; text-align: center; color: #888; margin-bottom: 4px; | |
| } | |
| #djm-now .dn { color: #e0e0e0; font-weight: 600; } | |
| #djm-now.is-muted { border-left: 3px solid #f87171; } | |
| #djm-now.is-fav { border-left: 3px solid #4ade80; } | |
| #djm-qadd { | |
| display: none; width: 100%; padding: 5px; | |
| background: transparent; border: 1px dashed #4ade80; | |
| border-radius: 5px; color: #4ade80; font-size: 10px; | |
| cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px; | |
| margin-top: 4px; font-family: inherit; | |
| } | |
| #djm-qadd:hover { background: rgba(74,222,128,0.08); } | |
| #djm-log { | |
| font-size: 10px; color: #555; line-height: 1.5; | |
| white-space: pre-wrap; max-height: 140px; overflow-y: auto; | |
| } | |
| #djm-song { | |
| font-size: 10px; color: #666; text-align: center; | |
| padding: 2px 0 4px; | |
| } | |
| </style> | |
| <div id="djm-panel"> | |
| <!-- Now Playing --> | |
| <div class="djm-sec"> | |
| <div class="djm-sec-t">Current DJ</div> | |
| <div id="djm-now">Waiting for DJ...</div> | |
| <div id="djm-song"></div> | |
| <button id="djm-qadd">+ Add to Favorites</button> | |
| </div> | |
| <!-- Toggle --> | |
| <div class="djm-sec"> | |
| <div class="djm-row"> | |
| <label>Auto-mute enabled</label> | |
| <div id="djm-sw-en" class="djm-sw ${state.enabled ? 'on' : ''}"></div> | |
| </div> | |
| </div> | |
| <!-- My Username --> | |
| <div class="djm-sec"> | |
| <div class="djm-sec-t">My Username (never muted)</div> | |
| <input class="djm-input" id="djm-me" placeholder="Your deepcut username" | |
| value="${esc(state.myUsername)}"> | |
| </div> | |
| <!-- Favorites --> | |
| <div class="djm-sec"> | |
| <div class="djm-sec-t">Favorite DJs (never muted)</div> | |
| <input class="djm-input" id="djm-addfav" placeholder="Type a DJ name, press Enter"> | |
| <div id="djm-favs"></div> | |
| </div> | |
| <!-- Log --> | |
| <div class="djm-sec"> | |
| <div class="djm-sec-t">Activity Log</div> | |
| <div id="djm-log"></div> | |
| </div> | |
| </div> | |
| <div id="djm-pill"> | |
| <div id="djm-dot" class="djm-off"></div> | |
| <div id="djm-label">DJ MUTER</div> | |
| </div> | |
| `; | |
| document.body.appendChild(root); | |
| state.uiMounted = true; | |
| bindEvents(); | |
| renderFavorites(); | |
| updateUI(); | |
| } | |
| function destroyUI() { | |
| const root = document.getElementById('djm-root'); | |
| if (root) root.remove(); | |
| state.uiMounted = false; | |
| // Reset playback state so re-entry starts clean | |
| state.currentDJ = null; | |
| state.isMuted = false; | |
| } | |
| function bindEvents() { | |
| document.getElementById('djm-pill').addEventListener('click', () => | |
| document.getElementById('djm-panel').classList.toggle('open')); | |
| document.getElementById('djm-sw-en').addEventListener('click', function () { | |
| state.enabled = !state.enabled; | |
| this.classList.toggle('on', state.enabled); | |
| store.set('enabled', state.enabled); | |
| if (!state.enabled) setMuted(false); | |
| updateUI(); | |
| }); | |
| document.getElementById('djm-me').addEventListener('change', function () { | |
| state.myUsername = this.value.trim(); | |
| store.set('myUsername', state.myUsername); | |
| addLog('\u{1F464} Username set: ' + (state.myUsername || '(none)')); | |
| state.currentDJ = null; | |
| checkDJ(); | |
| }); | |
| document.getElementById('djm-addfav').addEventListener('keydown', function (e) { | |
| if (e.key === 'Enter') { | |
| const name = this.value.trim(); | |
| if (name) { | |
| state.favorites.add(name.toLowerCase()); | |
| saveFavorites(); | |
| this.value = ''; | |
| renderFavorites(); | |
| addLog('\u2B50 Added favorite: ' + name); | |
| if (state.currentDJ && state.currentDJ.toLowerCase() === name.toLowerCase()) { | |
| setMuted(false); | |
| } | |
| updateUI(); | |
| } | |
| } | |
| }); | |
| document.getElementById('djm-qadd').addEventListener('click', () => { | |
| if (state.currentDJ) { | |
| state.favorites.add(state.currentDJ.toLowerCase()); | |
| saveFavorites(); | |
| renderFavorites(); | |
| addLog('\u2B50 Added favorite: ' + state.currentDJ); | |
| setMuted(false); | |
| updateUI(); | |
| } | |
| }); | |
| } | |
| function renderFavorites() { | |
| const el = document.getElementById('djm-favs'); | |
| if (!el) return; | |
| if (state.favorites.size === 0) { | |
| el.innerHTML = '<div class="djm-fav-empty">No favorites yet</div>'; | |
| return; | |
| } | |
| el.innerHTML = [...state.favorites].map(n => | |
| '<div class="djm-fav-item"><span>' + esc(n) + '</span>' + | |
| '<span class="djm-fav-rm" data-n="' + esc(n) + '">\u00D7</span></div>' | |
| ).join(''); | |
| el.querySelectorAll('.djm-fav-rm').forEach(btn => { | |
| btn.addEventListener('click', function () { | |
| const name = this.dataset.n; | |
| state.favorites.delete(name); | |
| saveFavorites(); | |
| renderFavorites(); | |
| addLog('\u2716 Removed: ' + name); | |
| state.currentDJ = null; | |
| checkDJ(); | |
| }); | |
| }); | |
| } | |
| function updateUI() { | |
| const dot = document.getElementById('djm-dot'); | |
| const label = document.getElementById('djm-label'); | |
| const now = document.getElementById('djm-now'); | |
| const qadd = document.getElementById('djm-qadd'); | |
| const songEl = document.getElementById('djm-song'); | |
| if (!dot) return; | |
| if (!state.enabled) { | |
| dot.className = 'djm-off'; | |
| label.textContent = 'DISABLED'; | |
| } else if (state.isMuted) { | |
| dot.className = 'djm-muted'; | |
| label.textContent = 'MUTED'; | |
| } else if (state.currentDJ) { | |
| dot.className = 'djm-listening'; | |
| label.textContent = 'PLAYING'; | |
| } else { | |
| dot.className = 'djm-off'; | |
| label.textContent = 'DJ MUTER'; | |
| } | |
| if (now && state.currentDJ) { | |
| const djLower = state.currentDJ.toLowerCase(); | |
| const isFav = state.favorites.has(djLower) || | |
| (state.myUsername && djLower === state.myUsername.toLowerCase()); | |
| now.innerHTML = '<span class="dn">' + esc(state.currentDJ) + '</span>' + | |
| (state.isMuted ? ' \u2014 muted' : ' \u2014 playing'); | |
| now.className = state.isMuted ? 'is-muted' : (isFav ? 'is-fav' : ''); | |
| qadd.style.display = isFav ? 'none' : 'block'; | |
| } else if (now) { | |
| now.innerHTML = 'Waiting for DJ...'; | |
| now.className = ''; | |
| qadd.style.display = 'none'; | |
| } | |
| if (songEl) { | |
| const artist = document.querySelector('.songboard-artist.songboard-main'); | |
| const song = document.querySelector('.songboard-song.songboard-main'); | |
| const a = artist ? artist.textContent.trim() : ''; | |
| const s = song ? song.textContent.trim() : ''; | |
| songEl.textContent = (a && s) ? a + ' \u2014 ' + s : ''; | |
| } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // LIFECYCLE — mount/unmount based on room context | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function syncToRoomContext() { | |
| const inRoom = isInsideRoom(); | |
| if (inRoom && !state.uiMounted) { | |
| createUI(); | |
| addLog('\u{1F680} DJ Muter v2.1 — entered room'); | |
| if (state.favorites.size === 0 && !state.myUsername) { | |
| addLog('\u{1F4A1} Set your username & add favorites'); | |
| } else { | |
| if (state.myUsername) addLog('\u{1F464} You: ' + state.myUsername); | |
| if (state.favorites.size > 0) addLog('\u2B50 ' + state.favorites.size + ' favorite(s) loaded'); | |
| } | |
| // Give the room a beat to render YT iframe / DJ name | |
| setTimeout(() => { startWatching(); checkDJ(); }, 800); | |
| } else if (!inRoom && state.uiMounted) { | |
| stopWatching(); | |
| destroyUI(); | |
| } | |
| } | |
| function watchRouteChanges() { | |
| // 1. History API hooks for SPA navigation | |
| const fire = () => setTimeout(syncToRoomContext, 50); | |
| ['pushState', 'replaceState'].forEach(m => { | |
| const orig = history[m]; | |
| history[m] = function () { | |
| const r = orig.apply(this, arguments); | |
| fire(); | |
| return r; | |
| }; | |
| }); | |
| window.addEventListener('popstate', fire); | |
| window.addEventListener('hashchange', fire); | |
| // 2. Periodic re-check — catches client-side renders where the URL | |
| // doesn't change but the room DOM mounts/unmounts (e.g. modal-style | |
| // room joins), and is a cheap safety net. | |
| setInterval(syncToRoomContext, 2000); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // INIT | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function init() { | |
| watchRouteChanges(); | |
| // Small delay so the initial page has a chance to render room DOM | |
| setTimeout(syncToRoomContext, 1000); | |
| } | |
| if (document.readyState === 'complete' || document.readyState === 'interactive') | |
| setTimeout(init, 500); | |
| else | |
| window.addEventListener('load', () => setTimeout(init, 500)); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment