Detects @mentions and keywords in TorrentBD shoutbox and alerts you with sound, toast popups, custom notifications, and customizable highlights.
// ==UserScript==
// @name TBD Shoutbox Notifier
// @version 1.1
// @description Detects @mentions and keywords in TorrentBD shoutbox and alerts you with sound, toast popups, custom notifications, and customizable highlights.
// @author Anik
// @namespace https://aonexyz.vercel.app/
// @match https://*.torrentbd.com/*
// @match https://*.torrentbd.net/*
// @match https://*.torrentbd.org/*
// @match https://*.torrentbd.me/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect github.com
// @connect raw.githubusercontent.com
// @connect objects.githubusercontent.com
// @license MIT
// @run-at document-end
// @icon https://iili.io/Bb6kpwb.png
// ==/UserScript==
(function () {
'use strict';
/* ═══════════════════════════════════════════════════════
STORE
═══════════════════════════════════════════════════════ */
const Store = {
get: (k, d) => GM_getValue(k, d),
set: (k, v) => GM_setValue(k, v),
};
/* ═══════════════════════════════════════════════════════
CONFIG
hlRowEnabled → left border + bg tint on row (default OFF, user toggles)
hlMentionOn → @username TEXT gets coloured (default ON, user can toggle off)
hlColor → colour for row border/bg
mentionColor → colour for username text highlight
═══════════════════════════════════════════════════════ */
const Cfg = {
username: Store.get('v1_username',''),
volume: Store.get('v1_volume',0.65),
hlColor: Store.get('v1_hlColor','#4ade80'),
mentionColor: Store.get('v1_mentionColor','#60a5fa'),
hlRowEnabled: Store.get('v1_hlRow',true),
hlMentionOn: Store.get('v1_hlMention',true),
keywords: Store.get('v1_keywords',[]),
popupMention: Store.get('v1_popupMention', true),
popupKeywords: Store.get('v1_popupKeywords', true),
// sound — stored as base64 data-URLs so they survive page reload
customSoundB64: Store.get('v1_customSoundB64', ''), // user-uploaded MP3
customSoundName: Store.get('v1_customSoundName', ''), // filename of user-uploaded MP3
defaultSoundUrl: Store.get('v1_defaultSound', 'https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-493469.mp3'),
defaultSoundB64: Store.get('v1_defaultSoundB64', ''), // fetched+cached version of defaultSoundUrl
darkMode: Store.get('v1_darkMode', true),
// keep old key aliases so existing save calls work
get customSoundUrl() { return this.customSoundB64; },
set customSoundUrl(v) { this.customSoundB64 = v; Store.set('v1_customSoundB64', v); },
save(k, v) { this[k] = v; Store.set(`v1_${k}`, v); },
};
/* ═══════════════════════════════════════════════════════
SEEN IDs
═══════════════════════════════════════════════════════ */
const Seen = (() => {
let list = Store.get('v1_seen', []);
return {
has: id => list.includes(id),
mark: id => {
if (list.includes(id)) return;
list.push(id);
if (list.length > 500) list.shift();
Store.set('v1_seen', list);
},
};
})();
/* ═══════════════════════════════════════════════════════
USERNAME AUTO-DETECT
═══════════════════════════════════════════════════════ */
function detectUser() {
for (const el of document.querySelectorAll('.tbdrank')) {
if (el.closest('#shoutbox-container')) continue;
const t = el.firstChild;
if (t?.nodeType === Node.TEXT_NODE) {
const name = t.nodeValue.trim();
if (name) return name;
}
}
return '';
}
/* ═══════════════════════════════════════════════════════
SOUND
• Custom sound → FileReader → base64 → GM_setValue → reload safe
• Default sound → GM_xmlhttpRequest (bypasses CORS) → base64 → GM_setValue
• AudioContext pre-decode → zero latency playback
═══════════════════════════════════════════════════════ */
const Sound = (() => {
let _ctx = null;
let _buffer = null;
function getCtx() {
if (!_ctx) _ctx = new (window.AudioContext || window.webkitAudioContext)();
return _ctx;
}
// Returns the active base64 data-URL (custom > cached default)
function activeSrc() {
if (Cfg.customSoundB64) return Cfg.customSoundB64;
if (Cfg.defaultSoundB64) return Cfg.defaultSoundB64;
return null;
}
// Decode a base64 data-URL into an AudioBuffer
function _decodeB64(dataUrl) {
return new Promise((resolve, reject) => {
try {
const ctx = getCtx();
const b64 = dataUrl.split(',')[1];
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
ctx.decodeAudioData(bytes.buffer.slice(0), resolve, reject);
} catch(e) { reject(e); }
});
}
// Fetch a remote URL via GM_xmlhttpRequest (bypasses CORS), save as base64
function _fetchAndCache(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
onload(res) {
try {
const bytes = new Uint8Array(res.response);
// Convert in chunks to avoid call stack overflow on large files
const CHUNK = 8192;
let bin = '';
for (let i = 0; i < bytes.length; i += CHUNK) {
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
}
const b64 = 'data:audio/mpeg;base64,' + btoa(bin);
// Cache it so future reloads don't need to fetch again
Cfg.defaultSoundB64 = b64;
Store.set('v1_defaultSoundB64', b64);
resolve(b64);
} catch(e) { reject(e); }
},
onerror(e) { reject(e); },
});
});
}
async function _load() {
try {
let src = activeSrc();
if (!src) {
// No cached base64 yet → fetch default sound now
src = await _fetchAndCache(Cfg.defaultSoundUrl);
}
_buffer = await _decodeB64(src);
} catch(e) {
_buffer = null;
}
}
function preload() { _load(); }
function reload() { _buffer = null; _load(); }
async function play() {
if (Cfg.volume < 0.01) return;
if (_buffer) {
try {
const ctx = getCtx();
// Resume AudioContext if suspended (browser policy)
if (ctx.state === 'suspended') {
try { await ctx.resume(); } catch(_) {}
}
const src = ctx.createBufferSource();
src.buffer = _buffer;
const gain = ctx.createGain();
gain.gain.value = Cfg.volume;
src.connect(gain);
gain.connect(ctx.destination);
src.start(0);
return;
} catch(_) {}
}
// fallback — Audio element (works even when tab is hidden in most browsers)
const fb = activeSrc() || Cfg.defaultSoundUrl;
try {
const a = new Audio(fb);
a.volume = Math.min(1, Math.max(0, Cfg.volume));
a.play().catch(() => {});
} catch(_) {}
}
// Instant preview: stream via Audio() immediately, cache buffer in background
function previewUrl(url) {
if (Cfg.volume < 0.01) return;
try {
const a = new Audio(url);
a.volume = Cfg.volume;
a.play().catch(() => {});
} catch(_) {}
// also cache the new buffer in background for future notification plays
_buffer = null;
_load();
}
return { preload, reload, play, previewUrl };
})();
/* ═══════════════════════════════════════════════════════
TOAST
═══════════════════════════════════════════════════════ */
const Toasts = (() => {
let wrap = null;
function getWrap() {
if (wrap) return wrap;
wrap = document.createElement('div');
wrap.id = 'tbn-toasts';
document.body.appendChild(wrap);
return wrap;
}
const ICONS = {
mention: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>`,
keyword: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>`,
};
function show(type) {
if (type === 'mention' && !Cfg.popupMention) return; // skip if mention popup disabled
if (type === 'keyword' && !Cfg.popupKeywords) return; // skip if keyword popup disabled
const el = document.createElement('div');
el.className = `tbn-toast tbn-toast-${type}`;
const iconBg = type === 'mention' ? `${Cfg.mentionColor}22` : 'rgba(96,165,250,.12)';
const iconClr = type === 'mention' ? Cfg.mentionColor : '#60a5fa';
el.innerHTML = `
<div class="tbn-t-icon" style="background:${iconBg};color:${iconClr};">${ICONS[type] || ICONS.keyword}</div>
<div class="tbn-t-body">
<div class="tbn-t-title">${type === 'mention' ? 'You were mentioned' : 'Keyword matched'}</div>
<div class="tbn-t-msg">${type === 'mention' ? 'Your name was @tagged in the shoutbox' : 'A keyword was detected in the shoutbox'}</div>
</div>
<button class="tbn-t-close" aria-label="Dismiss">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>`;
getWrap().appendChild(el);
requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('tbn-t-in')));
const dismiss = () => {
el.classList.remove('tbn-t-in');
el.classList.add('tbn-t-out');
setTimeout(() => el.remove(), 380);
};
el.querySelector('.tbn-t-close').addEventListener('click', dismiss);
setTimeout(dismiss, 5000);
}
return { show };
})();
/* ═══════════════════════════════════════════════════════
BROWSER NOTIFICATION (works when tab is hidden/minimized)
═══════════════════════════════════════════════════════ */
const BrowserNotify = (() => {
// Request permission once on load
function requestPermission() {
if (!('Notification' in window)) return;
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
function send(type) {
if (!('Notification' in window)) return;
if (Notification.permission !== 'granted') return;
const title = type === 'mention' ? '💬 You were mentioned!' : '🔑 Keyword matched!';
const body = type === 'mention'
? `Someone @tagged ${Cfg.username} in the shoutbox`
: 'A keyword was detected in the shoutbox';
try {
const n = new Notification(title, {
body,
icon: 'https://www.torrentbd.com/favicon.ico',
badge: 'https://www.torrentbd.com/favicon.ico',
tag: `tbn-${type}`, // replaces previous same-type notification
silent: true, // sound handled by Sound.play()
});
// Click on notification focuses the TBD tab
n.onclick = () => {
window.focus();
n.close();
};
// Auto-close after 6s
setTimeout(() => n.close(), 6000);
} catch (_) {}
}
return { requestPermission, send };
})();
/* ═══════════════════════════════════════════════════════
NOTIFY
═══════════════════════════════════════════════════════ */
function notify(type) {
Sound.play();
Toasts.show(type);
BrowserNotify.send(type);
}
/* ═══════════════════════════════════════════════════════
APPLY HIGHLIGHT TO A SHOUT ROW
1. hlMentionOn → wrap matching username text with coloured <mark>
2. hlRowEnabled → add left border + bg tint to the whole row
═══════════════════════════════════════════════════════ */
function applyHighlight(shoutEl, user) {
// ── 1. Username text highlight — check hlMentionOn ──
if (Cfg.hlMentionOn) {
const textEl = shoutEl.querySelector('.shout-text');
if (textEl && !textEl.querySelector('.tbn-mark')) {
const esc = user.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(@${esc}|${esc})`, 'gi');
textEl.innerHTML = textEl.innerHTML.replace(
regex,
`<mark class="tbn-mark" style="color:${Cfg.mentionColor};background:${Cfg.mentionColor}22;border-radius:3px;padding:0 2px;font-weight:700;font-style:normal;">$1</mark>`
);
}
}
// ── 2. Row border + bg ──
if (Cfg.hlRowEnabled) {
shoutEl.style.setProperty('--tbn-c', Cfg.hlColor);
shoutEl.classList.add('tbn-hl-row');
}
// ── 3. Flash ──
shoutEl.classList.remove('tbn-flash');
void shoutEl.offsetWidth;
shoutEl.classList.add('tbn-flash');
}
/* ═══════════════════════════════════════════════════════
DETECTION
═══════════════════════════════════════════════════════ */
function inspect(el, silent = false) {
if (!el?.id) return;
const body = el.querySelector('.shout-text');
if (!body) return;
const bodyText = body.textContent || '';
const user = Cfg.username.trim();
if (!user) return;
const done = Seen.has(el.id);
const esc = user.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Only check message body — rowText contains sender name and causes false positives
const mentionPats = [
new RegExp(`@${esc}`, 'i'),
new RegExp(`@${esc}\\.`, 'i'),
];
if (mentionPats.some(rx => rx.test(bodyText)) || bodyText.toLowerCase().includes('@' + user.toLowerCase())) {
applyHighlight(el, user);
if (!done) { if (!silent) notify('mention'); Seen.mark(el.id); }
return;
}
// keyword detection
for (const kw of Cfg.keywords.filter(k => k.trim())) {
const kwEsc = kw.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (new RegExp(`\\b${kwEsc}\\b`, 'i').test(bodyText)) {
// highlight matched keyword text — white + underline only (no row highlight)
if (!body.querySelector('.tbn-kw-mark')) {
const kwRegex = new RegExp(`(\\b${kwEsc}\\b)`, 'gi');
body.innerHTML = body.innerHTML.replace(
kwRegex,
`<mark class="tbn-kw-mark">$1</mark>`
);
}
el.classList.remove('tbn-flash');
void el.offsetWidth;
el.classList.add('tbn-flash');
if (!done) { if (!silent) notify('keyword'); Seen.mark(el.id); }
return;
}
}
}
/* ═══════════════════════════════════════════════════════
BUILD MODAL HTML
═══════════════════════════════════════════════════════ */
function buildModal() {
const modal = document.createElement('div');
modal.id = 'tbn-modal';
modal.style.display = 'none';
modal.innerHTML = `
<div id="tbn-backdrop"></div>
<div id="tbn-panel" role="dialog" aria-label="Notifier Settings">
<div id="tbn-hdr">
<div id="tbn-hdr-l">
<div id="tbn-hdr-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
</div>
<div>
<h2>Notifier Settings</h2>
<p>Configure your alerts and triggers</p>
</div>
</div>
<button id="tbn-close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</div>
<div id="tbn-body">
<!-- Username -->
<div class="tbn-fg">
<label class="tbn-label">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>
Username
</label>
<div class="tbn-iw">
<input id="tbn-uname" type="text" placeholder="Detection username..." autocomplete="off" spellcheck="false">
<div id="tbn-uname-dot"></div>
</div>
</div>
<!-- Custom Theme -->
<div class="tbn-fg">
<label class="tbn-label">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></svg>
Custom Theme
</label>
<!-- Row 1: two toggle cards -->
<div id="tbn-theme-grid">
<!-- Highlight Color (row border) — default OFF -->
<button class="tbn-tc" id="tog-hlrow">
<div class="tbn-tc-l">
<span class="tbn-tc-ico" id="ico-hlrow">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.66 7.93 12 2.27 6.34 7.93c-3.12 3.12-3.12 8.19 0 11.31C7.9 20.8 9.95 21.58 12 21.58c2.05 0 4.1-.78 5.66-2.34 3.12-3.12 3.12-8.19 0-11.31zM12 19.59c-1.6 0-3.11-.62-4.24-1.76C6.62 16.69 6 15.19 6 13.59c0-1.6.62-3.11 1.76-4.24L12 5.12v14.47z"/></svg>
</span>
<span class="tbn-tc-txt">Highlight Color</span>
</div>
<div class="tbn-dot" id="dot-hlrow"></div>
</button>
<!-- Highlight mention (username text) — default ON -->
<button class="tbn-tc" id="tog-hlmention">
<div class="tbn-tc-l">
<span class="tbn-tc-ico" id="ico-hlmention">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>
</span>
<span class="tbn-tc-txt">Highlight mention</span>
</div>
<div class="tbn-dot" id="dot-hlmention"></div>
</button>
</div>
<!-- Row 2: Highlight Colour picker -->
<div class="tbn-cpick" id="cpick-hlrow">
<div class="tbn-cp-l">
<div class="tbn-swatch" id="sw-hlrow" title="Pick colour"></div>
<input type="color" id="in-hlrow">
<div>
<span class="tbn-cp-name">Pick Highlight Colour</span>
<span class="tbn-cp-hex" id="hex-hlrow">#4ADE80</span>
</div>
</div>
<div class="tbn-presets">
<button class="tbn-pre" data-target="hlrow" data-color="#4ade80" style="background:#4ade80;"></button>
<button class="tbn-pre" data-target="hlrow" data-color="#60a5fa" style="background:#60a5fa;"></button>
<button class="tbn-pre" data-target="hlrow" data-color="#f87171" style="background:#f87171;"></button>
<button class="tbn-pre" data-target="hlrow" data-color="#fb923c" style="background:#fb923c;"></button>
<button class="tbn-pre" data-target="hlrow" data-color="#a78bfa" style="background:#a78bfa;"></button>
</div>
</div>
<!-- Row 3: Highlight mention colour picker -->
<div class="tbn-cpick" id="cpick-mention">
<div class="tbn-cp-l">
<div class="tbn-swatch" id="sw-mention" title="Pick colour"></div>
<input type="color" id="in-mention">
<div>
<span class="tbn-cp-name">Pick Mention Colour</span>
<span class="tbn-cp-hex" id="hex-mention">#FB923C</span>
</div>
</div>
<div class="tbn-presets">
<button class="tbn-pre" data-target="mention" data-color="#4ade80" style="background:#4ade80;"></button>
<button class="tbn-pre" data-target="mention" data-color="#60a5fa" style="background:#60a5fa;"></button>
<button class="tbn-pre" data-target="mention" data-color="#f87171" style="background:#f87171;"></button>
<button class="tbn-pre" data-target="mention" data-color="#fb923c" style="background:#fb923c;"></button>
<button class="tbn-pre" data-target="mention" data-color="#a78bfa" style="background:#a78bfa;"></button>
</div>
</div>
</div>
<!-- Audio Volume -->
<div class="tbn-fg">
<label class="tbn-label">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
Audio Preferences
</label>
<!-- 1. Default Sound button + popup board -->
<div style="position:relative;">
<div class="tbn-cpick tbn-sound-card" id="tbn-default-sound-btn" style="cursor:pointer;">
<div class="tbn-cp-l">
<div class="tbn-sound-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
</div>
<div>
<span class="tbn-cp-name">Default Sound</span>
<span class="tbn-cp-hex" id="tbn-default-sound-name">Standard Bell</span>
</div>
</div>
<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;flex-shrink:0;"><path d="M7 10l5 5 5-5z"/></svg>
</div>
<!-- Notification Board popup -->
<div id="tbn-nb-popup" style="display:none;">
<div id="tbn-nb-header">
<span id="tbn-nb-title">NOTIFICATION BOARD</span>
<button id="tbn-nb-close">✕</button>
</div>
<div id="tbn-nb-grid">
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-493469.mp3" data-name="Minimalistic"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Minimalistic</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-352705.mp3" data-name="Modern Chime"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Modern Chime</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-372475.mp3" data-name="Digital Alert"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Digital Alert</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-398649.mp3" data-name="Soft Pulse"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Soft Pulse</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-352755.mp3" data-name="Standard Bell"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Standard Bell</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494238.mp3" data-name="Echo Ring"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Echo Ring</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494256.mp3" data-name="Crystal Clear"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Crystal Clear</button>
<button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494544.mp3" data-name="Classic Pop"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Classic Pop</button>
</div>
</div>
</div>
<!-- 2. Custom Sound -->
<div class="tbn-cpick tbn-sound-card" id="tbn-sound-card">
<div class="tbn-cp-l">
<div class="tbn-sound-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
</div>
<div>
<span class="tbn-cp-name">Custom Sound</span>
<span class="tbn-cp-hex" id="tbn-sound-name">No file chosen</span>
</div>
</div>
<div style="display:flex;gap:7px;align-items:center;">
<button class="tbn-sound-btn" id="tbn-sound-pick" style="outline:none;">
Choose File
<input type="file" id="tbn-sound-file" accept=".mp3,audio/mp3,audio/mpeg" style="display:none;">
</button>
<button class="tbn-sound-remove" id="tbn-sound-remove" style="display:none;">✕</button>
</div>
</div>
<div id="tbn-vol-card">
<div id="tbn-vol-row">
<span id="tbn-vol-ico" class="tbn-vol-svg"></span>
<div id="tbn-vol-wrap">
<div id="tbn-vol-fill"></div>
<input type="range" id="tbn-vol-slider" min="0" max="1" step="0.001">
<div id="tbn-vol-thumb"></div>
</div>
<span id="tbn-vol-pct">65%</span>
</div>
<button id="tbn-test-sound">
Test Sound
</button>
</div>
</div>
<!-- Additional Keywords -->
<div class="tbn-fg">
<div class="tbn-kw-hdr">
<label class="tbn-label" style="margin:0;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l-5.5 9h11L12 2zm0 3.84L13.93 9h-3.87L12 5.84zM17.5 13c-2.49 0-4.5 2.01-4.5 4.5S15.01 22 17.5 22s4.5-2.01 4.5-4.5S19.99 13 17.5 13zm0 7c-1.38 0-2.5-1.12-2.5-2.5S16.12 15 17.5 15s2.5 1.12 2.5 2.5S18.88 20 17.5 20zM3 21.5h8v-8H3v8zm2-6h4v4H5v-4z"/></svg>
Additional Keywords (One per line)
</label>
<button id="tbn-kw-reset">Reset</button>
</div>
<textarea id="tbn-kw" placeholder="Add keywords to be notified about..." rows="4" spellcheck="false"></textarea>
</div>
<!-- Extras -->
<div class="tbn-fg">
<label class="tbn-label">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l7.59-7.59L21 8l-9 9z"/></svg>
Extras
</label>
<!-- 1. Popup mention toggle -->
<button class="tbn-tc" id="tog-popup">
<div class="tbn-tc-l">
<span class="tbn-tc-ico" id="ico-popup">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h2v3l4-3h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="10" r="2.4"/><path d="M14.4 10v1a2 2 0 0 0 4 0v-1a6.4 6.4 0 1 0-2.55 5.1"/></svg>
</span>
<span class="tbn-tc-txt">Popup mention</span>
</div>
<div class="tbn-dot" id="dot-popup"></div>
</button>
<!-- 2. Popup keywords toggle -->
<button class="tbn-tc" id="tog-popup-kw">
<div class="tbn-tc-l">
<span class="tbn-tc-ico" id="ico-popup-kw">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h7l1 2.5 1-2.5h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/><circle cx="8.5" cy="10" r="2"/><path d="M10.5 10h5M13.5 9.2v1.6M15 9.2v1.6"/></svg>
</span>
<span class="tbn-tc-txt">Popup keywords</span>
</div>
<div class="tbn-dot" id="dot-popup-kw"></div>
</button>
<!-- Dark / Light mode -->
<div id="tbn-mode-grid">
<button class="tbn-mode-btn" id="btn-dark">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/></svg>
Dark Mode
</button>
<button class="tbn-mode-btn" id="btn-light">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/></svg>
Light Mode
</button>
</div>
</div>
</div>
<div id="tbn-footer">
𝗗𝗲𝘃𝗲𝗹𝗼𝗽𝗲𝗱 𝗯𝘆
<strong>
<a href="https://aonexyz.vercel.app" target="_blank" style="text-decoration: none; color: inherit;">
𝟰𝗡𝟭𝗞
</a>
</strong>
</div>`;
document.body.appendChild(modal);
return modal;
}
/* ═══════════════════════════════════════════════════════
WIRE MODAL
═══════════════════════════════════════════════════════ */
function wireModal(modal) {
const Q = s => modal.querySelector(s);
const backdrop = Q('#tbn-backdrop');
const closeBtn = Q('#tbn-close');
const unameIn = Q('#tbn-uname');
const unameDot = Q('#tbn-uname-dot');
// Highlight Color (row border)
const togHlRow = Q('#tog-hlrow');
const dotHlRow = Q('#dot-hlrow');
const icoHlRow = Q('#ico-hlrow');
const swHlRow = Q('#sw-hlrow');
const inHlRow = Q('#in-hlrow');
const hexHlRow = Q('#hex-hlrow');
// Highlight mention (text)
const togHlMention = Q('#tog-hlmention');
const dotHlMention = Q('#dot-hlmention');
const icoHlMention = Q('#ico-hlmention');
const swMention = Q('#sw-mention');
const inMention = Q('#in-mention');
const hexMention = Q('#hex-mention');
const kwTA = Q('#tbn-kw');
const kwReset = Q('#tbn-kw-reset');
const volSlider = Q('#tbn-vol-slider');
const volFill = Q('#tbn-vol-fill');
const volThumb = Q('#tbn-vol-thumb');
const volPct = Q('#tbn-vol-pct');
const volIco = Q('#tbn-vol-ico');
const testSound = Q('#tbn-test-sound');
// SVG paths for each volume level
const VOL_ICONS = {
mute: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="20" height="20">
<path d="M11 5L6 9H2v6h4l5 4V5z"/>
<line x1="23" y1="9" x2="17" y2="15"/>
<line x1="17" y1="9" x2="23" y2="15"/>
</svg>`,
high: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="20" height="20">
<path d="M11 5L6 9H2v6h4l5 4V5z"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
</svg>`,
};
function getVolIcon(pct) {
return pct === 0 ? VOL_ICONS.mute : VOL_ICONS.high;
}
function setHlColor(c) {
Cfg.save('hlColor', c);
swHlRow.style.background = c;
swHlRow.style.boxShadow = `0 0 12px ${c}55`;
inHlRow.value = c;
hexHlRow.textContent = c.toUpperCase();
icoHlRow.style.color = Cfg.hlRowEnabled ? c : '#6b7280';
}
function setMentionColor(c) {
Cfg.save('mentionColor', c);
swMention.style.background = c;
swMention.style.boxShadow = `0 0 12px ${c}55`;
inMention.value = c;
hexMention.textContent = c.toUpperCase();
icoHlMention.style.color = Cfg.hlMentionOn ? c : '#6b7280';
}
/* ── toggle sync ── */
function syncHlRow() {
const on = Cfg.hlRowEnabled;
togHlRow.classList.toggle('tbn-tc-off', !on);
dotHlRow.style.background = on ? Cfg.hlColor : 'transparent';
dotHlRow.style.borderColor = on ? Cfg.hlColor : '#374151';
icoHlRow.style.color = on ? Cfg.hlColor : '#6b7280';
}
function syncHlMention() {
const on = Cfg.hlMentionOn;
togHlMention.classList.toggle('tbn-tc-off', !on);
dotHlMention.style.background = on ? Cfg.mentionColor : 'transparent';
dotHlMention.style.borderColor = on ? Cfg.mentionColor : '#374151';
icoHlMention.style.color = on ? Cfg.mentionColor : '#6b7280';
}
function setVol(v) {
// v is 0.0–1.0 (matches React's volume state)
Cfg.save('volume', v);
volSlider.value = v;
const pct = Math.round(v * 100);
volFill.style.width = pct + '%';
volPct.textContent = pct + '%';
// Move visual capsule thumb — left: calc(v*100% - 14px) for 28px wide thumb
volThumb.style.left = `calc(${pct}% - 14px)`;
// Dynamic volume SVG icon
volIco.innerHTML = getVolIcon(pct);
}
/* ── events ── */
backdrop.addEventListener('click', closeModal);
closeBtn.addEventListener('click', closeModal);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
unameIn.addEventListener('input', () => {
Cfg.save('username', unameIn.value.trim());
unameDot.className = Cfg.username ? 'tbn-dot-g' : 'tbn-dot-gr';
});
togHlRow.addEventListener('click', () => {
Cfg.hlRowEnabled = !Cfg.hlRowEnabled;
Store.set('v1_hlRow', Cfg.hlRowEnabled);
syncHlRow();
});
togHlMention.addEventListener('click', () => {
Cfg.hlMentionOn = !Cfg.hlMentionOn;
Store.set('v1_hlMention', Cfg.hlMentionOn);
syncHlMention();
});
swHlRow.addEventListener('click', () => inHlRow.click());
inHlRow.addEventListener('input', () => { setHlColor(inHlRow.value); syncHlRow(); });
swMention.addEventListener('click', () => inMention.click());
inMention.addEventListener('input', () => { setMentionColor(inMention.value); syncHlMention(); });
modal.querySelectorAll('.tbn-pre').forEach(btn => {
btn.addEventListener('click', () => {
const c = btn.dataset.color;
if (btn.dataset.target === 'hlrow') { setHlColor(c); syncHlRow(); }
else { setMentionColor(c); syncHlMention(); }
});
// hover → live preview on swatch + hex + toggle dot + icon
btn.addEventListener('mouseenter', () => {
const c = btn.dataset.color;
if (btn.dataset.target === 'hlrow') {
swHlRow.style.background = c;
swHlRow.style.boxShadow = `0 0 12px ${c}55`;
hexHlRow.textContent = c.toUpperCase();
dotHlRow.style.background = c;
dotHlRow.style.borderColor = c;
icoHlRow.style.color = c;
} else {
swMention.style.background = c;
swMention.style.boxShadow = `0 0 12px ${c}55`;
hexMention.textContent = c.toUpperCase();
dotHlMention.style.background = c;
dotHlMention.style.borderColor = c;
icoHlMention.style.color = c;
}
});
// mouse leave → restore saved colour
btn.addEventListener('mouseleave', () => {
if (btn.dataset.target === 'hlrow') { setHlColor(Cfg.hlColor); syncHlRow(); }
else { setMentionColor(Cfg.mentionColor); syncHlMention(); }
});
});
kwTA.addEventListener('input', () => {
Cfg.save('keywords', kwTA.value.split('\n').map(k => k.trim()).filter(Boolean));
});
kwReset.addEventListener('click', () => { kwTA.value = ''; Cfg.save('keywords', []); });
volSlider.addEventListener('input', () => setVol(parseFloat(volSlider.value)));
testSound.addEventListener('click', () => { ripple(testSound); Sound.play(); });
// ── Extras ──
const togPopup = Q('#tog-popup');
const dotPopup = Q('#dot-popup');
const icoPopup = Q('#ico-popup');
const togPopupKw = Q('#tog-popup-kw');
const dotPopupKw = Q('#dot-popup-kw');
const icoPopupKw = Q('#ico-popup-kw');
const soundPick = Q('#tbn-sound-pick');
const soundFile = Q('#tbn-sound-file');
const soundName = Q('#tbn-sound-name');
const btnDark = Q('#btn-dark');
const btnLight = Q('#btn-light');
function syncPopup() {
const on = Cfg.popupMention;
togPopup.classList.toggle('tbn-tc-off', !on);
dotPopup.style.background = on ? '#60a5fa' : 'transparent';
dotPopup.style.borderColor = on ? '#60a5fa' : '#374151';
icoPopup.style.color = on ? '#60a5fa' : '#6b7280';
}
function syncPopupKw() {
const on = Cfg.popupKeywords;
togPopupKw.classList.toggle('tbn-tc-off', !on);
dotPopupKw.style.background = on ? '#60a5fa' : 'transparent';
dotPopupKw.style.borderColor = on ? '#60a5fa' : '#374151';
icoPopupKw.style.color = on ? '#60a5fa' : '#6b7280';
}
function syncMode() {
const dark = Cfg.darkMode;
btnDark.classList.toggle('tbn-mode-active', dark);
btnLight.classList.toggle('tbn-mode-active', !dark);
applyTheme(dark);
}
togPopup.addEventListener('click', () => {
Cfg.save('popupMention', !Cfg.popupMention);
syncPopup();
});
togPopupKw.addEventListener('click', () => {
Cfg.save('popupKeywords', !Cfg.popupKeywords);
syncPopupKw();
});
soundPick.addEventListener('click', () => { soundFile.click(); soundPick.blur(); });
soundFile.addEventListener('change', () => {
const file = soundFile.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
// save base64 data-URL directly to GM storage → survives page reload
Cfg.customSoundB64 = e.target.result;
Store.set('v1_customSoundB64', e.target.result);
// save filename so it persists after page reload
Cfg.customSoundName = file.name;
Store.set('v1_customSoundName', file.name);
soundName.textContent = file.name;
soundRemove.style.display = 'flex';
soundPick.style.background = 'rgba(96,165,250,.28)';
soundPick.style.borderColor = 'rgba(96,165,250,.5)';
Sound.reload();
Sound.play();
syncNBButtons();
};
reader.readAsDataURL(file);
soundFile.value = '';
soundPick.blur();
});
const soundRemove = Q('#tbn-sound-remove');
soundRemove.style.display = Cfg.customSoundB64 ? 'flex' : 'none';
if (Cfg.customSoundB64) {
soundPick.style.background = 'rgba(96,165,250,.28)';
soundPick.style.borderColor = 'rgba(96,165,250,.5)';
soundName.textContent = '(Custom sound loaded)';
}
soundRemove.addEventListener('click', () => {
Cfg.customSoundB64 = '';
Store.set('v1_customSoundB64', '');
Cfg.customSoundName = '';
Store.set('v1_customSoundName', '');
soundName.textContent = 'No file chosen';
soundFile.value = '';
soundRemove.style.display = 'none';
soundPick.style.background = '';
soundPick.style.borderColor = '';
Sound.reload();
syncNBButtons();
});
// ── Notification Board popup ──
const nbBtn = Q('#tbn-default-sound-btn');
const nbPopup = Q('#tbn-nb-popup');
const nbClose = Q('#tbn-nb-close');
const defaultSoundName = Q('#tbn-default-sound-name');
function closeNB() {
nbPopup.classList.add('tbn-nb-closing');
setTimeout(() => {
nbPopup.style.display = 'none';
nbPopup.classList.remove('tbn-nb-closing');
}, 220);
}
nbBtn.addEventListener('click', () => {
if (nbPopup.style.display === 'none') {
nbPopup.classList.remove('tbn-nb-closing');
nbPopup.style.display = 'block';
} else {
closeNB();
}
});
nbClose.addEventListener('click', closeNB);
function syncNBButtons() {
Q('#tbn-nb-grid').querySelectorAll('.tbn-nb-btn').forEach(btn => {
const isActive = btn.dataset.url === Cfg.defaultSoundUrl && !Cfg.customSoundB64;
btn.classList.toggle('tbn-nb-active', isActive);
btn.querySelector('svg').style.color = isActive ? '#60a5fa' : '';
// Re-apply inline styles immediately so switching sounds updates colors right away
if (Cfg.darkMode) {
btn.style.background = isActive ? 'rgba(96,165,250,.15)' : 'rgba(255,255,255,.04)';
btn.style.borderColor = isActive ? 'rgba(30,58,138,.75)' : 'rgba(255,255,255,.08)';
btn.style.color = isActive ? '#60a5fa' : '#d1d5db';
} else {
btn.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(255,255,255,.8)';
btn.style.borderColor = isActive ? 'rgba(96,165,250,.5)' : 'rgba(30,58,138,.4)';
btn.style.color = isActive ? '#1d4ed8' : '#111827';
}
});
const activeBtn = Q('#tbn-nb-grid .tbn-nb-active');
defaultSoundName.textContent = activeBtn ? activeBtn.dataset.name : 'Minimalistic';
}
Q('#tbn-nb-grid').querySelectorAll('.tbn-nb-btn').forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.dataset.url;
// Save new default URL and clear old cached base64 so it gets re-fetched
Cfg.defaultSoundUrl = url;
Store.set('v1_defaultSound', url);
Cfg.defaultSoundB64 = '';
Store.set('v1_defaultSoundB64', '');
// Clear custom sound
Cfg.customSoundB64 = '';
Store.set('v1_customSoundB64', '');
soundFile.value = '';
soundRemove.style.display = 'none';
soundPick.style.background = '';
soundPick.style.borderColor = '';
soundName.textContent = 'No file chosen';
Sound.previewUrl(url);
syncNBButtons();
closeNB();
});
});
function syncPresets() { syncNBButtons(); }
btnDark.addEventListener('click', () => { Cfg.save('darkMode', true); syncMode(); });
btnLight.addEventListener('click', () => { Cfg.save('darkMode', false); syncMode(); });
function syncAll() {
const det = detectUser();
if (det && !Cfg.username) Cfg.save('username', det);
unameIn.value = Cfg.username;
unameDot.className = Cfg.username ? 'tbn-dot-g' : 'tbn-dot-gr';
setHlColor(Cfg.hlColor);
setMentionColor(Cfg.mentionColor);
syncHlRow();
syncHlMention();
setVol(Cfg.volume);
kwTA.value = Cfg.keywords.join('\n');
syncPopup();
syncPopupKw();
syncMode();
soundName.textContent = Cfg.customSoundB64
? (Cfg.customSoundName || '(Custom sound loaded)')
: 'No file chosen';
if (soundRemove) soundRemove.style.display = Cfg.customSoundB64 ? 'flex' : 'none';
if (Cfg.customSoundB64) {
soundPick.style.background = 'rgba(96,165,250,.28)';
soundPick.style.borderColor = 'rgba(96,165,250,.5)';
} else {
soundPick.style.background = '';
soundPick.style.borderColor = '';
}
syncNBButtons();
}
return { syncAll };
}
/* ── Dark/Light theme apply ── */
function applyTheme(dark) {
const panel = document.getElementById('tbn-panel');
if (!panel) return;
if (dark) {
// ── Dark mode — blue-tinted dark bg ──
panel.style.background = 'rgba(10,14,30,.92)';
panel.style.border = '2px solid rgba(96,165,250,.12)';
panel.style.color = '#e5e7eb';
panel.querySelectorAll('.tbn-label').forEach(el => el.style.color = '#6b7280');
panel.querySelectorAll('.tbn-tc-txt').forEach(el => el.style.color = '#d1d5db');
panel.querySelectorAll('.tbn-cp-name').forEach(el => el.style.color = '#e5e7eb');
panel.querySelectorAll('.tbn-cp-hex').forEach(el => el.style.color = '#6b7280');
panel.querySelectorAll('#tbn-hdr h2').forEach(el => el.style.color = '#fff');
panel.querySelectorAll('#tbn-hdr p').forEach(el => el.style.color = '#6b7280');
panel.querySelectorAll('#tbn-vol-pct').forEach(el => el.style.color = '#9ca3af');
panel.querySelectorAll('#tbn-test-sound').forEach(el => { el.style.color = '#d1d5db'; el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.07)'; });
panel.querySelectorAll('.tbn-tc').forEach(el => { el.style.background = 'rgba(96,165,250,.06)'; el.style.borderColor = 'rgba(96,165,250,.18)'; });
panel.querySelectorAll('.tbn-cpick').forEach(el => { el.style.background = 'rgba(96,165,250,.04)'; el.style.borderColor = 'rgba(96,165,250,.12)'; });
panel.querySelectorAll('.tbn-mode-btn').forEach(el => {
const isActive = el.classList.contains('tbn-mode-active');
el.style.color = isActive ? '#60a5fa' : '#6b7280';
el.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(255,255,255,.05)';
el.style.borderColor = isActive ? 'rgba(30,58,138,.75)' : 'rgba(255,255,255,.1)';
});
panel.querySelectorAll('#tbn-uname').forEach(el => { el.style.color = '#e5e7eb'; el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.1)'; });
panel.querySelectorAll('#tbn-kw').forEach(el => { el.style.color = '#e5e7eb'; el.style.background = 'rgba(255,255,255,.04)'; el.style.borderColor = 'rgba(255,255,255,.1)'; });
panel.querySelectorAll('#tbn-hdr').forEach(el => el.style.borderBottomColor = 'rgba(255,255,255,.05)');
panel.querySelectorAll('#tbn-footer').forEach(el => { el.style.color = '#4b5563'; el.style.borderTopColor = 'rgba(255,255,255,.05)'; });
panel.querySelectorAll('#tbn-vol-card').forEach(el => { el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.08)'; });
panel.querySelectorAll('#tbn-kw-reset').forEach(el => { el.style.background = '#b91c1c'; el.style.color = '#fff'; });
panel.querySelectorAll('.tbn-tc-ico').forEach(el => el.style.opacity = '1');
// Notification Board popup — dark mode: restore dark bg
panel.querySelectorAll('#tbn-nb-popup').forEach(el => {
el.style.background = 'rgba(12,17,35,.96)';
el.style.borderColor = 'rgba(30,58,138,.4)';
el.style.boxShadow = '0 20px 60px rgba(0,0,0,.6)';
});
panel.querySelectorAll('#tbn-nb-title').forEach(el => el.style.color = '#6b7280');
panel.querySelectorAll('#tbn-nb-close').forEach(el => { el.style.color = '#6b7280'; });
panel.querySelectorAll('.tbn-nb-btn:not(.tbn-nb-active)').forEach(el => {
el.style.background = 'rgba(255,255,255,.04)';
el.style.borderColor = 'rgba(255,255,255,.08)';
el.style.color = '#d1d5db';
});
panel.querySelectorAll('.tbn-nb-btn.tbn-nb-active').forEach(el => {
el.style.background = 'rgba(96,165,250,.15)';
el.style.borderColor = 'rgba(30,58,138,.75)';
el.style.color = '#60a5fa';
});
} else {
// ── Light mode — clean white, all borders visible ──
panel.style.background = 'rgba(235,242,255,.98)';
panel.style.border = '1px solid rgba(30,58,138,.55)';
panel.style.color = '#111827';
panel.querySelectorAll('.tbn-label').forEach(el => el.style.color = '#374151');
panel.querySelectorAll('.tbn-tc-txt').forEach(el => el.style.color = '#111827');
panel.querySelectorAll('.tbn-cp-name').forEach(el => el.style.color = '#111827');
panel.querySelectorAll('.tbn-cp-hex').forEach(el => el.style.color = '#374151');
panel.querySelectorAll('#tbn-hdr h2').forEach(el => el.style.color = '#111827');
panel.querySelectorAll('#tbn-hdr p').forEach(el => el.style.color = '#374151');
panel.querySelectorAll('#tbn-vol-pct').forEach(el => el.style.color = '#374151');
panel.querySelectorAll('#tbn-test-sound').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
panel.querySelectorAll('.tbn-tc').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
panel.querySelectorAll('.tbn-cpick').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.8)'; });
panel.querySelectorAll('.tbn-mode-btn').forEach(el => { const isActive = el.classList.contains('tbn-mode-active'); el.style.color = isActive ? '#1d4ed8' : '#374151'; el.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(235,242,255,.9)'; el.style.borderColor = isActive ? 'rgba(30,58,138,.95)' : 'rgba(30,58,138,.85)'; });
panel.querySelectorAll('#tbn-uname').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
panel.querySelectorAll('#tbn-kw').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
panel.querySelectorAll('#tbn-hdr').forEach(el => el.style.borderBottomColor = 'rgba(30,58,138,.5)');
panel.querySelectorAll('#tbn-footer').forEach(el => { el.style.color = '#374151'; el.style.borderTopColor = 'rgba(30,58,138,.5)'; });
panel.querySelectorAll('#tbn-vol-card').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.8)'; });
panel.querySelectorAll('#tbn-kw-reset').forEach(el => { el.style.background = '#dc2626'; el.style.color = '#fff'; });
panel.querySelectorAll('.tbn-tc-ico').forEach(el => el.style.opacity = '1');
// Notification Board popup — light mode: white+blueish
panel.querySelectorAll('#tbn-nb-popup').forEach(el => {
el.style.background = 'rgba(235,242,255,.99)';
el.style.borderColor = 'rgba(30,58,138,.85)';
el.style.boxShadow = '0 20px 60px rgba(96,165,250,.15)';
});
panel.querySelectorAll('#tbn-nb-title').forEach(el => el.style.color = '#374151');
panel.querySelectorAll('#tbn-nb-close').forEach(el => { el.style.color = '#374151'; });
panel.querySelectorAll('.tbn-nb-btn:not(.tbn-nb-active)').forEach(el => {
el.style.background = 'rgba(255,255,255,.8)';
el.style.borderColor = 'rgba(30,58,138,.75)';
el.style.color = '#111827';
});
panel.querySelectorAll('.tbn-nb-btn.tbn-nb-active').forEach(el => {
el.style.background = 'rgba(96,165,250,.18)';
el.style.borderColor = 'rgba(30,58,138,.9)';
el.style.color = '#1d4ed8';
});
}
}
function ripple(el) {
const r = document.createElement('span');
r.className = 'tbn-ripple';
el.appendChild(r);
setTimeout(() => r.remove(), 550);
}
let _syncAll = null;
function openModal() {
const m = document.getElementById('tbn-modal');
m.style.display = 'flex';
requestAnimationFrame(() => requestAnimationFrame(() => m.classList.add('tbn-modal-in')));
_syncAll?.();
}
function closeModal() {
const m = document.getElementById('tbn-modal');
if (!m) return;
m.classList.remove('tbn-modal-in');
m.classList.add('tbn-modal-out');
setTimeout(() => { m.style.display = 'none'; m.classList.remove('tbn-modal-out'); }, 300);
}
function addTrigger() {
const btn = document.createElement('button');
btn.id = 'tbn-trigger';
btn.title = 'Shoutbox Notifier Settings';
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>`;
btn.addEventListener('click', e => { e.stopPropagation(); openModal(); });
const anchor = document.querySelector('#shoutbox-container .content-title h6.left');
if (anchor) {
anchor.style.cssText += 'display:flex;align-items:center;gap:6px;';
anchor.appendChild(btn);
}
}
/* ═══════════════════════════════════════════════════════
OBSERVER
═══════════════════════════════════════════════════════ */
function startObserver() {
const box = document.getElementById('shouts-container');
if (!box) { setTimeout(startObserver, 500); return; }
// Old shouts — apply highlight only, do not notify
box.querySelectorAll('.shout-item').forEach(el => inspect(el, true));
new MutationObserver(muts => {
for (const m of muts)
m.addedNodes.forEach(n => {
if (n.nodeType === 1 && n.classList.contains('shout-item')) inspect(n);
});
}).observe(box, { childList: true });
}
/* ═══════════════════════════════════════════════════════
STYLES
═══════════════════════════════════════════════════════ */
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
/* ── username text mark (Highlight mention) ── */
.tbn-mark {
border-radius: 3px !important;
padding: 0 2px !important;
font-weight: 700 !important;
font-style: normal !important;
transition: color .3s, background .3s !important;
}
/* ── keyword text mark ── */
.tbn-kw-mark {
background: transparent !important;
color: rgba(255, 255, 255, 0.9) !important;
font-weight: 600 !important;
font-style: normal !important;
text-decoration: underline !important;
text-decoration-color: rgba(255, 255, 255, 0.45) !important;
text-underline-offset: 2px !important;
border-radius: 0 !important;
padding: 0 !important;
}
/* ── row highlight (Highlight Color) ── */
.tbn-hl-row {
border-left: 3px solid var(--tbn-c, #4ade80) !important;
background: linear-gradient(90deg, color-mix(in srgb, var(--tbn-c,#4ade80) 10%, transparent) 0%, transparent 70%) !important;
padding-left: 10px !important;
border-radius: 4px !important;
transition: background .4s ease, border-color .4s ease !important;
}
/* ── flash ── */
@keyframes tbn-flash-kf { 0%{opacity:1}35%{opacity:.35}70%{opacity:1}100%{opacity:1} }
.tbn-flash { animation: tbn-flash-kf .55s ease; }
/* ── toast container ── */
#tbn-toasts {
position:fixed; bottom:24px; right:24px; z-index:2147483647;
display:flex; flex-direction:column; gap:12px; pointer-events:none;
}
/* ── toast ── */
.tbn-toast {
pointer-events:auto;
display:flex; align-items:center; gap:16px;
padding:14px 16px; min-width:270px; max-width:340px;
background:rgba(17,24,39,.94);
backdrop-filter:blur(20px) saturate(1.3);
border:1px solid rgba(255,255,255,.1); border-radius:20px;
box-shadow:0 20px 60px rgba(0,0,0,.55);
font-family:'Inter',-apple-system,sans-serif;
opacity:0; transform:translateX(50px) scale(.9);
transition:opacity .32s ease, transform .38s cubic-bezier(.34,1.4,.64,1);
}
.tbn-toast.tbn-t-in { opacity:1; transform:translateX(0) scale(1); }
.tbn-toast.tbn-t-out { opacity:0; transform:translateX(40px) scale(.9); transition:opacity .3s ease, transform .28s ease; }
.tbn-t-icon { width:38px;height:38px;border-radius:12px;flex-shrink:0;display:flex;align-items:center;justify-content:center; }
.tbn-t-icon svg { width:19px;height:19px;display:block; }
.tbn-toast-mention .tbn-t-icon { background:rgba(251,146,60,.12); color:#fb923c; }
.tbn-toast-keyword .tbn-t-icon { background:rgba(96,165,250,.12); color:#60a5fa; }
.tbn-t-body { flex:1; }
.tbn-t-title { font-size:13.5px;font-weight:700;color:#fff;letter-spacing:-.01em; }
.tbn-t-msg { font-size:11.5px;color:#6b7280;margin-top:2px; }
.tbn-t-close { background:none;border:none;cursor:pointer;color:#374151;padding:4px;border-radius:8px;display:flex;align-items:center;transition:color .15s,background .15s; }
.tbn-t-close:hover { color:#9ca3af;background:rgba(255,255,255,.08); }
.tbn-t-close svg { width:15px;height:15px;display:block; }
/* ── trigger ── */
#tbn-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 10px;
cursor: pointer;
color: #6b7280;
transition: color .18s, background .22s, border-color .22s, transform .8s cubic-bezier(.34,1.56,.64,1);
}
#tbn-trigger:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.08);
transform: rotate(69deg);
}
#tbn-trigger svg {
width: 20px;
height: 20px;
display: block;
}
/* ── modal overlay ── */
#tbn-modal {
position:fixed;inset:0;z-index:2147483646;
display:flex;align-items:center;justify-content:center;padding:16px;
font-family:'Inter',-apple-system,sans-serif;
-webkit-font-smoothing:antialiased;
}
#tbn-backdrop {
position:absolute;inset:0;
background:rgba(0,0,0,.76);backdrop-filter:blur(6px);
opacity:0;transition:opacity .28s ease;
}
#tbn-modal.tbn-modal-in #tbn-backdrop { opacity:1; }
#tbn-modal.tbn-modal-out #tbn-backdrop { opacity:0; }
/* ── panel ── */
#tbn-panel {
position:relative;z-index:1;
width:100%;max-width:520px;max-height:578px;overflow-y:auto;
background:rgba(15,18,28,.88);
backdrop-filter:blur(20px) saturate(1.3);
border:1px solid rgba(255,255,255,.07);border-radius:28px;
box-shadow:0 32px 80px rgba(0,0,0,.7), 0 1px 0 rgba(255,255,255,.05) inset;
scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.1) transparent;
color:#e5e7eb;
opacity:0;transform:translateY(22px) scale(.97);
transition:opacity .3s ease, transform .36s cubic-bezier(.22,1,.36,1);
}
#tbn-panel::-webkit-scrollbar { width:6px; }
#tbn-panel::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1);border-radius:999px; }
#tbn-modal.tbn-modal-in #tbn-panel { opacity:1;transform:translateY(0) scale(1); }
#tbn-modal.tbn-modal-out #tbn-panel { opacity:0;transform:translateY(10px) scale(.97);transition:opacity .25s ease,transform .25s ease; }
/* header */
#tbn-hdr { display:flex;align-items:center;justify-content:space-between;padding:22px 22px 18px;border-bottom:1px solid rgba(255,255,255,.05); }
#tbn-hdr-l { display:flex;align-items:center;gap:13px; }
#tbn-hdr-icon {
width:42px;height:42px;border-radius:13px;flex-shrink:0;
background:transparent;border:none;
display:flex;align-items:center;justify-content:center;color:#60a5fa;
cursor:default;
}
#tbn-hdr-icon:hover {
transform:none;
box-shadow:none;
}
@keyframes tbn-spin-slow { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
#tbn-hdr-icon svg {
width:45px;height:45px;display:block;
animation:tbn-spin-slow 10s linear infinite;
animation-play-state:running;
transition:animation-play-state 0s;
}
#tbn-hdr-icon:hover svg {
animation-play-state:paused;
}
#tbn-hdr h2 { margin:0;font-size:17px;font-weight:700;color:#fff;letter-spacing:-.025em; }
#tbn-hdr p { margin:3px 0 0;font-size:12px;color:#6b7280; }
#tbn-close {
width:30px;height:30px;border-radius:50%;background:rgba(255,255,255,.07);border:none;
cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;
transition:background .15s,color .15s;flex-shrink:0;
}
#tbn-close:hover { background:rgba(255,255,255,.13);color:#e5e7eb; }
#tbn-close svg { width:15px;height:15px;display:block; }
/* body */
#tbn-body { padding:22px;display:flex;flex-direction:column;gap:22px; }
/* field group */
.tbn-fg { display:flex;flex-direction:column;gap:10px; }
.tbn-label { display:flex;align-items:center;gap:7px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em; }
.tbn-label svg { width:13px;height:13px;display:block; }
/* username input */
.tbn-iw {
position:relative;
transition:transform .2s ease, box-shadow .2s ease;
}
.tbn-iw:hover {
transform:translateY(-2px);
box-shadow:0 6px 20px rgba(0,0,0,.3);
border-radius:13px;
}
#tbn-uname {
width:100%;padding:11px 38px 11px 15px;
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:13px;
color:#e5e7eb;font-size:14px;font-family:'JetBrains Mono',monospace;
outline:none;box-sizing:border-box;
transition:border-color .18s,box-shadow .18s;
}
#tbn-uname:focus { border-color:rgba(96,165,250,.5);box-shadow:0 0 0 3px rgba(96,165,250,.1); }
#tbn-uname::placeholder { color:#374151; }
#tbn-uname-dot { position:absolute;right:13px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%; }
.tbn-dot-g { background:#4ade80;box-shadow:0 0 8px #4ade8099; }
.tbn-dot-gr { background:#374151; }
/* theme grid */
#tbn-theme-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px; }
/* toggle card — same bg always, only border/icon/dot change on ON/OFF */
.tbn-tc {
display:flex;align-items:center;justify-content:space-between;
padding:14px 14px;
background:rgba(255,255,255,.05);
border:1px solid rgba(255,255,255,.1);
border-radius:16px;
cursor:pointer;font-family:'Inter',sans-serif;
transition:border-color .22s, opacity .18s, transform .2s ease, box-shadow .2s ease;
text-align:left;
}
.tbn-tc:hover {
transform:translateY(-2px);
box-shadow:0 6px 20px rgba(0,0,0,.3);
}
.tbn-tc:focus,
.tbn-tc:active {
outline:none;
box-shadow:none;
background:rgba(255,255,255,.05) !important;
}
.tbn-tc:focus-visible {
outline:none;
}
/* ON state — slightly brighter border, no colour fill */
.tbn-tc:not(.tbn-tc-off) {
border-color:rgba(255,255,255,.2);
opacity:1;
background:rgba(255,255,255,.05);
}
.tbn-tc-l { display:flex;align-items:center;gap:9px; }
.tbn-tc-ico { width:17px;height:17px;display:flex;align-items:center;justify-content:center;transition:color .2s;flex-shrink:0; }
.tbn-tc-ico svg { width:17px;height:17px;display:block; }
.tbn-tc-txt { font-size:13px;font-weight:600;color:#d1d5db; }
.tbn-dot {
width:14px;height:14px;border-radius:50%;flex-shrink:0;
border:2px solid #374151;
transition:background .2s,border-color .2s,box-shadow .2s;
}
/* colour picker card */
.tbn-cpick {
display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:14px 15px;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:16px;
transition:transform .2s ease, box-shadow .2s ease;
position:relative;
}
.tbn-cpick:hover {
transform:translateY(-2px);
box-shadow:0 6px 20px rgba(0,0,0,.3);
}
.tbn-cp-l { display:flex;align-items:center;gap:11px;flex:1;min-width:0; }
.tbn-swatch {
width:48px;height:48px;border-radius:14px;flex-shrink:0;
border:2px solid rgba(255,255,255,.1);cursor:pointer;
transition:transform .2s ease,box-shadow .2s ease;
}
.tbn-swatch:hover { transform:translateY(-2px) scale(1.05); box-shadow:0 6px 18px rgba(0,0,0,.3); }
/* colour picker popup — appears on the left side of the screen */
input[type=color] {
position:fixed;
left:0;
top:50%;
width:1px; height:1px;
opacity:0; border:none; padding:0; cursor:pointer;
z-index:2147483647;
}
.tbn-cp-name { display:block;font-size:13.5px;font-weight:700;color:#e5e7eb; }
.tbn-cp-hex { display:block;font-size:11px;color:#6b7280;font-family:'JetBrains Mono',monospace;margin-top:2px; }
.tbn-presets { display:flex;gap:7px;flex-shrink:0;align-items:center; }
.tbn-pre {
width:24px;height:24px;border-radius:50%;
border:2px solid rgba(255,255,255,.2);cursor:pointer;
transition:transform .2s ease, box-shadow .2s ease;
}
.tbn-pre:hover { transform:translateY(-2px) scale(1.25); box-shadow:0 4px 12px rgba(0,0,0,.3); }
/* keywords */
.tbn-kw-hdr { display:flex;align-items:center;justify-content:space-between; }
#tbn-kw-reset {
font-size:12px;font-weight:700;color:#fff;
background:#b91c1c;border:none;border-radius:8px;
padding:5px 12px;cursor:pointer;font-family:'Inter',sans-serif;
transition:background .15s, transform .2s ease, box-shadow .2s ease;
}
#tbn-kw-reset:hover { background:#dc2626; transform:translateY(-2px); box-shadow:0 4px 14px rgba(185,28,28,.4); }
#tbn-kw {
width:100%;resize:vertical;min-height:96px;
padding:13px 15px;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:13px;
color:#e5e7eb;font-size:13px;font-family:'JetBrains Mono',monospace;
outline:none;box-sizing:border-box;line-height:1.65;
transition:border-color .18s, transform .2s ease, box-shadow .2s ease;
}
#tbn-kw:hover {
transform:translateY(-2px);
box-shadow:0 6px 20px rgba(0,0,0,.3);
}
#tbn-kw:focus { border-color:rgba(96,165,250,.4);box-shadow:0 0 0 3px rgba(96,165,250,.08); }
#tbn-kw::placeholder { color:#374151; }
/* volume */
#tbn-vol-card {
padding:20px;
background:rgba(255,255,255,.05);
border:1px solid rgba(255,255,255,.08);
border-radius:18px;
display:flex;flex-direction:column;gap:18px;
}
#tbn-vol-row { display:flex;align-items:center;gap:16px; }
.tbn-vol-svg {
display:flex; align-items:center; justify-content:center;
width:20px; height:20px; flex-shrink:0; color:#60a5fa;
transition:color .2s ease;
}
.tbn-vol-svg svg { display:block; }
/* track wrapper */
#tbn-vol-wrap {
flex:1; height:8px;
background:rgba(255,255,255,.08);
border-radius:9999px;
position:relative;
}
/* blue filled portion — smooth transition */
#tbn-vol-fill {
position:absolute; left:0; top:0; height:100%;
background:linear-gradient(90deg, #2563eb, #3b82f6, #60a5fa);
border-radius:9999px;
pointer-events:none;
transition:width 40ms ease-out;
}
/* transparent range input — sits on top, captures drag */
#tbn-vol-slider {
-webkit-appearance:none; appearance:none;
position:absolute; inset:0;
width:100%; height:100%;
opacity:0; cursor:pointer;
z-index:20; margin:0; padding:0;
}
/* visual capsule thumb — smooth movement */
#tbn-vol-thumb {
position:absolute;
top:50%; transform:translateY(-50%);
width:28px; height:14px;
background:#ffffff;
border-radius:9999px;
box-shadow:0 0 0 2px rgba(59,130,246,.35), 0 2px 8px rgba(0,0,0,.4), 0 0 14px rgba(255,255,255,.18);
pointer-events:none;
z-index:10;
transition:left 40ms ease-out, box-shadow .2s ease;
}
/* glow on drag — we can't detect :active on the invisible input,
so we keep a nice resting glow always */
#tbn-vol-pct {
font-size:12px; font-weight:700;
color:#9ca3af;
font-family:'JetBrains Mono',monospace;
min-width:36px; text-align:right;
}
/* Test Sound */
#tbn-test-sound {
width:100%; padding:12px;
background:rgba(255,255,255,.05);
border:1px solid rgba(255,255,255,.07);
border-radius:13px;
color:#d1d5db; font-size:13px; font-weight:700;
display:flex; align-items:center; justify-content:center; gap:8px;
cursor:pointer; font-family:'Inter',sans-serif;
transition:background .18s, border-color .18s, color .15s, transform .2s ease, box-shadow .2s ease;
position:relative; overflow:hidden;
}
#tbn-test-sound:hover { background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.14); color:#fff; transform:translateY(-2px); box-shadow:0 6px 20px rgba(0,0,0,.3); }
#tbn-test-sound:active { transform:scale(.98); }
#tbn-test-sound svg { width:13px;height:13px;display:block;fill:currentColor; }
/* footer */
#tbn-footer { text-align:center;padding:16px 22px 20px;border-top:1px solid rgba(255,255,255,.05);font-size:13px;color:#94a3b8; }
#tbn-footer strong { color:#60a5fa;font-weight:700; }
/* ── Extras ── */
/* Sound card */
.tbn-sound-card { cursor:default; }
.tbn-sound-icon {
width:42px;height:42px;border-radius:12px;flex-shrink:0;
background:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.15);
display:flex;align-items:center;justify-content:center;color:#60a5fa;
}
.tbn-sound-icon svg { width:22px;height:22px;display:block; }
.tbn-sound-btn {
padding:8px 16px;
background:rgba(96,165,250,.15);border:1px solid rgba(96,165,250,.25);
border-radius:10px;color:#60a5fa;font-size:12px;font-weight:700;
cursor:pointer;font-family:'Inter',sans-serif;white-space:nowrap;
outline:none;
transition:background .15s,transform .2s ease,box-shadow .2s ease;
}
.tbn-sound-btn:focus { outline:none; box-shadow:none; }
.tbn-sound-btn:hover { background:rgba(96,165,250,.25);transform:translateY(-2px);box-shadow:0 4px 14px rgba(96,165,250,.2); }
.tbn-sound-remove {
width:28px;height:28px;border-radius:8px;
background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.25);
color:#f87171;font-size:13px;font-weight:700;
cursor:pointer;display:none;align-items:center;justify-content:center;
outline:none;
transition:background .15s,transform .2s ease,box-shadow .2s ease;
}
.tbn-sound-remove:hover { background:rgba(239,68,68,.3);transform:translateY(-2px);box-shadow:0 4px 12px rgba(239,68,68,.2); }
/* Default sound presets */
#tbn-sound-presets {
display:flex; flex-direction:column; gap:6px;
}
.tbn-sound-preset {
width:100%; padding:9px 13px;
background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08);
border-radius:10px;
color:#d1d5db; font-size:13px; font-weight:500; text-align:left;
cursor:pointer; font-family:'Inter',sans-serif;
transition:background .15s, border-color .15s, transform .2s ease, box-shadow .2s ease;
outline:none;
}
.tbn-sound-preset:hover { background:rgba(96,165,250,.1); border-color:rgba(96,165,250,.25); transform:translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,.2); }
.tbn-sound-preset.tbn-preset-active { background:rgba(96,165,250,.18); border-color:rgba(96,165,250,.4); color:#60a5fa; font-weight:700; }
/* ── Notification Board popup ── */
#tbn-nb-popup {
position:absolute; left:0; right:0; top:calc(100% + 8px);
background:rgba(12,17,35,.96);
backdrop-filter:blur(20px) saturate(1.3);
border:1px solid rgba(96,165,250,.2);
border-radius:16px;
padding:16px;
z-index:999;
box-shadow:0 20px 60px rgba(0,0,0,.6);
animation:tbn-nb-in .28s cubic-bezier(.22,1,.36,1);
}
@keyframes tbn-nb-in { from { opacity:0; transform:translateY(-10px) scale(.97); } to { opacity:1; transform:translateY(0) scale(1); } }
@keyframes tbn-nb-out { from { opacity:1; transform:translateY(0) scale(1); } to { opacity:0; transform:translateY(-8px) scale(.97); } }
#tbn-nb-popup.tbn-nb-closing { animation:tbn-nb-out .22s ease forwards; }
#tbn-nb-header {
display:flex; align-items:center; justify-content:space-between;
margin-bottom:14px;
}
#tbn-nb-title {
font-size:11px; font-weight:700; color:#6b7280;
text-transform:uppercase; letter-spacing:.08em;
}
#tbn-nb-close {
background:none; border:none; cursor:pointer;
color:#6b7280; font-size:14px; padding:2px 6px;
border-radius:6px; outline:none;
transition:color .15s, background .15s;
}
#tbn-nb-close:hover { color:#e5e7eb; background:rgba(255,255,255,.08); }
#tbn-nb-grid {
display:grid; grid-template-columns:1fr 1fr; gap:8px;
}
.tbn-nb-btn {
display:flex; align-items:center; gap:8px;
padding:11px 13px;
background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08);
border-radius:12px;
color:#d1d5db; font-size:13px; font-weight:500; text-align:left;
cursor:pointer; font-family:'Inter',sans-serif;
transition:background .15s, border-color .15s, transform .18s ease, box-shadow .18s ease;
outline:none;
}
.tbn-nb-btn svg { width:13px; height:13px; display:block; flex-shrink:0; color:#6b7280; transition:color .15s; }
.tbn-nb-btn:hover { background:rgba(96,165,250,.1); border-color:rgba(96,165,250,.25); transform:translateY(-2px); box-shadow:0 4px 14px rgba(0,0,0,.3); }
.tbn-nb-btn.tbn-nb-active { background:rgba(96,165,250,.15); border-color:rgba(96,165,250,.4); color:#60a5fa; font-weight:700; }
.tbn-nb-btn.tbn-nb-active svg { color:#60a5fa; }
/* Dark/Light mode grid */
#tbn-mode-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px; }
.tbn-mode-btn {
display:flex;align-items:center;justify-content:center;gap:8px;
padding:12px;border-radius:14px;
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);
color:#6b7280;font-size:13px;font-weight:600;
cursor:pointer;font-family:'Inter',sans-serif;
transition:background .18s,border-color .18s,color .18s,transform .2s ease,box-shadow .2s ease;
}
.tbn-mode-btn svg { width:16px;height:16px;display:block; }
.tbn-mode-btn:hover { transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.3); }
.tbn-mode-btn.tbn-mode-active {
background:rgba(96,165,250,.18);border-color:rgba(96,165,250,.4);
color:#60a5fa;
}
/* ripple */
.tbn-ripple {
position:absolute;width:8px;height:8px;border-radius:50%;
background:rgba(255,255,255,.18);
top:50%;left:50%;transform:translate(-50%,-50%) scale(0);
animation:tbn-ripple-kf .52s ease-out forwards;pointer-events:none;
}
@keyframes tbn-ripple-kf { to { transform:translate(-50%,-50%) scale(32);opacity:0; } }
`);
/* ═══════════════════════════════════════════════════════
BOOT
═══════════════════════════════════════════════════════ */
window.addEventListener('load', () => {
BrowserNotify.requestPermission();
Sound.preload();
const det = detectUser();
if (det && !Cfg.username) Cfg.save('username', det);
const modal = buildModal();
const { syncAll } = wireModal(modal);
_syncAll = syncAll;
applyTheme(Cfg.darkMode);
addTrigger();
startObserver();
});
})();