Scroll the KICK comments to the screen.
// ==UserScript== // @name Kickコメントスクロール, Kick弾幕, Kick Comment Scroller // @namespace http://tampermonkey.net/ // @version 1.5 // @description Kickで弾幕を表示 // @match https://kick.com/* // @license MIT // @grant none // @run-at document-end // @name:en Kick Comment Scroller // @description:en Scroll the KICK comments to the screen. // @homepageURL https://github.com/XBACT/Kick.com-Comment-Scroller/ // @supportURL https://github.com/XBACT/Kick.com-Comment-Scroller/issues // ==/UserScript== (function() { 'use strict'; const translations = { ja: { title: "設定", duration: "通過時間 (秒)", fontSize: "フォントサイズ (px)", fontFamily: "フォントファミリー", opacity: "透明度", strokeWidth: "縁の太さ (px)", strokeOpacity: "縁の透明度", strokeColor: "縁の色", textColor: "テキストの色", lineSpacing: "行間(フォントサイズに対する割合)", blockEmoji: "絵文字を表示", fontWeight: "フォントの太さ", ngComments: "NGコメントリスト (カンマ区切り)", ngRegex: "正規表現フィルタ (オプション)", useUsernameColor: "ユーザーのグローバル名の色を使用", close: "閉じる", clearComments: "コメントを削除", maxComments: "最大表示数", unlimitedMaxComments: "最大表示数を無制限にする", overlapComments: "コメントを重ねる", language: "言語" }, en: { title: "Settings", duration: "Duration (seconds)", fontSize: "Font Size (px)", fontFamily: "Font Family", opacity: "Opacity", strokeWidth: "Stroke Width (px)", strokeOpacity: "Stroke Opacity", strokeColor: "Stroke Color", textColor: "Text Color", lineSpacing: "Line Spacing (ratio to font size)", blockEmoji: "Show Emojis", fontWeight: "Font Weight", ngComments: "Blocked Comments (comma-separated)", ngRegex: "Regex Filter (Optional)", useUsernameColor: "Use Username Color", close: "Close", clearComments: "Clear Comments", maxComments: "Max Comments", unlimitedMaxComments: "Unlimited Max Comments", overlapComments: "Overlap Comments", language: "Language" } }; const commentQueue = []; let settings = { duration: 5, fontSize: '75px', fontWeight: 'normal', fontFamily: "'SM P ゴシック', sans-serif", strokeWidth: '3.5px', strokeOpacity: 0.1, strokeColor: '#000000', textColor: '#ffffff', opacity: 1, blockEmoji: false, lineSpacing: 0, ngComments: '', ngRegex: '', useUsernameColor: false, language: 'ja', maxComments: 50, unlimitedMaxComments: true, overlapComments: false }; try { if (localStorage.getItem('kickCommentScrollerSettings')) { settings = JSON.parse(localStorage.getItem('kickCommentScrollerSettings')); if (!settings.language || !translations[settings.language]) { settings.language = 'ja'; } } } catch (e) {} const currentUrl = window.location.href; const isUserPage = /^https:\/\/kick\.com\/[a-zA-Z0-9_-]+/.test(currentUrl); let scrollContainer = document.createElement('div'); scrollContainer.id = 'kickCommentScrollerContainer'; Object.assign(scrollContainer.style, { position: 'absolute', pointerEvents: 'none', zIndex: '9999', overflow: 'hidden', top: '0px', left: '0px', width: '100%', height: '100%' }); let settingsPanel = document.createElement('div'); settingsPanel.id = 'kickCommentScrollerSettings'; Object.assign(settingsPanel.style, { position: 'absolute', zIndex: '10000', backgroundColor: 'rgba(0, 0, 0, 0.7)', color: '#fff', padding: '10px', borderRadius: '5px', fontSize: '14px', display: 'none', visibility: 'visible', opacity: '1' }); let settingsButton = document.createElement('button'); settingsButton.id = 'kickCommentScrollerButton'; settingsButton.textContent = '設定'; Object.assign(settingsButton.style, { position: 'fixed', zIndex: '10000', bottom: '11px', right: '149px', padding: '5px 10px', backgroundColor: '#333', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontFamily: "'Inter', sans-serif", fontWeight: '600', display: 'block', visibility: 'visible', opacity: '1' }); let lastUrl = window.location.href; let currentObserver = null; function resetObserver() { if (currentObserver) { currentObserver.disconnect(); } const chatContainer = findChatContainer(); if (chatContainer) { currentObserver = setupObserver(chatContainer); } } function monitorUrlChange() { const newUrl = window.location.href; if (newUrl !== lastUrl) { lastUrl = newUrl; resetObserver(); updateScrollContainer(); } setTimeout(monitorUrlChange, 1000); } settingsButton.addEventListener('click', () => { try { if (settingsPanel.classList.contains('visible')) { settingsPanel.classList.remove('visible'); settingsPanel.style.display = 'none'; } else { settingsPanel.classList.add('visible'); settingsPanel.style.display = 'block'; setPanelPosition(); } } catch (e) {} }); let styleSheet = document.createElement('style'); document.head.appendChild(styleSheet); let settingsPanelInitialized = false; function updateStylesheet() { styleSheet.textContent = ` #kickCommentScrollerContainer span > img { display: inline-block !important; vertical-align: middle !important; margin: 0 2px !important; object-fit: contain !important; height: ${settings.fontSize} !important; width: ${settings.fontSize} !important; max-height: ${settings.fontSize} !important; max-width: ${settings.fontSize} !important; } #kickCommentScrollerButton { display: block !important; visibility: visible !important; opacity: 1 !important; } #kickCommentScrollerSettings { display: none; visibility: visible !important; opacity: 1 !important; } #kickCommentScrollerSettings.visible { display: block !important; } #kickCommentScrollerSettings h3 { cursor: move; margin: 0 0 10px 0; padding: 5px; background-color: rgba(255, 255, 255, 0.1); } `; } updateStylesheet(); function createSettingsPanel() { if (settingsPanelInitialized) return; try { const t = translations[settings.language] || translations['ja']; settingsPanel.innerHTML = ` <h3>${t.title}</h3> <label>${t.duration}: <input type="range" id="duration" min="1" max="10" step="0.5" value="${settings.duration}"><span id="durationValue">${settings.duration}</span></label><br> <label>${t.fontSize}: <input type="range" id="fontSize" min="12" max="100" step="1" value="${parseInt(settings.fontSize)}"><span id="fontSizeValue">${parseInt(settings.fontSize)}</span></label><br> <label>${t.fontFamily}: <select id="fontFamily"> <option value="'SM P ゴシック', sans-serif" ${settings.fontFamily === "'SM P ゴシック', sans-serif" ? 'selected' : ''}>SM P ゴシック</option> <option value="'Arial', sans-serif" ${settings.fontFamily === "'Arial', sans-serif" ? 'selected' : ''}>Arial</option> <option value="'Helvetica', sans-serif" ${settings.fontFamily === "'Helvetica', sans-serif" ? 'selected' : ''}>Helvetica</option> <option value="'Times New Roman', serif" ${settings.fontFamily === "'Times New Roman', serif" ? 'selected' : ''}>Times New Roman</option> <option value="'Courier New', monospace" ${settings.fontFamily === "'Courier New', monospace" ? 'selected' : ''}>Courier New</option> </select> </label><br> <label>${t.opacity}: <input type="range" id="opacity" min="0" max="1" step="0.1" value="${settings.opacity}"><span id="opacityValue">${settings.opacity}</span></label><br> <label>${t.strokeWidth}: <input type="range" id="strokeWidth" min="0" max="10" step="0.5" value="${parseFloat(settings.strokeWidth)}"><span id="strokeWidthValue">${parseFloat(settings.strokeWidth)}</span></label><br> <label>${t.strokeOpacity}: <input type="range" id="strokeOpacity" min="0" max="1" step="0.1" value="${settings.strokeOpacity}"><span id="strokeOpacityValue">${settings.strokeOpacity}</span></label><br> <label>${t.strokeColor}: <input type="color" id="strokeColor" value="${settings.strokeColor}"><span id="strokeColorValue">${settings.strokeColor}</span></label><br> <label>${t.textColor}: <input type="color" id="textColor" value="${settings.textColor}"><span id="textColorValue">${settings.textColor}</span></label><br> <label>${t.useUsernameColor}: <input type="checkbox" id="useUsernameColor" ${settings.useUsernameColor ? 'checked' : ''}></label><br> <label>${t.lineSpacing}: <input type="range" id="lineSpacing" min="0" max="1" step="0.1" value="${settings.lineSpacing}"><span id="lineSpacingValue">${settings.lineSpacing.toFixed(1)}</span></label><br> <label>${t.blockEmoji}: <input type="checkbox" id="blockEmoji" ${settings.blockEmoji ? '' : 'checked'}></label><br> <label>${t.fontWeight}: <select id="fontWeight"> <option value="normal" ${settings.fontWeight === 'normal' ? 'selected' : ''}>${settings.language === 'ja' ? '標準' : 'Normal'}</option> <option value="bold" ${settings.fontWeight === 'bold' ? 'selected' : ''}>${settings.language === 'ja' ? '太字' : 'Bold'}</option> <option value="700" ${settings.fontWeight === '700' ? 'selected' : ''}>700</option> <option value="900" ${settings.fontWeight === '900' ? 'selected' : ''}>900</option> </select> </label><br> <label>${t.ngComments}: <input type="text" id="ngComments" value="${settings.ngComments}" placeholder="${settings.language === 'ja' ? '例: スパム,広告,NGワード' : 'e.g., spam,ad,NGword'}"></label><br> <label>${t.ngRegex}: <input type="text" id="ngRegex" value="${settings.ngRegex}" placeholder="${settings.language === 'ja' ? '例: ^(spam|ad)$' : 'e.g., ^(spam|ad)$'}"></label><br> <label>${t.maxComments}: <input type="range" id="maxComments" min="10" max="100" step="5" value="${settings.maxComments}" ${settings.unlimitedMaxComments ? 'disabled' : ''}><span id="maxCommentsValue">${settings.maxComments}</span></label><br> <label>${t.unlimitedMaxComments}: <input type="checkbox" id="unlimitedMaxComments" ${settings.unlimitedMaxComments ? 'checked' : ''}></label><br> <label>${t.overlapComments}: <input type="checkbox" id="overlapComments" ${settings.overlapComments ? 'checked' : ''}></label><br> <label>${t.language}: <select id="language"> <option value="ja" ${settings.language === 'ja' ? 'selected' : ''}>日本語</option> <option value="en" ${settings.language === 'en' ? 'selected' : ''}>English</option> </select> </label><br> <button id="closeSettings">${t.close}</button> <button id="clearComments">${t.clearComments}</button> `; document.getElementById('duration').addEventListener('input', (e) => { settings.duration = parseFloat(e.target.value); document.getElementById('durationValue').textContent = settings.duration; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('fontSize').addEventListener('input', (e) => { settings.fontSize = `${parseInt(e.target.value)}px`; document.getElementById('fontSizeValue').textContent = parseInt(e.target.value); localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); updateStylesheet(); }); document.getElementById('fontFamily').addEventListener('change', (e) => { settings.fontFamily = e.target.value; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('opacity').addEventListener('input', (e) => { settings.opacity = parseFloat(e.target.value); document.getElementById('opacityValue').textContent = settings.opacity.toFixed(1); localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('strokeWidth').addEventListener('input', (e) => { settings.strokeWidth = `${parseFloat(e.target.value)}px`; document.getElementById('strokeWidthValue').textContent = parseFloat(e.target.value); localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('strokeOpacity').addEventListener('input', (e) => { settings.strokeOpacity = parseFloat(e.target.value); document.getElementById('strokeOpacityValue').textContent = settings.strokeOpacity.toFixed(1); localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('strokeColor').addEventListener('input', (e) => { settings.strokeColor = e.target.value; document.getElementById('strokeColorValue').textContent = settings.strokeColor; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('textColor').addEventListener('input', (e) => { settings.textColor = e.target.value; document.getElementById('textColorValue').textContent = settings.textColor; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('useUsernameColor').addEventListener('change', (e) => { settings.useUsernameColor = e.target.checked; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('lineSpacing').addEventListener('input', (e) => { settings.lineSpacing = parseFloat(e.target.value); document.getElementById('lineSpacingValue').textContent = settings.lineSpacing.toFixed(1); localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('blockEmoji').addEventListener('change', (e) => { settings.blockEmoji = !e.target.checked; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('fontWeight').addEventListener('change', (e) => { settings.fontWeight = e.target.value; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('ngComments').addEventListener('input', (e) => { settings.ngComments = e.target.value; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('ngRegex').addEventListener('input', (e) => { settings.ngRegex = e.target.value; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('maxComments').addEventListener('input', (e) => { settings.maxComments = parseInt(e.target.value); document.getElementById('maxCommentsValue').textContent = settings.maxComments; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('unlimitedMaxComments').addEventListener('change', (e) => { settings.unlimitedMaxComments = e.target.checked; document.getElementById('maxComments').disabled = settings.unlimitedMaxComments; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('overlapComments').addEventListener('change', (e) => { settings.overlapComments = e.target.checked; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); }); document.getElementById('language').addEventListener('change', (e) => { settings.language = e.target.value; localStorage.setItem('kickCommentScrollerSettings', JSON.stringify(settings)); settingsPanelInitialized = false; createSettingsPanel(); }); document.getElementById('closeSettings').addEventListener('click', () => { settingsPanel.classList.remove('visible'); settingsPanel.style.display = 'none'; }); document.getElementById('clearComments').addEventListener('click', () => { while (scrollContainer.firstChild) { scrollContainer.removeChild(scrollContainer.firstChild); } displayedComments.clear(); usedHeights.length = 0; }); const header = settingsPanel.querySelector('h3'); header.addEventListener('mousedown', (e) => { let shiftX = e.clientX - settingsPanel.getBoundingClientRect().left; let shiftY = e.clientY - settingsPanel.getBoundingClientRect().top; function moveAt(pageX, pageY) { settingsPanel.style.left = pageX - shiftX + 'px'; settingsPanel.style.top = pageY - shiftY + 'px'; } function onMouseMove(event) { moveAt(event.pageX, event.pageY); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', onMouseMove); }, { once: true }); }); settingsPanel.ondragstart = () => false; settingsPanelInitialized = true; } catch (e) {} } function ensurePanelInDOM() { try { if (!document.getElementById('kickCommentScrollerSettings') && document.body) { document.body.appendChild(settingsPanel); } } catch (e) {} } function ensureButtonInDOM() { try { if (!document.getElementById('kickCommentScrollerButton') && document.body) { document.body.appendChild(settingsButton); } } catch (e) {} } function ensurePanelAndContent() { try { ensurePanelInDOM(); if (settingsPanel.innerHTML.trim() === '' || !settingsPanelInitialized) { createSettingsPanel(); } } catch (e) {} } setInterval(ensurePanelAndContent, 5000); function ensureSettingsButton() { try { ensureButtonInDOM(); } catch (e) {} } setInterval(ensureSettingsButton, 5000); function getVideoFrame() { try { let videoFrame = document.querySelector('div.relative.aspect-video.w-full') || document.querySelector('div[id*="amazon-ivs-player"]')?.parentElement || document.querySelector('video')?.parentElement?.parentElement || document.querySelector('div[class*="video-player"]') || document.querySelector('div[class*="player-container"]') || document.body; if (!videoFrame || !videoFrame.getBoundingClientRect) { videoFrame = { offsetTop: 0, offsetLeft: 0, offsetWidth: window.innerWidth, offsetHeight: window.innerHeight, top: 0, left: 0, width: window.innerWidth, height: window.innerHeight, getBoundingClientRect: () => ({ top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }) }; } return videoFrame; } catch (e) { return { offsetTop: 0, offsetLeft: 0, offsetWidth: window.innerWidth, offsetHeight: window.innerHeight, top: 0, left: 0, width: window.innerWidth, height: window.innerHeight, getBoundingClientRect: () => ({ top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }) }; } } function setPanelPosition() { try { const checkPosition = () => { if (settingsPanel.offsetWidth === 0 || settingsPanel.offsetHeight === 0) { setTimeout(checkPosition, 1); return; } const rect = getVideoFrame().getBoundingClientRect ? getVideoFrame().getBoundingClientRect() : getVideoFrame(); const top = rect.top + window.scrollY + 10; const left = Math.max(10, rect.left + window.scrollX + 10); Object.assign(settingsPanel.style, { top: `${top}px`, left: `${left}px` }); }; checkPosition(); } catch (e) {} } function updateScrollContainer() { try { const videoFrame = getVideoFrame(); if (videoFrame && !document.getElementById('kickCommentScrollerContainer')) { videoFrame.appendChild ? videoFrame.appendChild(scrollContainer) : document.body.appendChild(scrollContainer); } else if (videoFrame) { const rect = videoFrame.getBoundingClientRect ? videoFrame.getBoundingClientRect() : videoFrame; Object.assign(scrollContainer.style, { top: '0px', left: '0px', width: `${rect.width}px`, height: `${rect.height}px` }); } if (settingsPanel.classList.contains('visible')) { setPanelPosition(); } } catch (e) {} } const displayedComments = new Set(); const usedHeights = []; const MAX_HEIGHTS = 10; function getNextHeight(frameHeight, commentHeight) { try { const maxHeight = frameHeight - commentHeight; if (maxHeight < 0) return 0; if (usedHeights.length === 0) { usedHeights.push(0); return 0; } const fontSizePx = parseInt(settings.fontSize); const scaledLineSpacing = fontSizePx * settings.lineSpacing; const minSpacing = Math.max(fontSizePx, commentHeight); const totalSpacing = minSpacing + scaledLineSpacing; if (settings.overlapComments) { const randomHeight = Math.floor(Math.random() * maxHeight); usedHeights.push(randomHeight); return randomHeight; } else { for (let i = 0; i <= maxHeight / totalSpacing; i++) { const height = i * totalSpacing; const topPosition = height % maxHeight; if (!usedHeights.some(h => Math.abs(h - topPosition) < minSpacing)) { usedHeights.push(topPosition); return topPosition; } } return Math.floor(Math.random() * maxHeight); } } catch (e) { return 0; } } function applyStrokeEffect(element) { try { const w = parseFloat(settings.strokeWidth) || 2; const strokeColor = `${settings.strokeColor}${Math.round((settings.strokeOpacity || 0.8) * 255).toString(16).padStart(2, '0')}`; const shadow = ` ${w}px ${w}px 0 ${strokeColor}, ${-w}px ${w}px 0 ${strokeColor}, ${w}px ${-w}px 0 ${strokeColor}, ${-w}px ${-w}px 0 ${strokeColor}, ${w}px 0px 0 ${strokeColor}, ${-w}px 0px 0 ${strokeColor}, 0px ${w}px 0 ${strokeColor}, 0px ${-w}px 0 ${strokeColor} `; element.style.textShadow = shadow; } catch (e) {} } function isNgComment(commentText) { try { if (settings.ngComments) { const ngList = settings.ngComments.split(',').map(word => word.trim()).filter(word => word); if (ngList.some(word => commentText.includes(word))) return true; } if (settings.ngRegex) { const regex = new RegExp(settings.ngRegex, 'i'); if (regex.test(commentText)) return true; } return false; } catch (e) { return false; } } function scrollComment(commentText, imgElements = [], uniqueId, usernameColor = null) { try { if (!commentText && imgElements.length === 0 || isNgComment(commentText)) return; if (!settings.unlimitedMaxComments && displayedComments.size >= settings.maxComments) { commentQueue.push({ commentText, imgElements, uniqueId, usernameColor }); return; } displayedComments.add(uniqueId); const videoFrame = getVideoFrame(); const rect = videoFrame.getBoundingClientRect ? videoFrame.getBoundingClientRect() : videoFrame; const frameWidth = rect.width; const frameHeight = rect.height; const scrollComment = document.createElement('span'); const textColor = settings.useUsernameColor && usernameColor ? usernameColor : settings.textColor; Object.assign(scrollComment.style, { position: 'absolute', color: textColor, fontSize: settings.fontSize, fontFamily: settings.fontFamily, fontWeight: settings.fontWeight, opacity: settings.opacity, whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', lineHeight: settings.fontSize, height: settings.fontSize }); applyStrokeEffect(scrollComment); if (commentText && commentText.trim()) { scrollComment.appendChild(document.createTextNode(commentText)); } if (imgElements.length > 0 && !settings.blockEmoji) { imgElements.forEach(img => { const clonedImg = document.createElement('img'); clonedImg.src = img.src; Object.assign(clonedImg.style, { height: settings.fontSize, width: settings.fontSize, objectFit: 'contain', verticalAlign: 'middle', margin: '0 2px', display: 'inline-block' }); scrollComment.appendChild(clonedImg); }); } scrollContainer.appendChild(scrollComment); const commentWidth = scrollComment.offsetWidth; const commentHeight = scrollComment.offsetHeight; scrollComment.style.height = `${commentHeight}px`; const topPosition = getNextHeight(frameHeight, commentHeight); scrollComment.style.top = `${topPosition}px`; scrollComment.style.left = `${frameWidth}px`; const travelDistance = -(frameWidth + commentWidth); Object.assign(scrollComment.style, { transition: `transform ${settings.duration}s linear`, transform: `translateX(${travelDistance}px)` }); scrollComment.addEventListener('transitionend', () => { scrollComment.remove(); usedHeights.splice(usedHeights.indexOf(topPosition), 1); displayedComments.delete(uniqueId); if (commentQueue.length > 0) { const next = commentQueue.shift(); scrollComment(next.commentText, next.imgElements, next.uniqueId, next.usernameColor); } }, { once: true }); } catch (e) {} } function setupObserver(target) { try { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const newNodes = mutation.addedNodes; for (let node of newNodes) { if (node.nodeType !== 1 || node.dataset.processed) continue; node.dataset.processed = 'true'; let commentText = ''; let imgElements = []; let usernameColor = null; if (!settings.blockEmoji) { const emoteSpans = node.querySelectorAll('span[data-emote-id]'); imgElements = Array.from(emoteSpans) .map(span => span.querySelector('img')) .filter(img => img); } let contentNode = node.querySelector('span[class*="font-normal"][class*="leading-\\[1\\.55\\]"]'); if (contentNode) { let rawText = contentNode.textContent.trim(); commentText = rawText.replace(/^\d{1,2}:\d{2}\s+[^:]+:\s*/, '').trim(); const childNodes = contentNode.childNodes; for (let child of childNodes) { if (child.nodeType === 3 && child.textContent.trim()) { commentText = child.textContent.replace(/^\d{1,2}:\d{2}\s+[^:]+:\s*/, '').trim(); break; } } } const usernameNode = node.querySelector('button.inline.font-bold'); if (usernameNode && usernameNode.style.color) { usernameColor = usernameNode.style.color; } if (!commentText && imgElements.length === 0) continue; const imgSrcs = imgElements.map(img => img ? img.src : '').join(''); const uniqueId = `${commentText}_${imgSrcs}`; if (displayedComments.has(uniqueId)) continue; scrollComment(commentText, imgElements, uniqueId, usernameColor); } }); }); observer.observe(target, { childList: true, subtree: true }); } catch (e) {} } function findChatContainer() { try { return document.getElementById('chatroom-messages') || document.getElementById('chat-message-actions') || document.querySelector('div[data-index]') || document.querySelector('div[class*="chat"]') || document.querySelector('div[id*="chat"]') || document.querySelector('div[class*="message-container"]') || document.querySelector('div[class*="betterhover"]') || document.body; } catch (e) { return document.body; } } function monitorChatContainer() { try { const chatContainer = findChatContainer(); if (chatContainer && !currentObserver) { currentObserver = setupObserver(chatContainer); } } catch (e) {} setTimeout(monitorChatContainer, 1000); } function initialize() { try { if (document.body) { ensurePanelInDOM(); ensureButtonInDOM(); if (!settingsPanelInitialized) { createSettingsPanel(); } updateScrollContainer(); monitorChatContainer(); monitorUrlChange(); } else { setTimeout(initialize, 1000); } } catch (e) {} } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1000)); } else { setTimeout(initialize, 1000); } window.addEventListener('load', initialize); window.addEventListener('resize', updateScrollContainer); window.addEventListener('scroll', updateScrollContainer); })();