// ==UserScript== // @name canvas.cdawgva - targeted pixel mute // @namespace http://tampermonkey.net/ // @version 1.18 // @description Targeted pixel notification mute + custom replacement // @match https://canvas.cdawgva.com/* // @grant none // @run-at document-start // @updateURL https://raw.githubusercontent.com/swordofbling/cdawgva-pixel-sound/main/cdawgva-pixel-sound.user.js // @downloadURL https://raw.githubusercontent.com/swordofbling/cdawgva-pixel-sound/main/cdawgva-pixel-sound.user.js // ==/UserScript== (function () { 'use strict'; // ---------- CONFIG ---------- const STORAGE_KEY = 'cdawgva_sound_settings_v9'; const DEFAULTS = { allowed: [6], volume: 100, preAllowPrev: true }; // preAllowPrev enforced const POLL_INTERVAL = 400; // debug flag (toggled by Alt+Shift+S) window.__CDGVA_SOUND_DEBUG = false; // ---------- Fingerprint seeds ---------- const TARGET_FINGERPRINTS = [ { sr: 48000, len: 51270, ch: 2, smallHash: null, note: 'pixel-notif-seed-1' }, { sr: 48000, len: 13375, ch: 1, smallHash: null, note: 'pixel-notif-seed-2' } ]; const STACK_PATTERNS = [ /canvas\.cdawgva\.com\/:328:94808/, /canvas\.cdawgva\.com\/:318:10022/, /play@https?:\/\/canvas\.cdawgva\.com/ ]; // ---------- storage helpers ---------- function loadSettings() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return Object.assign({}, DEFAULTS); const s = Object.assign({}, DEFAULTS, JSON.parse(raw)); s.preAllowPrev = true; // enforced if (!Array.isArray(s.allowed)) s.allowed = Array.isArray(DEFAULTS.allowed) ? DEFAULTS.allowed.slice() : [6]; return s; } catch (e) { console.warn('cdawgva-sound: loadSettings error', e); return Object.assign({}, DEFAULTS); } } function saveSettings(s) { try { s = Object.assign({}, s, { preAllowPrev: true }); localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch (e) { console.warn('cdawgva-sound: saveSettings error', e); } } // ---------- custom replacement storage (no replaceAlways) ---------- function loadCustomSettings() { try { const raw = localStorage.getItem(STORAGE_KEY + '_custom'); if (!raw) return { mode: null, url: null, dataUrl: null, playWhenAllowed: true }; const parsed = JSON.parse(raw); parsed.playWhenAllowed = true; // enforced return parsed; } catch (e) { return { mode: null, url: null, dataUrl: null, playWhenAllowed: true }; } } function saveCustomSettings(s) { try { s = Object.assign({}, s, { playWhenAllowed: true }); localStorage.setItem(STORAGE_KEY + '_custom', JSON.stringify(s)); } catch (e) { console.warn('custom settings save error', e); } } // ---------- fingerprint helpers ---------- function captureStackString() { try { const s = (new Error()).stack || ''; return s.split('\n').slice(2).join('\n'); } catch (e) { return ''; } } function stackMatches(stack) { if (!stack) return false; try { return STACK_PATTERNS.some(rx => rx.test(stack)); } catch (e) { return false; } } function computeSmallHashFromBuffer(buffer, sampleCount = 4096) { try { if (!buffer || typeof buffer.length !== 'number') return null; const ch = buffer.numberOfChannels || 1; const len = Math.min(sampleCount, buffer.length); let acc = 2166136261 >>> 0; for (let c = 0; c < ch; c++) { const data = buffer.getChannelData(c); for (let i = 0; i < len; i += 4) { const v = Math.floor((data[i] || 0) * 32767); acc ^= (v & 0xFFFF); acc = (acc * 16777619) >>> 0; } } return acc >>> 0; } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('fingerprint compute error', e); return null; } } function fingerprintFromBuffer(buffer) { if (!buffer) return null; return { sr: buffer.sampleRate || null, len: buffer.length || null, ch: buffer.numberOfChannels || null, smallHash: computeSmallHashFromBuffer(buffer) }; } function fingerprintMatches(a, b) { if (!a || !b) return false; if (a.len !== b.len) return false; if (a.ch !== b.ch) return false; if (a.sr !== b.sr) return false; if ((a.smallHash != null) && (b.smallHash != null)) return a.smallHash === b.smallHash; return true; } // persist/load fingerprints function persistFingerprints() { try { const toSave = TARGET_FINGERPRINTS.map(p => ({ sr: p.sr, len: p.len, ch: p.ch, smallHash: p.smallHash, note: p.note })); localStorage.setItem(STORAGE_KEY + '_fingerprints', JSON.stringify(toSave)); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('persist fingerprints failed', e); } } function loadPersistedFingerprints() { try { const raw = localStorage.getItem(STORAGE_KEY + '_fingerprints'); if (!raw) return; const arr = JSON.parse(raw); if (!Array.isArray(arr)) return; for (const p of arr) { const exists = TARGET_FINGERPRINTS.some(tp => tp.sr === p.sr && tp.len === p.len && tp.ch === p.ch && (tp.smallHash === p.smallHash)); if (!exists) TARGET_FINGERPRINTS.push({ sr: p.sr, len: p.len, ch: p.ch, smallHash: p.smallHash || null, note: p.note || 'persisted' }); } } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('load persisted fingerprints error', e); } } loadPersistedFingerprints(); // ---------- page fraction / settings logic ---------- function parseFractionFromText(text) { if (!text) return null; let m = text.match(/\b(\d+)\s*(?:of)\s*(\d+)\b/i); if (m) return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) }; m = text.match(/\b(\d+)\s*\/\s*(\d+)\b/); if (m) return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) }; return null; } function queryFractionFromDOM() { try { const selectors = [ 'nav.grid-5.switcher .switcher-button.active small', 'nav.switcher .switcher-button.active small', 'nav .switcher-button.active small', 'nav.grid-5.switcher', 'nav.switcher' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (!el) continue; const txt = (el.innerText || el.textContent || '').trim(); const f = parseFractionFromText(txt); if (f) return f; const span = el.querySelector('span'); if (span) { const f2 = parseFractionFromText((span.innerText || span.textContent || '').trim()); if (f2) return f2; } } if (document.title) { const ft = parseFractionFromText(document.title); if (ft) return ft; } } catch (e) {} return null; } let lastFraction = null, lastFractionTime = 0; function updateCachedFraction() { const f = queryFractionFromDOM(); if (f) { lastFraction = f; lastFractionTime = Date.now(); return true; } return false; } setInterval(updateCachedFraction, POLL_INTERVAL); function startFractionObserver() { try { const target = document.querySelector('nav.grid-5.switcher') || document.querySelector('nav.switcher') || document.documentElement; if (!target) return; const mo = new MutationObserver(() => updateCachedFraction()); mo.observe(target, { childList: true, subtree: true, characterData: true }); updateCachedFraction(); } catch (e) { setTimeout(startFractionObserver, 500); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', startFractionObserver, { once: true }); else startFractionObserver(); function findCurrentFractionWithCache() { const f = queryFractionFromDOM(); if (f) return f; if (lastFraction && (Date.now() - lastFractionTime) <= 1500) return lastFraction; return null; } // ---------- shouldAllowAudioNow: preAllowPrev always true ---------- function shouldAllowAudioNow(settings) { const frac = findCurrentFractionWithCache(); if (!frac) { if (window.__CDGVA_SOUND_DEBUG) console.debug('no fraction -> block by default'); return false; } const preAllowPrev = true; if (settings.allowed.includes(frac.x)) { if (window.__CDGVA_SOUND_DEBUG) console.debug('allowed by list', frac.x); return true; } if (preAllowPrev && settings.allowed.includes(frac.x + 1)) { if (window.__CDGVA_SOUND_DEBUG) console.debug('preAllowPrev allowed', frac.x); return true; } if (window.__CDGVA_SOUND_DEBUG) console.debug('blocked by list', frac.x); return false; } // ---------- custom sound management ---------- let customAudioContext = null; let customAudioBuffer = null; let customSettings = loadCustomSettings(); function ensureAudioContext() { try { if (!customAudioContext) { const AC = window.AudioContext || window.webkitAudioContext; customAudioContext = new (AC)(); } return customAudioContext; } catch (e) { return null; } } async function decodeArrayBufferToBuffer(arrayBuffer) { try { const ctx = ensureAudioContext(); if (!ctx) return null; return await ctx.decodeAudioData(arrayBuffer.slice(0)); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('decode error', e); return null; } } async function loadCustomFromUrl(url) { try { if (!url) return null; const resp = await fetch(url, {mode:'cors'}); if (!resp.ok) throw new Error('fetch failed ' + resp.status); const ab = await resp.arrayBuffer(); const buf = await decodeArrayBufferToBuffer(ab); if (buf) { customAudioBuffer = buf; customSettings = Object.assign(customSettings, { mode: 'url', url, playWhenAllowed: true }); saveCustomSettings(customSettings); console.info('cdawgva-sound: loaded custom audio from URL', url); return buf; } } catch (e) { console.warn('cdawgva-sound: loadCustomFromUrl error', e); } return null; } async function loadCustomFromDataUrl(dataUrl) { try { if (!dataUrl) return null; const comma = dataUrl.indexOf(','); if (comma < 0) return null; const b64 = dataUrl.slice(comma + 1); const binary = atob(b64); const len = binary.length; const ab = new ArrayBuffer(len); const dv = new Uint8Array(ab); for (let i = 0; i < len; i++) dv[i] = binary.charCodeAt(i); const buf = await decodeArrayBufferToBuffer(ab); if (buf) { customAudioBuffer = buf; customSettings = Object.assign(customSettings, { mode: 'data', dataUrl, playWhenAllowed: true }); saveCustomSettings(customSettings); console.info('cdawgva-sound: loaded custom audio from data URL'); return buf; } } catch (e) { console.warn('cdawgva-sound: loadCustomFromDataUrl error', e); } return null; } (function initCustomAtStart() { try { customSettings = loadCustomSettings(); if (customSettings.dataUrl) { loadCustomFromDataUrl(customSettings.dataUrl); } else if (customSettings.url) { loadCustomFromUrl(customSettings.url); } } catch (e) {} })(); function playCustomSound(volume = 1) { try { const buf = customAudioBuffer; if (!buf) return false; const ctx = ensureAudioContext(); if (!ctx) return false; const src = ctx.createBufferSource(); src.buffer = buf; const g = ctx.createGain(); g.gain.value = Math.max(0, Math.min(1, volume)); src.connect(g); g.connect(ctx.destination); try { src.start(0); } catch (e) { try { src.start(); } catch (ee) {} } const dur = (buf.duration || 1) + 0.1; setTimeout(() => { try { src.disconnect(); g.disconnect(); } catch (_) {} }, (dur * 1000) + 200); return true; } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('playCustomSound error', e); return false; } } // ---------- last seen fingerprint ---------- let lastBlockedFingerprint = null; // ---------- patch AudioBufferSourceNode.start (conditional + replacement, no replaceAlways) ---------- (function patchAudioStartConditional() { if (window.__cdgva_start_patched_v118) return; window.__cdgva_start_patched_v118 = true; try { if (typeof AudioBufferSourceNode === 'undefined' || !AudioBufferSourceNode.prototype) { if (window.__CDGVA_SOUND_DEBUG) console.log('AudioBufferSourceNode not present yet'); return; } const origStart = AudioBufferSourceNode.prototype.start; AudioBufferSourceNode.prototype.start = function (...args) { try { const buffer = this.buffer; const stack = captureStackString(); if (!buffer) return origStart.apply(this, args); const fp = fingerprintFromBuffer(buffer); if (!fp) return origStart.apply(this, args); lastBlockedFingerprint = fp; const matchedTarget = TARGET_FINGERPRINTS.find(t => fingerprintMatches(t, fp)); if (stackMatches(stack) && matchedTarget) { const settings = loadSettings(); const custom = loadCustomSettings(); const allowNow = shouldAllowAudioNow(settings); // playWhenAllowed forced true by design if (allowNow) { if (customAudioBuffer) { try { this.__cdgva_blocked = true; } catch (_) {} if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: allowed pixel -> playing custom replacement', matchedTarget); playCustomSound(getVolumeFloat(settings)); return; } else { if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: allowed pixel but no custom audio -> letting original play', matchedTarget); return origStart.apply(this, args); } } else { try { this.__cdgva_blocked = true; } catch (_) {} if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: pixel not allowed -> blocking original', matchedTarget); return; } } return origStart.apply(this, args); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('cdawgva-sound: error in AudioBufferSourceNode.start patch', e); return origStart.apply(this, args); } }; if (window.__CDGVA_SOUND_DEBUG) console.log('cdawgva-sound: AudioBufferSourceNode.start patched (v1.18)'); } catch (e) { console.warn('cdawgva-sound: failed to patch AudioBufferSourceNode.start', e); } })(); // ---------- Howler fallback (same rules) ---------- (function patchHowlerFallbackConditionalV118() { if (window.__cdgva_howler_patched_v118) return; window.__cdgva_howler_patched_v118 = true; try { const Howl = window.Howl || (window.Howler && window.Howler.Howl); if (!Howl || !Howl.prototype) { if (window.__CDGVA_SOUND_DEBUG) console.log('Howler not present yet for patch'); return; } const origPlay = Howl.prototype.play; Howl.prototype.play = function (...args) { try { const stack = captureStackString(); if (!stackMatches(stack)) return origPlay.apply(this, args); const src = this._src || this._srcs || this._srcArray || ''; const spriteKeys = this._sprite ? Object.keys(this._sprite) : []; const metaBlob = (src ? JSON.stringify(src) : '') + '|' + JSON.stringify(spriteKeys); const looksLikePixel = /pixel/i.test(metaBlob) || /notification/i.test(metaBlob); const settingsNow = loadSettings(); const custom = loadCustomSettings(); const allowNow = shouldAllowAudioNow(settingsNow); if (looksLikePixel) { const id = origPlay.apply(this, args); if (allowNow) { if (customAudioBuffer) { try { if (typeof this.stop === 'function') this.stop(id); } catch (_) {} playCustomSound(getVolumeFloat(settingsNow)); console.info('cdawgva-sound: replaced Howler.play (allowed) with custom', { id }); return id; } else { if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: Howler.play allowed but no custom -> let play', { id }); return id; } } else { try { if (typeof this.stop === 'function') this.stop(id); } catch (_) {} console.info('cdawgva-sound: Howler.play blocked (not allowed)', { id }); return id; } } } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('Howler.play replacement error', e); } return origPlay.apply(this, args); }; if (window.__CDGVA_SOUND_DEBUG) console.log('cdawgva-sound: Howler.play patched (v1.18)'); } catch (e) { console.warn('cdawgva-sound: failed to patch Howler.play', e); } })(); // ---------- volume helper ---------- function getVolumeFloat(settings) { const v = Math.max(0, Math.min(100, Number(settings.volume || 100))); return v / 100; } // ---------- UI: badge with allowed checkboxes restored ---------- function createBadgeUI() { if (document.getElementById('cdawg-sound-panel-root')) return; const settings = loadSettings(); customSettings = loadCustomSettings(); const root = document.createElement('div'); root.id = 'cdawg-sound-panel-root'; root.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:2147483647;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;'; const badge = document.createElement('button'); badge.id = 'cdawg-sound-badge'; badge.type = 'button'; badge.title = 'cdawgva sound settings'; badge.style.cssText = 'width:54px;height:44px;border-radius:10px;border:0;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:16px;padding:6px;'; badge.innerHTML = '
🔊
'; const panel = document.createElement('div'); panel.style.cssText = 'display:none;position:absolute;bottom:50px;right:0;width:360px;background:#fff;padding:10px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);color:#111;font-size:13px;'; panel.id = 'cdawg-sound-panel'; const header = document.createElement('div'); header.style.cssText = 'font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;'; header.innerHTML = 'Sound settingsv1.18'; // numbers checkboxes (1..6) const numbers = document.createElement('div'); numbers.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;'; for (let i = 1; i <= 6; i++) { const label = document.createElement('label'); label.style.cssText = 'display:flex;align-items:center;gap:6px;padding:3px 6px;border-radius:6px;cursor:pointer;border:1px solid transparent;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = String(i); cb.checked = (settings.allowed || []).includes(i); cb.style.cssText = 'width:16px;height:16px;'; cb.addEventListener('change', () => { const s = loadSettings(); const val = parseInt(cb.value, 10); if (cb.checked) { if (!s.allowed.includes(val)) s.allowed.push(val); } else { s.allowed = s.allowed.filter(x => x !== val); } s.allowed.sort((a,b)=>a-b); saveSettings(s); // update allowed line text if present const allowedLine = document.getElementById('cdawg-allowed-line'); if (allowedLine) allowedLine.innerText = 'Allowed pixels (stored): ' + (s.allowed || []).join(','); }); const span = document.createElement('span'); span.innerText = String(i); span.style.cssText = 'font-weight:600'; label.appendChild(cb); label.appendChild(span); numbers.appendChild(label); } // allowed line below header const allowedLine = document.createElement('div'); allowedLine.id = 'cdawg-allowed-line'; allowedLine.style.cssText = 'font-size:12px;color:#444;margin-bottom:8px;'; allowedLine.innerText = 'Allowed pixels (stored): ' + (settings.allowed || []).join(','); // volume row const volRow = document.createElement('div'); volRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;'; const volLabel = document.createElement('div'); volLabel.innerText = 'Volume'; volLabel.style.cssText = 'flex:1;opacity:.85;'; const volValue = document.createElement('div'); volValue.innerText = `${settings.volume}%`; volValue.style.cssText = 'width:44px;text-align:right;opacity:.85;'; const volInput = document.createElement('input'); volInput.type = 'range'; volInput.min = '0'; volInput.max = '100'; volInput.value = String(settings.volume); volInput.style.cssText = 'width:160px;'; volInput.addEventListener('input', () => { volValue.innerText = `${volInput.value}%`; }); volInput.addEventListener('change', () => { const s = loadSettings(); s.volume = Number(volInput.value); saveSettings(s); }); volRow.appendChild(volLabel); volRow.appendChild(volInput); volRow.appendChild(volValue); // info line removed (per your request) — we keep enforcement in code // custom sound controls const customSection = document.createElement('div'); customSection.style.cssText = 'border-top:1px solid #eee;padding-top:8px;margin-top:8px;'; const customHeader = document.createElement('div'); customHeader.innerText = 'Custom replacement sound'; customHeader.style.cssText = 'font-weight:600;margin-bottom:6px;'; customSection.appendChild(customHeader); const urlRow = document.createElement('div'); urlRow.style.cssText = 'display:flex;gap:6px;margin-bottom:6px;'; const urlInput = document.createElement('input'); urlInput.type = 'text'; urlInput.placeholder = 'https://example.com/your.wav'; urlInput.style.cssText = 'flex:1;padding:6px;border-radius:6px;border:1px solid #ddd;'; urlInput.value = (customSettings && customSettings.url) ? customSettings.url : ''; const urlBtn = document.createElement('button'); urlBtn.innerText = 'Load'; urlBtn.style.cssText = 'padding:6px;border-radius:6px;border:0;background:#111;color:#fff;cursor:pointer;'; urlBtn.addEventListener('click', async () => { const val = urlInput.value.trim(); if (!val) { alert('Enter a URL'); return; } urlBtn.disabled = true; urlBtn.innerText = 'Loading...'; const buf = await loadCustomFromUrl(val); urlBtn.disabled = false; urlBtn.innerText = 'Load'; if (buf) alert('Custom sound loaded from URL. It will be used when the selected pixel is allowed.'); else alert('Failed to load/parse audio from URL (CORS or unsupported format?). Check console.'); }); urlRow.appendChild(urlInput); urlRow.appendChild(urlBtn); customSection.appendChild(urlRow); const fileRow = document.createElement('div'); fileRow.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:6px;'; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'audio/*'; fileInput.style.cssText = 'flex:1;'; const fileBtn = document.createElement('button'); fileBtn.innerText = 'Upload'; fileBtn.style.cssText = 'padding:6px;border-radius:6px;border:0;background:#111;color:#fff;cursor:pointer;'; fileBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', async () => { const file = fileInput.files && fileInput.files[0]; if (!file) return; try { const reader = new FileReader(); reader.onload = async (ev) => { const ab = ev.target.result; const buf = await decodeArrayBufferToBuffer(ab); if (!buf) { alert('Failed to decode audio file'); return; } const r2 = new FileReader(); r2.onload = (ev2) => { const dataUrl = ev2.target.result; customSettings = Object.assign(customSettings, { mode: 'data', dataUrl, playWhenAllowed: true }); saveCustomSettings(customSettings); loadCustomFromDataUrl(dataUrl); alert('Uploaded and stored custom audio (saved to localStorage). Will play when selected.'); }; r2.readAsDataURL(file); }; reader.readAsArrayBuffer(file); } catch (e) { console.warn('upload error', e); alert('Upload failed'); } }); fileRow.appendChild(fileInput); fileRow.appendChild(fileBtn); customSection.appendChild(fileRow); const testCustomBtn = document.createElement('button'); testCustomBtn.innerText = 'Play custom sound now'; testCustomBtn.style.cssText = 'padding:8px;border-radius:8px;border:0;background:#0a84ff;color:#fff;cursor:pointer;width:100%;'; testCustomBtn.addEventListener('click', () => { const s = loadSettings(); if (!customAudioBuffer) { alert('No custom audio loaded yet. Use URL or upload.'); return; } playCustomSound(getVolumeFloat(s)); }); customSection.appendChild(testCustomBtn); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-top:8px;'; const resetBtn = document.createElement('button'); resetBtn.type='button'; resetBtn.innerText='Reset'; resetBtn.style.cssText='padding:6px 8px;border-radius:8px;border:0;cursor:pointer;background:#eee;'; resetBtn.addEventListener('click', () => { saveSettings(DEFAULTS); panel.remove(); document.getElementById('cdawg-sound-panel-root')?.remove(); setTimeout(createBadgeUI, 50); }); const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.innerText='Close'; closeBtn.style.cssText='padding:6px 8px;border-radius:8px;border:0;cursor:pointer;background:#eee;'; closeBtn.addEventListener('click', () => { panel.style.display='none'; }); footer.appendChild(resetBtn); footer.appendChild(closeBtn); panel.appendChild(header); panel.appendChild(numbers); panel.appendChild(allowedLine); panel.appendChild(volRow); panel.appendChild(customSection); panel.appendChild(footer); badge.addEventListener('click', (e) => { e.stopPropagation(); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); document.addEventListener('click', (e) => { if (!root.contains(e.target)) panel.style.display='none'; }); root.appendChild(panel); root.appendChild(badge); function attach() { try { document.body.appendChild(root); } catch (e) { setTimeout(attach, 50); } } attach(); function findTimerTextFromDOM() { try { const timeEl = document.querySelector('button.switcher-button.active time, nav .switcher-button.active time'); if (timeEl) { const human = (timeEl.textContent || '').trim(); const title = timeEl.getAttribute('title'); if (human) return human; if (title) return title; } const small = document.querySelector('button.switcher-button.active small, nav .switcher-button.active small'); if (small) { const t = (small.textContent || '').trim(); if (t && /in\s+\d+/.test(t)) return t; } const anyTime = document.querySelector('time[datetime]'); if (anyTime) return (anyTime.textContent || '').trim() || anyTime.getAttribute('title') || null; } catch (e) {} return null; } function updateBadgeDisplay() { const main = document.getElementById('cdawg-sound-badge-main'); if (!main) return; const timer = findTimerTextFromDOM(); const frac = findCurrentFractionWithCache(); let display = '🔊'; let titleParts = []; if (timer) { const short = timer.replace(/\s+/g, ' ').trim(); display = `🔊\n${short}`; titleParts.push(`Timer: ${short}`); } else if (frac) { display = `🔊\n${frac.x}/${frac.y || '?'}`; titleParts.push(`Pixels: ${frac.x} of ${frac.y || '?'}`); } else { const ft = (document.title && (function(){ try { return parseFractionFromText(document.title); } catch(e){return null;} })()) || null; if (ft) { display = `🔊\n${ft.x}/${ft.y || '?'}`; titleParts.push(`Pixels (title): ${ft.x} of ${ft.y}`); } else { display = '🔊'; titleParts.push('No timer or pixels found'); } } main.innerHTML = display.split('\n').map((line, idx) => idx === 0 ? `
${line}
` : `
${line}
`).join(''); const settingsNow = loadSettings(); const allowedList = (settingsNow.allowed || []).join(','); const tt = `${titleParts.join(' - ')}\nAllowed: [${allowedList}] - Volume: ${settingsNow.volume}%`; badge.title = tt; } setInterval(updateBadgeDisplay, POLL_INTERVAL); setTimeout(updateBadgeDisplay, 60); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createBadgeUI); else createBadgeUI(); // ---------- debug panel (created but hidden by default). Alt+Shift+S toggles visibility and debug flag ---------- function createDebugPanel() { if (document.getElementById('cdgva-sound-debug-root')) return; const root = document.createElement('div'); root.id = 'cdgva-sound-debug-root'; root.style.cssText = 'position:fixed;left:12px;bottom:12px;z-index:2147483646;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:12px;display:none;'; const box = document.createElement('div'); box.style.cssText = 'background:#fff;border-radius:8px;padding:8px;box-shadow:0 6px 18px rgba(0,0,0,0.18);width:320px;'; box.innerHTML = `
cdawgva-sound debug
`; const info = document.createElement('div'); info.style.cssText = 'font-size:12px;color:#333;margin-bottom:8px;'; info.innerText = 'Debug panel: verbose logs are enabled while visible. Press Alt+Shift+S to hide and disable debug logs.'; box.appendChild(info); const addLastBtn = document.createElement('button'); addLastBtn.innerText = 'Add last seen fingerprint'; addLastBtn.style.cssText = 'padding:6px;border-radius:6px;border:0;cursor:pointer;background:#eee;width:100%;margin-top:6px;'; addLastBtn.addEventListener('click', () => { if (!lastBlockedFingerprint) { alert('No last seen buffer fingerprint recorded yet. Reproduce the target sound (with debug visible) first.'); return; } const entry = (function addTargetFingerprint(fp) { if (!fp) return; const entry = { sr: fp.sr, len: fp.len, ch: fp.ch, smallHash: fp.smallHash || null, note: fp.note || ('added-' + Date.now()) }; TARGET_FINGERPRINTS.push(entry); persistFingerprints(); console.info('cdawgva-sound: added target fingerprint', entry); return entry; })(lastBlockedFingerprint); alert('Added fingerprint: ' + JSON.stringify({ sr: entry.sr, len: entry.len, ch: entry.ch, smallHash: entry.smallHash })); }); box.appendChild(addLastBtn); const list = document.createElement('div'); list.style.cssText = 'margin-top:8px;max-height:220px;overflow:auto;font-size:11px;opacity:.95;'; function refreshList() { list.innerHTML = TARGET_FINGERPRINTS.map((t, i) => `
#${i+1} ${t.sr}/${t.len}/ch${t.ch}${t.smallHash? ' hash:'+t.smallHash : ''} ${t.note? ' — '+t.note : ''}
`).join(''); } refreshList(); box.appendChild(list); const removeLastBtn = document.createElement('button'); removeLastBtn.innerText = 'Remove last fingerprint'; removeLastBtn.style.cssText = 'padding:6px;border-radius:6px;border:0;cursor:pointer;background:#fee;width:100%;margin-top:6px;'; removeLastBtn.addEventListener('click', () => { TARGET_FINGERPRINTS.pop(); persistFingerprints(); refreshList(); alert('Removed last fingerprint'); }); box.appendChild(removeLastBtn); root.appendChild(box); function attach() { try { document.body.appendChild(root); } catch (e) { setTimeout(attach, 200); } } attach(); setInterval(refreshList, 2000); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createDebugPanel, { once: true }); else createDebugPanel(); // Shortcut: Alt+Shift+S toggles debug panel visibility and debug flag window.addEventListener('keydown', (e) => { if (e.altKey && e.shiftKey && (e.key === 'S' || e.key === 's')) { const root = document.getElementById('cdgva-sound-debug-root'); if (!root) return; if (root.style.display === 'none' || !root.style.display) { root.style.display = 'block'; window.__CDGVA_SOUND_DEBUG = true; console.info('cdawgva-sound: debug panel shown — verbose logging enabled.'); } else { root.style.display = 'none'; window.__CDGVA_SOUND_DEBUG = false; console.info('cdawgva-sound: debug panel hidden — verbose logging disabled.'); } } }); // ---------- small console API ---------- window.__cdgva_sound = window.__cdgva_sound || {}; window.__cdgva_sound.getTargetFingerprints = () => TARGET_FINGERPRINTS.slice(); window.__cdgva_sound.addFingerprint = (obj) => { TARGET_FINGERPRINTS.push(obj); persistFingerprints(); }; window.__cdgva_sound.lastSeen = () => lastBlockedFingerprint; window.__cdgva_sound.setDebug = (v) => { window.__CDGVA_SOUND_DEBUG = !!v; const root = document.getElementById('cdgva-sound-debug-root'); if (root) root.style.display = window.__CDGVA_SOUND_DEBUG ? 'block' : 'none'; return window.__CDGVA_SOUND_DEBUG; }; console.info('cdawgva-sound: v1.18 loaded — allowed checkboxes restored. Alt+Shift+S toggles debug panel.'); })();