您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Complete chat enhancement: emote replacer, keyword highlighter, and custom flairs for vyneer.me VODs
// ==UserScript== // @name Vyneer.me VODs Chat Enhancement // @namespace FishVernanda // @version 2025-10-05 // @description Complete chat enhancement: emote replacer, keyword highlighter, and custom flairs for vyneer.me VODs // @author FishVernanda // @match https://vyneer.me/vods/* // @match http://localhost:1234/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // ======================================== // EMOTE REPLACER // ======================================== GM_addStyle(` /* Flair Icons */ .flair.flair30 { background-image: url("https://wikicdn.destiny.gg/c/ca/Flair_league_master.png"); height: 16px; width: 16px; } .flair.flair4 { background-image: url("https://wikicdn.destiny.gg/8/87/Flair_4.png"); height: 16px; width: 16px; } .flair.flair5 { background-image: url("https://wikicdn.destiny.gg/5/5e/Flair_5.png"); height: 16px; width: 16px; } .flair.flair16 { background-image: url("https://wikicdn.destiny.gg/d/df/Flair_emote_contributor.png"); height: 16px; width: 16px; } .flair.flair10 { background-image: url("https://wikicdn.destiny.gg/6/61/Flair_starcraft_2.png"); height: 16px; width: 16px; } .flair.flair25 { background-image: url("https://cdn.destiny.gg/flairs/5f28cdec6ed24.png"); height: 16px; width: 16px; } .flair.flair27 { background-image: url("https://cdn.destiny.gg/flairs/60b130f07c0c4.png"); height: 18px; width: 18px; } /* Emotes */ .emote.RainbowPls { width: 32px; height: 32px; background-image: url('https://wikicdn.destiny.gg/f/f4/GRainbowPls.png'); } .msg-chat .emote.RainbowPls { margin-top: -30px; top: 7.5px; } .emote.NathanD { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/4/41/NathanD.png'); } .msg-chat .emote.NathanD { margin-top: -28px; top: 7px; } .emote.pokiKick { width: 61px; height: 32px; background-image: url('https://wikicdn.destiny.gg/b/b1/Pokikick.gif'); } .msg-chat .emote.pokiKick { margin-top: -32px; top: 8px; } .emote.nathanW { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/e/ef/NathanW.png'); } .msg-chat .emote.nathanW { margin-top: -28px; top: 7px; } .emote.melW { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/2/2e/MelWemote.png'); } .msg-chat .emote.melW { margin-top: -28px; top: 7px; } .emote.DestiSenpaii { width: 32px; height: 30px; background-image: url('https://wikicdn.destiny.gg/1/18/DestiSenpaii.png'); } .msg-chat .emote.DestiSenpaii { margin-top: -30px; top: 7px; } .emote.NathanSenpai { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/a/a4/NathanSenpai.png'); } .msg-chat .emote.NathanSenpai { margin-top: -28px; top: 7px; } .emote.PICNIC { width: 50px; height: 20px; background-image: url('https://wikicdn.destiny.gg/f/f8/PICNIC.png'); } .msg-chat .emote.PICNIC { margin-top: -20px; top: 6px; } .emote.Yoda1 { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/1/18/Yoda1.png'); } .msg-chat .emote.Yoda1 { margin-top: -28px; top: 7px; } .emote.DatGeoff { width: 21px; height: 30px; background-image: url('https://wikicdn.destiny.gg/3/3c/DatGeoff.png'); } .msg-chat .emote.DatGeoff { margin-top: -30px; top: 7px; } .emote.ComfyMel { width: 32px; height: 32px; background-image: url('https://wikicdn.destiny.gg/5/56/ComfyMel.png'); } .msg-chat .emote.ComfyMel { margin-top: -32px; top: 7px; } .emote.ATAB { width: 40px; height: 30px; background-image: url('https://wikicdn.destiny.gg/c/c2/ATAB.png'); } .msg-chat .emote.ATAB { margin-top: -30px; top: 7px; } .emote.nathanShroom { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/3/32/NathanShroom.png'); } .msg-chat .emote.nathanShroom { margin-top: -28px; top: 7px; } .emote.ComfyAYA { width: 32px; height: 32px; background-image: url('https://wikicdn.destiny.gg/9/95/ComfyAYA.png'); } .msg-chat .emote.ComfyAYA { margin-top: -32px; top: 7px; } .emote.AUTISTINY { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/8/83/NathanDerp.png'); } .msg-chat .emote.AUTISTINY { margin-top: -28px; top: 7px; } .emote.SoDoge { width: 52px; height: 30px; background-image: url('https://wikicdn.destiny.gg/7/73/SoDoge.png'); } .msg-chat .emote.SoDoge { margin-top: -30px; top: 7px; } .emote.nathanYikes { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/1/1a/NathanYikes.png'); } .msg-chat .emote.nathanYikes { margin-top: -28px; top: 7px; } .emote.nathanEZ { width: 28px; height: 28px; background-image: url('https://wikicdn.destiny.gg/0/0e/NathanEZ.png'); } .msg-chat .emote.nathanEZ { margin-top: -28px; top: 7px; } .emote.TF { height: 30px; width: 37px; background-image: url("https://wikicdn.destiny.gg/c/c8/TrollFace.png"); } .msg-chat .emote.TF { margin-top: -30px; top: 7.5px; } .emote.tf { height: 30px; width: 37px; background-image: url("https://wikicdn.destiny.gg/c/c8/TrollFace.png"); } .msg-chat .emote.tf { margin-top: -30px; top: 7.5px; } `); const emoteMap = { rainbowpls: "RainbowPls", pokikick: "pokiKick", nathanw: "nathanW", melw: "melW", destisenpaii: "DestiSenpaii", nathansenpai: "NathanSenpai", picnic: "PICNIC", yoda1: "Yoda1", datgeoff: "DatGeoff", comfymel: "ComfyMel", atab: "ATAB", nathand: "NathanD", nathanshroom: "nathanShroom", comfyaya: "ComfyAYA", autistiny: "AUTISTINY", sodoge: "SoDoge", nathanyikes: "nathanYikes", nathanez: "nathanEZ", tf: "TF", }; function getFullEmoteMap() { const fullMap = {...emoteMap}; for (const [searchName, emoteData] of Object.entries(customEmotes)) { fullMap[searchName.toLowerCase()] = emoteData.displayName || searchName; } return fullMap; } function replaceTextWithEmotesInNode(textNode) { const parent = textNode.parentNode; const text = textNode.nodeValue; const fullEmoteMap = getFullEmoteMap(); const pattern = new RegExp(`\\b(${Object.keys(fullEmoteMap).join('|')})\\b`, 'gi'); if (!pattern.test(text)) return; const parts = text.split(pattern); parent.removeChild(textNode); for (const part of parts) { if (!part) continue; const key = part.toLowerCase(); if (fullEmoteMap[key]) { const emoteDiv = document.createElement('div'); emoteDiv.className = 'emote ' + fullEmoteMap[key]; emoteDiv.title = fullEmoteMap[key]; parent.appendChild(emoteDiv); } else { parent.appendChild(document.createTextNode(part)); } } } function processMessageSpanForEmotes(span) { const childNodes = Array.from(span.childNodes); for (const node of childNodes) { if (node.nodeType === Node.TEXT_NODE) { replaceTextWithEmotesInNode(node); } } } function processExistingMessagesForEmotes() { const chatMessages = document.querySelectorAll('#chat-stream .msg-chat .message'); chatMessages.forEach(processMessageSpanForEmotes); } // ======================================== // KEYWORD HIGHLIGHTER & SETTINGS UI // ======================================== GM_addStyle(` .msg-highlight { color: #dedede !important; background-color: #06263e !important; } #highlight-settings-container { position: relative; display: inline-block; } #highlight-settings-content { display: none; position: absolute; right: 0; top: 32px; background-color: #1c1c1c; border: 1px solid #444; padding: 12px; z-index: 1001; width: 300px; box-shadow: 0 2px 8px rgba(0,0,0,.4); border-radius: 4px; max-height: 70vh; overflow: auto; } .label-text { color: #ccc; font-size: 12px; margin-bottom: 4px; display: block; } textarea { width: 90% !important; background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 2px; font-size: 12px; padding: 6px; } select { background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 2px; font-size: 12px; padding: 4px; } .emote-section, .flair-row, .blacklist-row { background: #1a1a1a; border: 1px solid #333; border-radius: 4px; padding: 8px; margin-bottom: 8px; } .settings-export-import { display: flex; gap: 4px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #666; justify-content: flex-end; align-items: center; } .settings-export-import .focus-checkbox-wrapper { margin-right: auto; display: flex; align-items: center; gap: 6px; } .settings-export-import .focus-checkbox-wrapper input[type="checkbox"] { cursor: pointer; } .settings-export-import .focus-checkbox-wrapper label { cursor: pointer; font-size: 12px; color: #ccc; } .settings-export-import button { padding: 4px 8px; background: #444; border: 1px solid #666; color: #fff; cursor: pointer; border-radius: 3px; font-size: 11px; } .settings-export-import button:hover { background: #555; } .settings-export-import button.success { background: #2d5016; border-color: #4a8028; } .settings-export-import button.error { background: #661111; border-color: #881111; } #highlightKeywords, #flair34Users, #vipUsers { width: 95%; margin-top: 5px; margin-bottom: 10px; resize: vertical; min-height: 80px; } .flair-row { display: block; margin-bottom: 12px; border-top: 1px solid rgba(255,255,255,0.04); padding-top: 8px; } .flair-row .flair-top { display: flex; gap: 8px; align-items: center; } .flair-row .flair-top select { flex: 1 1 auto; padding: 6px; font-size: 13px; } .remove-flair, .remove-emote, .remove-blacklist { flex: 0 0 30px; height: 30px; background: #661111; border: 1px solid #881111; color: #fff; cursor: pointer; border-radius: 3px; font-weight: bold; } .remove-flair:hover, .remove-emote:hover, .remove-blacklist:hover { background: #881111; } .flair-row textarea { width: 95%; margin-top: 6px; min-height: 70px; resize: vertical; padding: 6px; } #addFlairButton { padding: 6px 12px; background: #444; border: 1px solid #666; color: #fff; cursor: pointer; border-radius: 3px; margin-top: 5px; } #addFlairButton:hover { background: #666; } .emote-section, .flair-removal-section, .custom-flairs-section { margin-top: 15px; padding: 12px; background: #1a1a1a; border: 1px solid #333; border-radius: 4px; } .settings-section-header { font-size: 14px; color: #ccc; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #333; } .emote-row { display: block; margin-bottom: 12px; border-top: 1px solid rgba(255,255,255,0.04); padding-top: 8px; max-width: 340px; } .toggle-advanced { background: none !important; border: none !important; color: #aaa !important; padding: 4px 0 !important; cursor: pointer !important; width: 100% !important; text-align: left !important; margin: 8px 0 !important; font-size: 12px !important; } .toggle-advanced:hover { color: #fff !important; } .emote-advanced-settings { display: none; margin-top: 8px; padding: 8px; background: #1a1a1a; border-radius: 4px; } .emote-row-header { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; } .toggle-advanced { background: none !important; border: none !important; color: #aaa !important; padding: 4px 0 !important; cursor: pointer !important; width: 100% !important; text-align: left !important; margin: 8px 0 !important; font-size: 12px !important; } .toggle-advanced:hover { color: #fff !important; } .emote-advanced-settings { display: none; margin-top: 8px; padding: 8px; background: #1a1a1a; border-radius: 4px; } .emote-row-header button.remove-emote { flex: 0 0 30px; height: 30px; background: #661111; border: 1px solid #881111; color: #fff; cursor: pointer; border-radius: 3px; font-weight: bold; } .emote-row-header button.remove-emote:hover { background: #881111; } .emote-row input[type="text"] { width: 180px; box-sizing: border-box; padding: 4px 6px; margin-bottom: 4px; font-size: 12px; background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 2px; } .emote-row input[type="number"] { width: 70px; box-sizing: border-box; padding: 4px 6px; margin-bottom: 4px; font-size: 12px; background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 2px; } .emote-row .emote-dimensions { margin-bottom: 8px; } .emote-row .emote-dimensions .dimension-group { display: flex; gap: 8px; margin-bottom: 4px; } .emote-row .emote-dimensions .dimension-group > div { flex: 1; } .emote-row textarea { width: 100%; box-sizing: border-box; margin-top: 4px; min-height: 60px; resize: vertical; padding: 6px; font-family: monospace; font-size: 11px; background: #2a2a2a; border: 1px solid #444; color: #fff; border-radius: 2px; } .emote-row label { display: block; font-size: 11px; color: #aaa; margin-bottom: 2px; } .emote-section { margin-top: 15px; padding: 8px; background: #1a1a1a; border-radius: 4px; } .emote-advanced-settings { display: none; margin-top: 8px; padding: 8px; background: #1a1a1a; border-radius: 4px; } .toggle-advanced { background: none !important; border: none !important; color: #aaa !important; padding: 4px 0 !important; cursor: pointer !important; width: 100% !important; text-align: left !important; margin: 8px 0 !important; font-size: 12px !important; } .toggle-advanced:hover { color: #fff !important; } .emote-advanced-settings { background: #1a1a1a; border-radius: 4px; padding: 8px; margin-top: 8px; } .advanced-toggle { background: none; border: none; color: #aaa; padding: 4px 0; cursor: pointer; width: 100%; text-align: left; margin-top: 8px; font-size: 12px; } .advanced-toggle:hover { color: #fff; } .emote-property { margin-bottom: 8px; } .emote-presets { margin-bottom: 12px; padding: 8px; background: #2a2a2a; border-radius: 4px; } .emote-presets select { width: 100%; padding: 6px; background: #333; border: 1px solid #444; color: #fff; border-radius: 2px; } #addEmoteButton { padding: 6px 12px; background: #444; border: 1px solid #666; color: #fff; cursor: pointer; border-radius: 3px; margin-top: 5px; } #addEmoteButton:hover { background: #666; } .flair-removal-section { margin-top: 15px; padding-top: 15px; border-top: 1px solid #666; } .blacklist-row { display: block; margin-bottom: 12px; border-top: 1px solid rgba(255,255,255,0.04); padding-top: 8px; } .blacklist-row .blacklist-top { display: flex; gap: 8px; align-items: center; } .blacklist-row .blacklist-top select { flex: 1 1 auto; padding: 6px; font-size: 13px; } .blacklist-row .blacklist-top button.remove-blacklist { flex: 0 0 30px; height: 30px; background: #661111; border: 1px solid #881111; color: #fff; cursor: pointer; border-radius: 3px; font-weight: bold; } .blacklist-row .blacklist-top button.remove-blacklist:hover { background: #881111; } .blacklist-row textarea { width: 95%; margin-top: 6px; min-height: 70px; resize: vertical; padding: 6px; } #addBlacklistButton { padding: 6px 12px; background: #883333; border: 1px solid #aa4444; color: #fff; cursor: pointer; border-radius: 3px; margin-top: 5px; } #addBlacklistButton:hover { background: #aa4444; } #highlight-settings-button { cursor: pointer; margin-left: 4px; } #highlight-settings-button .octicon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; } `); let keywordsToHighlight = []; let flair34Users = []; let vipUsers = []; let selectedFlairUsers = {}; let flairBlacklist = {}; let customEmotes = {}; let includeMentionsInFocus = false; const flairDefinitions = { 'flair13': 'Subscriber Tier 1', 'flair1': 'Subscriber Tier 2', 'flair3': 'Subscriber Tier 3', 'flair8': 'Subscriber Tier 4', 'flair42': 'Subscriber Tier 5', 'subscriber': 'Subscriber', 'flair9': 'Twitch Subscriber', 'flair32': 'Tier 1 AE Sub', 'flair22': 'Tier 2 AE Sub', 'flair24': 'Tier 3 AE Sub', 'flair26': 'Tier 4 AE Sub', 'flair33': 'Tier 5 AE Sub', 'bot': 'Bot', 'flair11': 'Old Bot', 'admin': 'Admin', 'moderator': 'Moderator', 'vip': 'VIP', 'flair12': 'Broadcaster', 'flair20': 'Verified', 'flair2': 'Notable Chatter', 'flair17': 'Extra Notable Chatter', 'flair4': 'Trusted', 'flair5': 'Contributor', 'flair16': 'Emote Contributor', 'flair25': 'Youtube Contributor', 'flair18': 'Emote Manager', 'flair19': 'DGG Shirt Designer', 'flair21': 'YouTube Editor', 'flair27': 'TikTok Editor', 'flair31': 'Developer', 'flair7': 'NFL Chatter', 'flair10': 'StarCraft 2', 'flair30': 'League Master', 'flair29': 'Weight Lifting', 'flair23': 'Doctor', 'flair28': 'Lawyer', 'flair34': 'Conductor', 'flair15': 'Birthday', 'flair58': 'New User' }; function loadKeywords() { const savedKeywords = GM_getValue('highlightKeywords', ''); keywordsToHighlight = savedKeywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); } function loadFlair34Users() { const saved = GM_getValue('flair34Users', ''); flair34Users = saved.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); } function loadVipUsers() { const saved = GM_getValue('vipUsers', ''); vipUsers = saved.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); } function loadSelectedFlairUsers() { const saved = GM_getValue('selectedFlairUsers', '{}'); try { selectedFlairUsers = JSON.parse(saved); } catch (e) { selectedFlairUsers = {}; } } function loadFlairBlacklist() { const saved = GM_getValue('flairBlacklist', '{}'); try { flairBlacklist = JSON.parse(saved); } catch (e) { flairBlacklist = {}; } } function loadCustomEmotes() { const saved = GM_getValue('customEmotes', '{}'); try { customEmotes = JSON.parse(saved); } catch (e) { customEmotes = {}; } } function loadIncludeMentionsInFocus() { includeMentionsInFocus = GM_getValue('includeMentionsInFocus', false); } function applyCustomEmoteStyles() { let emoteStyles = ''; for (const [emoteName, emoteData] of Object.entries(customEmotes)) { const className = emoteData.displayName || emoteName; emoteStyles += ` .emote.${className} { width: ${emoteData.width}px; height: ${emoteData.height}px; background-image: url('${emoteData.url}'); } .msg-chat .emote.${className} { margin-top: ${emoteData.marginTop || -emoteData.height}px; top: ${emoteData.top || Math.floor(emoteData.height / 4)}px; ${emoteData.marginLeft ? `margin-left: ${emoteData.marginLeft};` : ''} ${emoteData.marginRight ? `margin-right: ${emoteData.marginRight};` : ''} ${emoteData.transform ? `transform: ${emoteData.transform};` : ''} ${emoteData.filter ? `filter: ${emoteData.filter};` : ''} ${emoteData.backgroundPosition ? `background-position: ${emoteData.backgroundPosition};` : ''} ${emoteData.backgroundSize ? `background-size: ${emoteData.backgroundSize};` : ''} ${emoteData.backgroundRepeat ? `background-repeat: ${emoteData.backgroundRepeat};` : ''} ${emoteData.transition ? `transition: ${emoteData.transition};` : ''} } `; if (emoteData.animation) { emoteStyles += emoteData.animation + '\n'; } } if (emoteStyles) { GM_addStyle(emoteStyles); } } function saveKeywords(keywordsStr) { const keywords = keywordsStr.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); keywordsToHighlight = keywords; GM_setValue('highlightKeywords', keywords.join(',')); clearHighlights(); processExistingMessagesForHighlight(); } function saveFlair34Users(usersStr) { const users = usersStr.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); flair34Users = users; GM_setValue('flair34Users', users.join(',')); processExistingMessagesForFlairs(); } function saveVipUsers(usersStr) { const users = usersStr.split(',').map(k => k.trim().toLowerCase()).filter(Boolean); vipUsers = users; GM_setValue('vipUsers', users.join(',')); processExistingMessagesForFlairs(); } function clearHighlights() { const highlighted = document.querySelectorAll('#chat-stream .msg-highlight'); highlighted.forEach(el => el.classList.remove('msg-highlight')); } function shouldHighlight(text) { const lower = text.toLowerCase(); return keywordsToHighlight.some(name => lower.includes(name)); } function processMessageForHighlight(div) { const rawText = div.textContent || ''; if (shouldHighlight(rawText)) { div.classList.add('msg-highlight'); } } function processExistingMessagesForHighlight() { const messages = document.querySelectorAll('#chat-stream .msg-chat'); messages.forEach(processMessageForHighlight); } function setupSettingsUI() { const settingsContainer = document.getElementById('settings'); if (!settingsContainer) { setTimeout(setupSettingsUI, 500); return; } const dropdownContainer = document.createElement('div'); dropdownContainer.id = 'highlight-settings-container'; dropdownContainer.className = 'settings-dropdown'; const button = document.createElement('a'); button.id = 'highlight-settings-button'; button.setAttribute('aria-label', 'Highlight Settings'); button.setAttribute('data-tippy-content', 'Highlight Settings'); button.innerHTML = '<span class="octicon octicon-gear"></span>'; const content = document.createElement('div'); content.id = 'highlight-settings-content'; const exportImportDiv = document.createElement('div'); exportImportDiv.className = 'settings-export-import'; const focusCheckboxWrapper = document.createElement('div'); focusCheckboxWrapper.className = 'focus-checkbox-wrapper'; const focusCheckbox = document.createElement('input'); focusCheckbox.type = 'checkbox'; focusCheckbox.id = 'includeMentionsCheckbox'; focusCheckbox.checked = includeMentionsInFocus; focusCheckbox.addEventListener('change', () => { includeMentionsInFocus = focusCheckbox.checked; GM_setValue('includeMentionsInFocus', includeMentionsInFocus); // No need to call updateFocusRules - it checks the checkbox dynamically }); const focusLabel = document.createElement('label'); focusLabel.htmlFor = 'includeMentionsCheckbox'; focusLabel.textContent = 'Include mentions when focused'; focusCheckboxWrapper.appendChild(focusCheckbox); focusCheckboxWrapper.appendChild(focusLabel); const exportButton = document.createElement('button'); exportButton.textContent = 'Copy'; exportButton.addEventListener('click', () => { const settings = { highlightKeywords: keywordsToHighlight, flair34Users: flair34Users, vipUsers: vipUsers, selectedFlairUsers: selectedFlairUsers, flairBlacklist: flairBlacklist, customEmotes: customEmotes, includeMentionsInFocus: includeMentionsInFocus }; navigator.clipboard.writeText(JSON.stringify(settings, null, 2)).then(() => { const originalText = exportButton.textContent; exportButton.textContent = '✓'; exportButton.classList.add('success'); setTimeout(() => { exportButton.textContent = originalText; exportButton.classList.remove('success'); }, 2000); }).catch(err => { console.error('Failed to copy:', err); exportButton.textContent = '✗'; exportButton.classList.add('error'); setTimeout(() => { exportButton.textContent = 'Copy'; exportButton.classList.remove('error'); }, 2000); }); }); const importButton = document.createElement('button'); importButton.textContent = 'Paste'; const label1 = document.createElement('label'); label1.className = 'label-text'; label1.textContent = 'Highlight Keywords:'; const textarea1 = document.createElement('textarea'); textarea1.id = 'highlightKeywords'; textarea1.className = 'settings-options'; textarea1.placeholder = 'Comma separated keywords...'; textarea1.value = keywordsToHighlight.join(','); textarea1.addEventListener('change', () => saveKeywords(textarea1.value)); const label2 = document.createElement('label'); label2.className = 'label-text'; label2.textContent = 'Conductors (Flair34):'; const textarea2 = document.createElement('textarea'); textarea2.id = 'flair34Users'; textarea2.className = 'settings-options'; textarea2.placeholder = 'Comma separated usernames...'; textarea2.value = flair34Users.join(','); textarea2.addEventListener('change', () => saveFlair34Users(textarea2.value)); const label3 = document.createElement('label'); label3.className = 'label-text'; label3.textContent = 'VIP Users:'; const textarea3 = document.createElement('textarea'); textarea3.id = 'vipUsers'; textarea3.className = 'settings-options'; textarea3.placeholder = 'Comma separated usernames...'; textarea3.value = vipUsers.join(','); textarea3.addEventListener('change', () => saveVipUsers(textarea3.value)); const label4 = document.createElement('label'); label4.className = 'label-text'; label4.textContent = 'Custom Flairs:'; label4.style.marginTop = '10px'; const flairContainer = document.createElement('div'); flairContainer.id = 'flairContainer'; const addButton = document.createElement('button'); addButton.id = 'addFlairButton'; addButton.textContent = '+ Add Flair'; addButton.addEventListener('click', () => addFlairRow()); function createFlairRow(flairKey = '', users = '') { const row = document.createElement('div'); row.className = 'flair-row'; const top = document.createElement('div'); top.className = 'flair-top'; const select = document.createElement('select'); const defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.textContent = 'Select flair...'; select.appendChild(defaultOpt); for (const [key, name] of Object.entries(flairDefinitions)) { const opt = document.createElement('option'); opt.value = key; opt.textContent = `${name}`; if (key === flairKey) opt.selected = true; select.appendChild(opt); } const removeBtn = document.createElement('button'); removeBtn.className = 'remove-flair'; removeBtn.textContent = '×'; removeBtn.title = 'Remove this flair mapping'; removeBtn.addEventListener('click', (e) => { e.stopPropagation(); row.remove(); saveAllFlairRows(); }); top.appendChild(select); top.appendChild(removeBtn); const textarea = document.createElement('textarea'); textarea.placeholder = 'Comma separated usernames...'; textarea.value = users; select.addEventListener('change', () => saveAllFlairRows()); textarea.addEventListener('input', () => saveAllFlairRows()); row.appendChild(top); row.appendChild(textarea); return row; } function addFlairRow(flairKey = '', users = '') { const row = createFlairRow(flairKey, users); flairContainer.appendChild(row); } function saveAllFlairRows() { const rows = flairContainer.querySelectorAll('.flair-row'); const newFlairUsers = {}; rows.forEach(row => { const select = row.querySelector('select'); const textarea = row.querySelector('textarea'); const flair = select.value; const users = textarea.value.split(',').map(u => u.trim().toLowerCase()).filter(Boolean); if (flair && users.length > 0) { if (!newFlairUsers[flair]) { newFlairUsers[flair] = []; } newFlairUsers[flair].push(...users); } }); selectedFlairUsers = newFlairUsers; GM_setValue('selectedFlairUsers', JSON.stringify(selectedFlairUsers)); processExistingMessagesForFlairs(); } for (const [flairKey, usersList] of Object.entries(selectedFlairUsers)) { addFlairRow(flairKey, Array.isArray(usersList) ? usersList.join(',') : String(usersList)); } const removalSection = document.createElement('div'); removalSection.className = 'flair-removal-section'; const removalLabel = document.createElement('label'); removalLabel.className = 'label-text'; removalLabel.textContent = 'Remove Flairs from Users:'; const blacklistContainer = document.createElement('div'); blacklistContainer.id = 'blacklistContainer'; const addBlacklistButton = document.createElement('button'); addBlacklistButton.id = 'addBlacklistButton'; addBlacklistButton.textContent = '+ Add Flair Removal'; addBlacklistButton.addEventListener('click', () => addBlacklistRow()); function createBlacklistRow(flairKey = '', users = '') { const row = document.createElement('div'); row.className = 'blacklist-row'; const top = document.createElement('div'); top.className = 'blacklist-top'; const select = document.createElement('select'); const defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.textContent = 'Select flair to remove...'; select.appendChild(defaultOpt); for (const [key, name] of Object.entries(flairDefinitions)) { const opt = document.createElement('option'); opt.value = key; opt.textContent = `${name}`; if (key === flairKey) opt.selected = true; select.appendChild(opt); } const removeBtn = document.createElement('button'); removeBtn.className = 'remove-blacklist'; removeBtn.textContent = '×'; removeBtn.title = 'Remove this flair removal mapping'; removeBtn.addEventListener('click', (e) => { e.stopPropagation(); row.remove(); saveAllBlacklistRows(); }); top.appendChild(select); top.appendChild(removeBtn); const textarea = document.createElement('textarea'); textarea.placeholder = 'Comma separated usernames to remove this flair from...'; textarea.value = users; select.addEventListener('change', () => saveAllBlacklistRows()); textarea.addEventListener('input', () => saveAllBlacklistRows()); row.appendChild(top); row.appendChild(textarea); return row; } function addBlacklistRow(flairKey = '', users = '') { const row = createBlacklistRow(flairKey, users); blacklistContainer.appendChild(row); } function saveAllBlacklistRows() { const rows = blacklistContainer.querySelectorAll('.blacklist-row'); const newBlacklist = {}; rows.forEach(row => { const select = row.querySelector('select'); const textarea = row.querySelector('textarea'); const flair = select.value; const users = textarea.value.split(',').map(u => u.trim().toLowerCase()).filter(Boolean); if (flair && users.length > 0) { users.forEach(username => { if (!newBlacklist[username]) { newBlacklist[username] = []; } if (!newBlacklist[username].includes(flair)) { newBlacklist[username].push(flair); } }); } }); flairBlacklist = newBlacklist; GM_setValue('flairBlacklist', JSON.stringify(flairBlacklist)); processExistingMessagesForFlairRemoval(); } const rebuildBlacklistRows = () => { blacklistContainer.innerHTML = ''; const blacklistByFlair = {}; for (const [username, flairs] of Object.entries(flairBlacklist)) { flairs.forEach(flair => { if (!blacklistByFlair[flair]) { blacklistByFlair[flair] = []; } blacklistByFlair[flair].push(username); }); } for (const [flair, users] of Object.entries(blacklistByFlair)) { addBlacklistRow(flair, users.join(',')); } }; rebuildBlacklistRows(); importButton.addEventListener('click', () => { navigator.clipboard.readText().then(text => { try { const settings = JSON.parse(text); if (!settings || typeof settings !== 'object') { throw new Error('Invalid settings format'); } if (Array.isArray(settings.highlightKeywords)) { keywordsToHighlight = settings.highlightKeywords; GM_setValue('highlightKeywords', keywordsToHighlight.join(',')); textarea1.value = keywordsToHighlight.join(','); } if (Array.isArray(settings.flair34Users)) { flair34Users = settings.flair34Users; GM_setValue('flair34Users', flair34Users.join(',')); textarea2.value = flair34Users.join(','); } if (Array.isArray(settings.vipUsers)) { vipUsers = settings.vipUsers; GM_setValue('vipUsers', vipUsers.join(',')); textarea3.value = vipUsers.join(','); } if (settings.selectedFlairUsers && typeof settings.selectedFlairUsers === 'object') { selectedFlairUsers = settings.selectedFlairUsers; GM_setValue('selectedFlairUsers', JSON.stringify(selectedFlairUsers)); flairContainer.innerHTML = ''; for (const [flairKey, usersList] of Object.entries(selectedFlairUsers)) { addFlairRow(flairKey, Array.isArray(usersList) ? usersList.join(',') : String(usersList)); } } if (settings.flairBlacklist && typeof settings.flairBlacklist === 'object') { flairBlacklist = settings.flairBlacklist; GM_setValue('flairBlacklist', JSON.stringify(flairBlacklist)); rebuildBlacklistRows(); } if (settings.customEmotes && typeof settings.customEmotes === 'object') { customEmotes = settings.customEmotes; GM_setValue('customEmotes', JSON.stringify(customEmotes)); rebuildEmoteRows(); applyCustomEmoteStyles(); } if (typeof settings.includeMentionsInFocus === 'boolean') { includeMentionsInFocus = settings.includeMentionsInFocus; GM_setValue('includeMentionsInFocus', includeMentionsInFocus); focusCheckbox.checked = includeMentionsInFocus; updateFocusRules(); } clearHighlights(); processExistingMessagesForHighlight(); processExistingMessagesForFlairRemoval(); processExistingMessagesForFlairs(); processExistingMessagesForEmotes(); const originalText = importButton.textContent; importButton.textContent = '✓'; importButton.classList.add('success'); setTimeout(() => { importButton.textContent = originalText; importButton.classList.remove('success'); }, 2000); } catch (err) { console.error('Failed to parse settings:', err); importButton.textContent = '✗'; importButton.classList.add('error'); setTimeout(() => { importButton.textContent = 'Paste'; importButton.classList.remove('error'); }, 2000); } }).catch(err => { console.error('Failed to read clipboard:', err); importButton.textContent = '✗'; importButton.classList.add('error'); setTimeout(() => { importButton.textContent = 'Paste'; importButton.classList.remove('error'); }, 2000); }); }); removalSection.appendChild(removalLabel); removalSection.appendChild(blacklistContainer); removalSection.appendChild(addBlacklistButton); // Custom Emote Section const emoteSection = document.createElement('div'); emoteSection.className = 'emote-section'; const emoteLabel = document.createElement('label'); emoteLabel.className = 'label-text'; emoteLabel.textContent = 'Custom Emotes:'; const emoteContainer = document.createElement('div'); emoteContainer.id = 'emoteContainer'; const addEmoteButton = document.createElement('button'); addEmoteButton.id = 'addEmoteButton'; addEmoteButton.textContent = '+ Add Emote'; addEmoteButton.addEventListener('click', () => addEmoteRow()); // Keep track of advanced toggle state for each row const advancedStates = new WeakMap(); function createAdvancedToggle(section) { const toggle = document.createElement('button'); toggle.className = 'advanced-toggle'; toggle.innerHTML = '▶ Advanced Settings'; toggle.style.background = 'none'; toggle.style.border = 'none'; toggle.style.color = '#aaa'; toggle.style.padding = '4px 0'; toggle.style.cursor = 'pointer'; toggle.style.width = '100%'; toggle.style.textAlign = 'left'; toggle.style.marginTop = '8px'; toggle.addEventListener('click', () => { const isCollapsed = section.style.display === 'none'; section.style.display = isCollapsed ? 'block' : 'none'; toggle.innerHTML = `${isCollapsed ? '▼' : '▶'} Advanced Settings`; }); return toggle; } function createEmoteRow(searchName = '', data = {}) { const row = document.createElement('div'); row.className = 'emote-row'; // Header with remove button const rowHeader = document.createElement('div'); rowHeader.className = 'emote-row-header'; const removeButton = document.createElement('button'); removeButton.className = 'remove-emote'; removeButton.textContent = '×'; removeButton.title = 'Remove this emote'; removeButton.addEventListener('click', (e) => { e.stopPropagation(); row.remove(); saveAllEmoteRows(); }); rowHeader.appendChild(removeButton); row.appendChild(rowHeader); // Basic fields const createField = (labelText, type, placeholder, value) => { const container = document.createElement('div'); const label = document.createElement('label'); label.textContent = labelText; const input = document.createElement('input'); input.type = type; input.placeholder = placeholder; input.value = value; input.addEventListener('input', () => saveAllEmoteRows()); container.appendChild(label); container.appendChild(input); return { container, input }; }; const searchField = createField('Search Name (case-insensitive):', 'text', 'e.g., itsrawww', searchName); const displayField = createField('Display Name (case-sensitive):', 'text', 'e.g., ITSRAWWW', data.displayName || ''); const urlField = createField('Emote URL:', 'text', 'https://wikicdn.destiny.gg/...', data.url || ''); row.appendChild(searchField.container); row.appendChild(displayField.container); row.appendChild(urlField.container); // Dimensions section const dimDiv = document.createElement('div'); dimDiv.className = 'emote-dimensions'; // Add preset selector const presetDiv = document.createElement('div'); presetDiv.className = 'emote-presets'; const presetLabel = document.createElement('label'); presetLabel.textContent = 'Preset Size:'; presetLabel.style.fontSize = '12px'; presetLabel.style.color = '#ccc'; presetLabel.style.display = 'block'; presetLabel.style.marginBottom = '4px'; const presetSelect = document.createElement('select'); presetSelect.style.marginBottom = '8px'; const presets = [ { label: 'Custom Size', w: '', h: '', mt: '', t: '' }, { label: 'Standard Emote (28x28)', w: 28, h: 28, mt: -28, t: 7 }, { label: 'Large Emote (32x32)', w: 32, h: 32, mt: -32, t: 7 }, { label: 'Wide Emote (52x30)', w: 52, h: 30, mt: -30, t: 7 }, { label: 'Extra Wide (61x32)', w: 61, h: 32, mt: -32, t: 8 }, { label: 'Small Wide (50x20)', w: 50, h: 20, mt: -20, t: 6 }, { label: 'Medium Wide (40x30)', w: 40, h: 30, mt: -30, t: 7 }, { label: 'Portrait (21x30)', w: 21, h: 30, mt: -30, t: 7 }, { label: 'Medium Portrait (37x30)', w: 37, h: 30, mt: -30, t: 7.5 } ]; presets.forEach(preset => { const option = document.createElement('option'); option.textContent = preset.label; option.value = JSON.stringify(preset); presetSelect.appendChild(option); }); presetSelect.addEventListener('change', () => { const preset = JSON.parse(presetSelect.value); widthInput.value = preset.w; heightInput.value = preset.h; marginTopInput.value = preset.mt; topInput.value = preset.t; saveAllEmoteRows(); }); presetDiv.appendChild(presetLabel); presetDiv.appendChild(presetSelect); // Size inputs const dimensionsLabel = document.createElement('label'); dimensionsLabel.textContent = 'Custom Size:'; dimensionsLabel.style.display = 'block'; dimensionsLabel.style.marginBottom = '4px'; const widthInput = document.createElement('input'); widthInput.type = 'number'; widthInput.placeholder = 'Width'; widthInput.value = data.width || ''; widthInput.addEventListener('input', () => saveAllEmoteRows()); const heightInput = document.createElement('input'); heightInput.type = 'number'; heightInput.placeholder = 'Height'; heightInput.value = data.height || ''; heightInput.addEventListener('input', () => saveAllEmoteRows()); // Position adjustments const positionLabel = document.createElement('label'); positionLabel.textContent = 'Position Adjustment:'; positionLabel.style.display = 'block'; positionLabel.style.marginTop = '8px'; positionLabel.style.marginBottom = '4px'; const posDiv = document.createElement('div'); posDiv.className = 'emote-position'; const marginTopInput = document.createElement('input'); marginTopInput.type = 'number'; marginTopInput.placeholder = 'e.g., -28'; marginTopInput.value = data.marginTop || ''; marginTopInput.title = 'Usually negative height (e.g., -28 for 28px height)'; marginTopInput.addEventListener('input', () => saveAllEmoteRows()); const topInput = document.createElement('input'); topInput.type = 'number'; topInput.placeholder = 'e.g., 7'; topInput.value = data.top || ''; topInput.title = 'Usually height/4 (e.g., 7 for 28px height)'; topInput.addEventListener('input', () => saveAllEmoteRows()); const createInputGroup = (labelText, input) => { const group = document.createElement('div'); group.style.marginBottom = '8px'; const label = document.createElement('label'); label.textContent = labelText; label.style.display = 'block'; label.style.fontSize = '11px'; label.style.color = '#aaa'; label.style.marginBottom = '2px'; group.appendChild(label); group.appendChild(input); return group; }; const marginTopGroup = createInputGroup('Margin Top:', marginTopInput); const topGroup = createInputGroup('Top Offset:', topInput); posDiv.appendChild(marginTopGroup); posDiv.appendChild(topGroup); // Create Advanced Settings section const advancedSection = document.createElement('div'); advancedSection.className = 'emote-advanced-settings'; const toggleBtn = createAdvancedToggle(advancedSection); const advancedLabel = document.createElement('label'); advancedLabel.textContent = 'Advanced Properties:'; advancedLabel.style.display = 'block'; advancedLabel.style.marginBottom = '8px'; advancedLabel.style.fontWeight = 'bold'; const createInput = (label, placeholder, tooltip = '') => { const container = document.createElement('div'); container.className = 'emote-property'; container.style.marginBottom = '8px'; const inputLabel = document.createElement('label'); inputLabel.textContent = label; inputLabel.style.display = 'block'; inputLabel.style.fontSize = '11px'; inputLabel.style.marginBottom = '2px'; inputLabel.style.color = '#aaa'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = placeholder; input.style.width = '100%'; input.title = tooltip; input.addEventListener('input', () => saveAllEmoteRows()); container.appendChild(inputLabel); container.appendChild(input); return container; }; // Basic dimensions and position dimDiv.appendChild(presetDiv); dimDiv.appendChild(dimensionsLabel); dimDiv.appendChild(widthInput); dimDiv.appendChild(heightInput); dimDiv.appendChild(positionLabel); dimDiv.appendChild(posDiv); posDiv.appendChild(marginTopInput); posDiv.appendChild(topInput); // Advanced properties const advancedProps = [ { label: 'Margin Left', placeholder: 'e.g., -2px', tooltip: 'Left margin (can be negative)' }, { label: 'Margin Right', placeholder: 'e.g., -4px', tooltip: 'Right margin (can be negative)' }, { label: 'Transform', placeholder: 'e.g., scaleX(-1)', tooltip: 'CSS transform property (scale, rotate, etc)' }, { label: 'Filter', placeholder: 'e.g., contrast(1) brightness(1.25)', tooltip: 'CSS filters like contrast, brightness, etc' }, { label: 'Background Position', placeholder: 'e.g., 0px or -32px', tooltip: 'For sprite sheets, position of the image' }, { label: 'Background Size', placeholder: 'e.g., 5544px 34px', tooltip: 'Size of background image (for sprite sheets)' }, { label: 'Background Repeat', placeholder: 'e.g., no-repeat', tooltip: 'How the background image repeats' }, { label: 'Transition', placeholder: 'e.g., all 0.2s', tooltip: 'Transition effects for hover states' } ]; advancedSection.appendChild(advancedLabel); advancedProps.forEach(prop => { advancedSection.appendChild(createInput(prop.label, prop.placeholder, prop.tooltip)); }); // Animation Keyframes section const keyframesLabel = document.createElement('label'); keyframesLabel.textContent = 'Animation Keyframes:'; keyframesLabel.style.display = 'block'; keyframesLabel.style.marginBottom = '4px'; keyframesLabel.style.marginTop = '12px'; const keyframesTip = document.createElement('div'); keyframesTip.textContent = 'Define @keyframes animation. For hover effects, create two animations (e.g., myEmote-anim and myEmote-hover)'; keyframesTip.style.fontSize = '11px'; keyframesTip.style.color = '#888'; keyframesTip.style.marginBottom = '8px'; const animationInput = document.createElement('textarea'); animationInput.placeholder = `@keyframes myEmote-anim { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } @keyframes myEmote-hover { 0% { filter: brightness(1); } 100% { filter: brightness(1.5); } }`; animationInput.style.height = '150px'; animationInput.style.fontFamily = 'monospace'; animationInput.value = data.animation || ''; animationInput.addEventListener('input', () => saveAllEmoteRows()); // Create single advanced toggle button const advSettingsToggle = document.createElement('button'); const advSettingsBtn = createAdvancedToggle(advancedSection); dimDiv.appendChild(advSettingsBtn); dimDiv.appendChild(advancedSection); advancedSection.appendChild(keyframesLabel); advancedSection.appendChild(keyframesTip); advancedSection.appendChild(animationInput); const animLabel = document.createElement('label'); animLabel.textContent = 'CSS Animation Keyframes (optional):'; const animTextarea = document.createElement('textarea'); animTextarea.placeholder = '@keyframes example-anim { ... }'; animTextarea.value = data.animation || ''; animTextarea.addEventListener('input', () => saveAllEmoteRows()); // Create basic field labels const searchLabel = document.createElement('label'); searchLabel.textContent = 'Search Name (case-insensitive):'; searchLabel.className = 'label-text'; const displayLabel = document.createElement('label'); displayLabel.textContent = 'Display Name (case-sensitive):'; displayLabel.className = 'label-text'; const urlLabel = document.createElement('label'); urlLabel.textContent = 'Emote URL:'; urlLabel.className = 'label-text'; const dimLabel = document.createElement('label'); dimLabel.textContent = 'Dimensions:'; dimLabel.className = 'label-text'; // Create input fields const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'e.g., itsrawww'; searchInput.value = searchName; const displayInput = document.createElement('input'); displayInput.type = 'text'; displayInput.placeholder = 'e.g., ITSRAWWW'; displayInput.value = data.displayName || ''; const urlInput = document.createElement('input'); urlInput.type = 'text'; urlInput.placeholder = 'https://wikicdn.destiny.gg/...'; urlInput.value = data.url || ''; // Add event listeners searchInput.addEventListener('input', () => saveAllEmoteRows()); displayInput.addEventListener('input', () => saveAllEmoteRows()); urlInput.addEventListener('input', () => saveAllEmoteRows()); // Append elements in order row.appendChild(searchLabel); row.appendChild(searchInput); row.appendChild(displayLabel); row.appendChild(displayInput); row.appendChild(urlLabel); row.appendChild(urlInput); row.appendChild(dimLabel); row.appendChild(dimDiv); row.appendChild(animLabel); row.appendChild(animTextarea); return row; } function addEmoteRow(searchName = '', data = {}) { const row = createEmoteRow(searchName, data); emoteContainer.appendChild(row); } function saveAllEmoteRows() { const rows = emoteContainer.querySelectorAll('.emote-row'); const newEmotes = {}; rows.forEach(row => { const inputs = row.querySelectorAll('input'); const searchName = inputs[0].value.trim().toLowerCase(); const displayName = inputs[1].value.trim(); const url = inputs[2].value.trim(); const width = parseInt(inputs[3].value) || 0; const height = parseInt(inputs[4].value) || 0; const marginTop = parseInt(inputs[5].value) || -height; // Default to -height if not set const top = parseInt(inputs[6].value) || Math.floor(height/4); // Default to height/4 if not set const animation = row.querySelector('textarea').value.trim(); if (searchName && displayName && url && width && height) { const emoteData = { displayName, url, width, height, marginTop, top }; // Add optional properties if they have values const optionalProps = { marginLeft: inputs[7]?.value || null, marginRight: inputs[8]?.value || null, transform: inputs[9]?.value || null, filter: inputs[10]?.value || null, backgroundPosition: inputs[11]?.value || null, backgroundSize: inputs[12]?.value || null, backgroundRepeat: inputs[13]?.value || null, transition: inputs[14]?.value || null, animation: animation || null }; // Only include properties that have values Object.entries(optionalProps).forEach(([key, value]) => { if (value) emoteData[key] = value; }); newEmotes[searchName] = emoteData; } }); customEmotes = newEmotes; GM_setValue('customEmotes', JSON.stringify(customEmotes)); applyCustomEmoteStyles(); processExistingMessagesForEmotes(); } const rebuildEmoteRows = () => { emoteContainer.innerHTML = ''; for (const [searchName, data] of Object.entries(customEmotes)) { addEmoteRow(searchName, data); } }; rebuildEmoteRows(); emoteSection.appendChild(emoteLabel); emoteSection.appendChild(emoteContainer); emoteSection.appendChild(addEmoteButton); exportImportDiv.appendChild(focusCheckboxWrapper); exportImportDiv.appendChild(exportButton); exportImportDiv.appendChild(importButton); content.appendChild(exportImportDiv); content.appendChild(label1); content.appendChild(textarea1); content.appendChild(label2); content.appendChild(textarea2); content.appendChild(label3); content.appendChild(textarea3); content.appendChild(label4); content.appendChild(flairContainer); content.appendChild(addButton); content.appendChild(emoteSection); content.appendChild(removalSection); dropdownContainer.appendChild(button); dropdownContainer.appendChild(content); button.addEventListener('click', (e) => { e.stopPropagation(); const isDisplayed = content.style.display === 'block'; document.querySelectorAll('.class-settings-content, .class-skip-content').forEach(p => { if (p.style.display === 'block') { p.style.display = 'none'; } }); content.style.display = isDisplayed ? 'none' : 'block'; }); document.addEventListener('click', (e) => { if (!dropdownContainer.contains(e.target)) { content.style.display = 'none'; } }); const existingSettings = settingsContainer.querySelector('.settings-dropdown'); if (existingSettings) { existingSettings.parentElement.insertBefore(dropdownContainer, existingSettings); } else { settingsContainer.appendChild(dropdownContainer); } } // ======================================== // FLAIR BLACKLIST (REMOVE FLAIRS FROM CHAT) // ======================================== function stripFlairsFromMessage(msg) { const username = msg.getAttribute('data-username'); if (!username) return; const lowerUsername = username.toLowerCase(); if (!flairBlacklist[lowerUsername]) return; const flairsToRemove = flairBlacklist[lowerUsername]; const featuresSpan = msg.querySelector('.features'); const userSpan = msg.querySelector('span.user'); if (featuresSpan) { flairsToRemove.forEach(flairClass => { const flairIcon = featuresSpan.querySelector(`i.flair.${flairClass}`); if (flairIcon) { flairIcon.remove(); } }); } if (userSpan) { flairsToRemove.forEach(flairClass => { if (userSpan.classList.contains(flairClass)) { userSpan.classList.remove(flairClass); } }); } } function processExistingMessagesForFlairRemoval() { const messages = document.querySelectorAll('#chat-stream .msg-chat'); messages.forEach(stripFlairsFromMessage); } // ======================================== // CUSTOM FLAIRS // ======================================== const customFlairs = { flair4: "https://wikicdn.destiny.gg/8/87/Flair_4.png", flair5: "https://wikicdn.destiny.gg/5/5e/Flair_5.png", flair10: "https://wikicdn.destiny.gg/6/61/Flair_starcraft_2.png", flair16: "https://wikicdn.destiny.gg/d/df/Flair_emote_contributor.png", flair25: "https://cdn.destiny.gg/flairs/5f28cdec6ed24.png", flair27: "https://cdn.destiny.gg/flairs/60b130f07c0c4.png", flair30: "https://wikicdn.destiny.gg/c/ca/Flair_league_master.png" }; const defaultFlairDefinitions = { 'flair4': 'Trusted', 'flair5': 'Contributor', 'flair10': 'Starcraft Player', 'flair16': 'Emote Contributor', 'flair25': 'League Champion', 'flair27': 'Premium Subscriber', 'flair30': 'League Master' }; // UPDATED: Flair color mappings from flairs.css // This ensures username colors are applied correctly const flairColors = { 'flair1': '#2ADDC8', // T2 Sub 'flair3': '#4DB524', // T3 Sub 'flair7': '#FC4C02', 'flair8': '#DD29D2', // T4 Sub 'flair9': '#5900ff', // Twitch Sub 'flair11': '#929292', 'flair12': '#E79015', 'flair13': '#59AEEA', // T1 Sub 'flair17': '#FCE205', 'flair18': '#f082bb', 'flair22': '#2ADDC8', // T2 AE Sub 'flair24': '#4DB524', // T3 AE Sub 'flair26': '#DD29D2', // T4 AE Sub 'flair32': '#59AEEA', // T1 AE Sub 'bot': '#0088CC', 'admin': '#EE1F1F', 'moderator': '#DB4C1C', 'subscriber': '#59AEEA', 'vip': '#ffbb00' }; function applyCustomFlairs() { let styleOverrides = ''; // Base rainbow gradient definition styleOverrides += ` :root { --rainbow-saturation: 100%; --rainbow-lightness: 65%; --rainbow-gradient: repeating-linear-gradient( 90deg, hsl(0, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(45, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(90, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(135, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(180, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(225, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(270, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(315, var(--rainbow-saturation), var(--rainbow-lightness)), hsl(360, var(--rainbow-saturation), var(--rainbow-lightness)) 50% ); } @keyframes move { to { background-position-x: -100%; } } `; // Apply custom flair styles first for (const [flair, url] of Object.entries(customFlairs)) { styleOverrides += ` i.flair.${flair}, i.flair.${flair}.${flair} { display: inline-block !important; background-image: url("${url}") !important; background-size: 16px 16px !important; background-repeat: no-repeat !important; background-position: center !important; width: 16px !important; height: 16px !important; margin: 0 1px !important; vertical-align: middle !important; } `; } // Order flairs by destiny.gg's ordering (lower numbers appear first) const flairPriority = { 'admin': 1, // Admin (highest priority) 'moderator': 1, // Moderator 'flair18': 2, // Emote Manager 'flair12': 3, // Broadcaster 'flair20': 3, // Verified 'flair7': 3, // NFL Chatter 'flair33': 2, // T5 AE Sub (higher priority) 'flair42': 2, // T5 Sub (higher priority) 'flair26': 4, // T4 AE Sub 'flair8': 4, // T4 Sub 'flair24': 5, // T3 AE Sub 'flair3': 5, // T3 Sub 'flair22': 6, // T2 AE Sub 'flair1': 6, // T2 Sub 'flair32': 7, // T1 AE Sub 'flair13': 7, // T1 Sub 'subscriber': 8, // Regular subscriber 'flair9': 8, // Twitch subscriber 'bot': 10, // Bot 'flair11': 11, // Old Bot 'flair31': 50, // Developer 'flair23': 50, // Doctor 'flair28': 50, // Lawyer 'flair34': 50, // Conductor 'flair29': 50, // Weight Lifting 'flair58': 50, // New User 'flair2': 127, // Notable Chatter 'flair15': 127, // Birthday 'flair17': 127, // Extra Notable Chatter 'protected': 127 // Protected }; // Sort flairs by priority before creating styles const sortedFlairs = Object.entries(flairColors) .sort(([a], [b]) => (flairPriority[b] || 0) - (flairPriority[a] || 0)); // Create style rules in priority order for (const [flair, color] of sortedFlairs) { const priority = flairPriority[flair] || 0; styleOverrides += ` .user.${flair} { color: ${color} !important; --flair-priority: ${priority}; } i.flair.${flair} { z-index: ${100 - priority} !important; position: relative !important; } `; } // Special handling for rainbow flairs styleOverrides += ` .flair.flair33, .flair.flair42 { background-image: url("https://cdn.destiny.gg/flairs/66465c372e9c9.png"); height: 19px; width: 18px; order: 3; } .user.flair33, .user.flair42 { color: transparent; background: var(--rainbow-gradient); background-clip: text; background-size: 200% 100%; -webkit-background-clip: text; animation: move 3s linear infinite; position: relative; order: 3; } `; GM_addStyle(styleOverrides); } function injectFlair34(msg) { const username = msg.getAttribute('data-username'); if (!username) return; if (!flair34Users.includes(username.toLowerCase())) return; const featuresSpan = msg.querySelector('.features'); if (!featuresSpan) return; if (featuresSpan.querySelector('i.flair.flair34')) return; const flair34 = document.createElement('i'); flair34.setAttribute('data-flair', 'flair34'); flair34.setAttribute('data-injected', 'true'); flair34.className = 'flair flair34'; flair34.title = 'Conductor'; featuresSpan.appendChild(flair34); } function injectVIP(msg) { const username = msg.getAttribute('data-username'); if (!username || !vipUsers.includes(username.toLowerCase())) return; const userSpan = msg.querySelector('span.user'); if (userSpan && !userSpan.classList.contains('vip')) { userSpan.classList.add('vip'); } } function injectCustomFlair(msg) { const username = msg.getAttribute('data-username'); if (!username) return; const lowerUsername = username.toLowerCase(); // Find or create features span let featuresSpan = msg.querySelector('.features'); if (!featuresSpan) { featuresSpan = document.createElement('span'); featuresSpan.className = 'features'; // Insert after time-seconds const timeSeconds = msg.querySelector('.time-seconds'); if (timeSeconds && timeSeconds.nextSibling) { msg.insertBefore(featuresSpan, timeSeconds.nextSibling); } } // Find or create user span let userSpan = msg.querySelector('.user'); if (!userSpan) { userSpan = document.createElement('span'); userSpan.className = 'user'; userSpan.setAttribute('onclick', 'document._addFocusRule("' + username + '")'); // Add after features span if (featuresSpan.nextSibling) { msg.insertBefore(userSpan, featuresSpan.nextSibling); } else { msg.appendChild(userSpan); } } // Store existing flairs before clearing const existingFlairs = []; featuresSpan.querySelectorAll('i.flair').forEach(flair => { const classes = Array.from(flair.classList); const flairClass = classes.find(cls => cls !== 'flair'); if (flairClass) existingFlairs.push(flairClass); }); // Store existing user classes const existingUserClasses = Array.from(userSpan.classList) .filter(cls => cls.startsWith('flair') || ['admin', 'moderator', 'subscriber', 'protected', 'bot', 'vip'].includes(cls)); // Clear existing flairs featuresSpan.innerHTML = ''; // Always add user-[username] class and preserve username const currentText = userSpan.textContent; userSpan.className = `user-${username} user`; userSpan.textContent = currentText || username; // Function to add a flair const addFlair = (flairClass) => { // Add flair icon if it doesn't exist if (!featuresSpan.querySelector(`i.flair.${flairClass}`)) { const flairElement = document.createElement('i'); flairElement.className = `flair ${flairClass}`; flairElement.title = flairDefinitions[flairClass] || flairClass; featuresSpan.appendChild(flairElement); } // Add class to username if not present if (!userSpan.classList.contains(flairClass)) { userSpan.classList.add(flairClass); } }; // First, restore existing flairs existingFlairs.forEach(flairClass => { addFlair(flairClass); }); // Restore existing user classes existingUserClasses.forEach(className => { if (!userSpan.classList.contains(className)) { userSpan.classList.add(className); } }); // Then handle custom flairs for (const [flairKey, usersList] of Object.entries(selectedFlairUsers)) { if (Array.isArray(usersList) && usersList.includes(lowerUsername)) { addFlair(flairKey); } } } function processExistingMessagesForFlairs() { const messages = document.querySelectorAll('#chat-stream .msg-chat'); messages.forEach(msg => { // Don't clear existing flairs, just apply custom ones injectCustomFlair(msg); }); } // ======================================== // FOCUS ENHANCEMENT - INCLUDE MENTIONS // ======================================== function updateFocusRules() { // Remove any existing scripts first const existingScript = document.getElementById('focus-override-script'); if (existingScript) { existingScript.remove(); } // Add the base styles once GM_addStyle(` #chat-stream.hide-on-focus .msg-chat { opacity: 0.3 !important; } #chat-stream.hide-on-focus .msg-chat[data-focused="true"], #chat-stream.hide-on-focus .msg-chat[data-mentioned="true"] { opacity: 1 !important; position: relative !important; z-index: 1 !important; } `); // Create and add our focus handling script const focusScript = document.createElement('script'); focusScript.id = 'focus-override-script'; focusScript.textContent = ` document.addEventListener('click', function(event) { const target = event.target; // Handle user click if (target.classList.contains('user')) { const username = target.textContent; console.log('User clicked:', username); // Clear previous focus document.querySelectorAll('#chat-stream .msg-chat[data-focused="true"], #chat-stream .msg-chat[data-mentioned="true"]') .forEach(msg => { msg.removeAttribute('data-focused'); msg.removeAttribute('data-mentioned'); }); // Focus user's messages document.querySelectorAll(\`#chat-stream .msg-chat[data-username="\${username}"]\`) .forEach(msg => { msg.setAttribute('data-focused', 'true'); console.log('Focused message:', msg.textContent); }); // Check for mentions if enabled const includeMentions = document.getElementById('includeMentionsCheckbox')?.checked; console.log('Include mentions:', includeMentions); if (includeMentions) { document.querySelectorAll('#chat-stream .msg-chat') .forEach(msg => { if (msg.getAttribute('data-username') !== username) { // Get all the text content including emote titles const messageSpan = msg.querySelector('.message'); if (messageSpan) { let textContent = ''; messageSpan.childNodes.forEach(node => { if (node.nodeType === 3) { // Text node textContent += node.textContent + ' '; } else if (node.nodeType === 1 && node.classList.contains('emote')) { // Add emote title/alt text if available textContent += (node.title || node.alt || '') + ' '; } }); textContent = textContent.trim(); console.log('Checking message text:', textContent); if (textContent.toLowerCase().includes(username.toLowerCase())) { msg.setAttribute('data-mentioned', 'true'); console.log('Found mention in:', textContent); } } } }); } // Add hide-on-focus class to chat stream document.getElementById('chat-stream').classList.add('hide-on-focus'); event.stopPropagation(); } // Handle message click (remove focus) else if (target.classList.contains('message')) { document.querySelectorAll('#chat-stream .msg-chat[data-focused="true"], #chat-stream .msg-chat[data-mentioned="true"]') .forEach(msg => { msg.removeAttribute('data-focused'); msg.removeAttribute('data-mentioned'); }); document.getElementById('chat-stream').classList.remove('hide-on-focus'); } }, true); `; document.head.appendChild(focusScript); } function setupMentionObserver() { const chatContainer = document.querySelector('#chat-stream'); if (!chatContainer) return; const mentionObserver = new MutationObserver((mutations) => { const script = document.createElement('script'); script.textContent = ` (function() { if (window.focused) { // Check the checkbox state dynamically const checkbox = document.getElementById('includeMentionsCheckbox'); if (!checkbox || !checkbox.checked) return; const username = window.focused; const messages = document.querySelectorAll('#chat-stream .msg-chat:not(.mention-focused):not([data-username="' + username + '"])'); messages.forEach(msg => { const messageSpan = msg.querySelector('.message'); if (messageSpan) { const text = messageSpan.innerText || messageSpan.textContent || ''; const regex = new RegExp('\\\\b' + username + '\\\\b', 'i'); if (regex.test(text)) { msg.classList.add('mention-focused'); console.log('Added mention-focused to new message from', msg.getAttribute('data-username'), ':', text.substring(0, 50)); } } }); } })(); `; document.head.appendChild(script); script.remove(); }); mentionObserver.observe(chatContainer, { childList: true }); } // ======================================== // UNIFIED OBSERVER // ======================================== function setupUnifiedObserver() { const chatContainer = document.querySelector('#chat-stream'); if (!chatContainer) { console.warn('Vyneer.me chat container (#chat-stream) not found, retrying...'); setTimeout(setupUnifiedObserver, 1000); return; } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === 1 && addedNode.classList?.contains('msg-chat')) { // Process flairs first injectCustomFlair(addedNode); // Then process emotes const messageSpan = addedNode.querySelector('.message'); if (messageSpan) { processMessageSpanForEmotes(messageSpan); // Check for mentions if a user is focused if (chatContainer.classList.contains('hide-on-focus')) { const focusedMsg = document.querySelector('#chat-stream .msg-chat[data-focused="true"]'); const focusedUsername = focusedMsg?.getAttribute('data-username'); if (focusedUsername && document.getElementById('includeMentionsCheckbox')?.checked) { if (addedNode.getAttribute('data-username') !== focusedUsername) { let textContent = ''; messageSpan.childNodes.forEach(node => { if (node.nodeType === 3) { // Text node textContent += node.textContent + ' '; } else if (node.nodeType === 1 && node.classList.contains('emote')) { textContent += (node.title || node.alt || '') + ' '; } }); textContent = textContent.trim(); if (textContent.toLowerCase().includes(focusedUsername.toLowerCase())) { addedNode.setAttribute('data-mentioned', 'true'); } } } } } // Process other features processMessageForHighlight(addedNode); stripFlairsFromMessage(addedNode); injectFlair34(addedNode); injectVIP(addedNode); } } } } }); observer.observe(chatContainer, { childList: true }); } // ======================================== // INITIALIZATION // ======================================== function init() { console.log('=== Vyneer.me Enhancement Suite Initializing ==='); applyCustomFlairs(); loadKeywords(); loadFlair34Users(); loadVipUsers(); loadSelectedFlairUsers(); loadFlairBlacklist(); loadCustomEmotes(); loadIncludeMentionsInFocus(); setTimeout(() => { console.log('Processing existing messages...'); applyCustomEmoteStyles(); processExistingMessagesForEmotes(); processExistingMessagesForHighlight(); processExistingMessagesForFlairRemoval(); processExistingMessagesForFlairs(); setupUnifiedObserver(); setupMentionObserver(); updateFocusRules(); setupSettingsUI(); }, 1000); } if (document.readyState === 'complete') { setTimeout(init, 500); } else { window.addEventListener('load', function() { setTimeout(init, 500); }); } })();