您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等
// ==UserScript== // @name YouTube聊天室增強 YouTube Chat Enhancement // @name:zh-tw YouTube聊天室增強 // @name:en YouTube Chat Enhancement // @namespace http://tampermonkey.net/ // @version 19.5 // @description 多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等 // @description:zh-tw 多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等 // @description:en Multi-color automated user highlighting;Non-native user blocking;Flexible UI operations and feature selection;Removal of distracting pinned messages;Spam cleanup/flagging;Message count statistics;Improved @mention experience // @match *://www.youtube.com/live_chat* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addStyle // @license MIT // ==/UserScript== (function(){'use strict'; const LANG = { 'zh-TW': { buttons: {'封鎖':'封鎖','編輯':'編輯','刪除':'刪除','清除':'清除'}, tooltips: { flag: '臨時模式: 切換至臨時用戶上色,開啟時上色不儲存', pin: '清除置頂: 開啟/關閉自動移除置頂訊息', highlight: mode => `高亮模式: ${mode} (雙擊切換模式)`, block: mode => `封鎖模式: ${mode} (雙擊切換模式)`, mention: mode => `提及高亮: ${mode} (雙擊切換模式)`, spam: mode => `洗版過濾: ${mode} (雙擊切換模式)`, counter: '留言計數: 顯示/隱藏用戶留言計數', clearConfirm: '確定清除所有設定?', clearButton: '確認' } }, 'en': { buttons: {'封鎖':'Block','編輯':'Edit','刪除':'Delete','清除':'Clear'}, tooltips: { flag: 'Temporary mode: Switch to temporary user coloring, colors are not saved when enabled', pin: 'Pin removal: Toggle auto-remove pinned messages', highlight: mode => `Highlight mode: ${mode} (Double-click to switch)`, block: mode => `Block mode: ${mode} (Double-click to switch)`, mention: mode => `Mention highlight: ${mode} (Double-click to switch)`, spam: mode => `Spam filter: ${mode} (Double-click to switch)`, counter: 'Message counter: Show/hide user message counts', clearConfirm: 'Confirm reset all settings?', clearButton: 'Confirm' } } }; const currentLang = navigator.language.startsWith('zh') ? 'zh-TW' : 'en'; // 顏色選項對照表 (16進位色碼) const COLOR_OPTIONS = { "淺藍":"#5FA6E8", "藍色":"#2463D1", "深藍":"#0000FF", "紫色":"#FF00FF", "淺綠":"#98FB98", "綠色":"#00FF00", "深綠":"#006400", "青色":"#00FFFF", "粉紅":"#FFC0CB", "淺紅":"#F08080", "紅色":"#FF0000", "深紅":"#8B0000", "橙色":"#FFA500", "金色":"#FFD700", "灰色":"#BDBDBD", "深灰":"#404040" }; // 系統常數設定 (單位:毫秒) const MENU_AUTO_CLOSE_DELAY = 30000, // 選單自動關閉延遲 THROTTLE_DELAY = 150, // 節流控制延遲 TEMP_USER_EXPIRE_TIME = 40000, // 臨時用戶資料過期時間 MAX_MESSAGE_CACHE_SIZE = 400, // 最大訊息快取數量 CLEANUP_INTERVAL = 40000, // 系統清理間隔 SPAM_CHECK_INTERVAL = 500, // 垃圾訊息檢查間隔 FLAG_DURATION = 60000, // 標記用戶持續時間 MESSAGE_CACHE_LIMIT = 400, // 訊息快取上限 DOUBLE_CLICK_DELAY = 350, // 雙擊判定間隔 PIN_CHECK_INTERVAL = 60000; // 置頂訊息檢查間隔 const HIGHLIGHT_MODES = { BOTH:0, NAME_ONLY:1, MESSAGE_ONLY:2 }, SPAM_MODES = { MARK:0, REMOVE:1 }, BLOCK_MODES = { MARK:0, HIDE:1 }; let userColorSettings = JSON.parse(localStorage.getItem('userColorSettings')) || {}, blockedUsers = JSON.parse(localStorage.getItem('blockedUsers')) || [], currentMenu = null, menuTimeoutId = null, featureSettings = JSON.parse(localStorage.getItem('featureSettings')) || { pinEnabled: false, highlightEnabled: false, blockEnabled: false, buttonsVisible: false, mentionHighlightEnabled: false, spamFilterEnabled: false, counterEnabled: false, spamMode: SPAM_MODES.MARK, blockMode: BLOCK_MODES.MARK, flagMode: false }, highlightSettings = JSON.parse(localStorage.getItem('highlightSettings')) || { defaultMode: HIGHLIGHT_MODES.BOTH, tempMode: HIGHLIGHT_MODES.BOTH }, tempUsers = JSON.parse(localStorage.getItem('tempUsers')) || {}, flaggedUsers = {}, lastTempUserCleanupTime = Date.now(), userMessageCounts = {}, lastSpamCheckTime = 0, lastClickTime = 0, clickCount = 0, pinRemoved = false, lastPinCheckTime = 0; const userColorCache = new Map(), blockedUsersSet = new Set(blockedUsers), tempUserCache = new Map(), styleCache = new WeakMap(); class LRUCache { constructor(limit) { this.limit = limit; this.cache = new Map(); } has(key) { return this.cache.has(key); } get(key) { const value = this.cache.get(key); if (value) { this.cache.delete(key); this.cache.set(key, value); } return value; } set(key, value) { if (this.cache.has(key)) this.cache.delete(key); else if (this.cache.size >= this.limit) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(key, value); } delete(key) { this.cache.delete(key); } clear() { this.cache.clear(); } } const messageCache = new LRUCache(MESSAGE_CACHE_LIMIT), processedMessages = new LRUCache(MAX_MESSAGE_CACHE_SIZE * 2); const style = document.createElement('style'); style.textContent = `:root{--highlight-color:inherit;--flagged-color:#FF0000} .ytcm-menu{position:fixed;background-color:white;border:1px solid black;padding:5px;z-index:9999;box-shadow:2px 2px 5px rgba(0,0,0,0.2);border-radius:5px}.ytcm-color-item{cursor:pointer;padding:0;border-radius:3px;margin:2px;border:1px solid #ddd;transition:transform 0.1s;min-width:40px;height:25px}.ytcm-color-item:hover{transform:scale(1.1);box-shadow:0 0 5px rgba(0,0,0,0.3)}.ytcm-list-item{cursor:pointer;padding:5px;background-color:#f0f0f0;border-radius:3px;margin:2px}.ytcm-button{cursor:pointer;padding:5px 8px;margin:5px 2px 0 2px;border-radius:3px;border:1px solid #ccc;background-color:#f8f8f8}.ytcm-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}.ytcm-button-row{display:flex;justify-content:space-between;margin-top:5px}.ytcm-flex-wrap{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}.ytcm-control-panel{position:fixed;left:0;bottom:75px;z-index:9998;display:flex;flex-direction:column;gap:8px;padding:0}.ytcm-control-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}.ytcm-control-btn.active{-webkit-text-stroke:1px black}.ytcm-control-btn.inactive{-webkit-text-stroke:1px red}.ytcm-toggle-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}.ytcm-main-buttons{display:${featureSettings.buttonsVisible?'flex':'none'};flex-direction:column;gap:8px}.ytcm-message-count{font-size:0.6em;opacity:0.7;margin-left:3px;display:inline-block}[data-blocked="true"][data-block-mode="mark"] #message{text-decoration:line-through!important;font-style:italic!important}[data-blocked="true"][data-block-mode="hide"]{display:none!important}[data-spam="true"] #message{text-decoration:line-through!important}[data-flagged="true"]{--flagged-opacity:1}[data-highlight="name"] #author-name,[data-highlight="both"] #author-name{color:var(--highlight-color)!important;font-weight:bold!important}[data-highlight="message"] #message,[data-highlight="both"] #message{color:var(--highlight-color)!important;font-weight:bold!important}[data-flagged="true"] #author-name,[data-flagged="true"] #message{color:var(--flagged-color)!important;font-weight:bold!important;font-style:italic!important;opacity:var(--flagged-opacity,1)}`;document.head.appendChild(style); document.head.appendChild(style); function initializeCaches() { Object.entries(userColorSettings).forEach(([user, color]) => userColorCache.set(user, color)); Object.entries(tempUsers).forEach(([user, data]) => tempUserCache.set(user, data)); Object.entries(flaggedUsers).forEach(([user, expireTime]) => { if (expireTime > Date.now()) updateAllMessages(user); }); } function updateAllMessages(userName) { const messages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer')) .filter(msg => { const nameElement = msg.querySelector('#author-name'); return nameElement && nameElement.textContent.trim() === userName && msg.style.display !== 'none'; }); messages.forEach(msg => { processedMessages.delete(msg); styleCache.delete(msg); processMessage(msg, true); }); } function createControlPanel() { const panel = document.createElement('div'); panel.className = 'ytcm-control-panel'; const mainButtons = document.createElement('div'); mainButtons.className = 'ytcm-main-buttons'; const buttons = [ { text: '臨', className: `ytcm-control-btn ${featureSettings.flagMode ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.flag, onClick: () => handleButtonClick('臨', () => { featureSettings.flagMode = !featureSettings.flagMode; updateButtonState('臨', featureSettings.flagMode); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); }) }, { text: '頂', className: `ytcm-control-btn ${featureSettings.pinEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.pin, onClick: () => handleButtonClick('頂', () => { featureSettings.pinEnabled = !featureSettings.pinEnabled; pinRemoved = false; updateButtonState('頂', featureSettings.pinEnabled); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); }) }, { text: '亮', className: `ytcm-control-btn ${featureSettings.highlightEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)), onClick: () => handleButtonClick( '亮', () => { featureSettings.highlightEnabled = !featureSettings.highlightEnabled; updateButtonState('亮', featureSettings.highlightEnabled); updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode))); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); updateAllMessages(); }, () => { highlightSettings.defaultMode = (highlightSettings.defaultMode + 1) % 3; updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode))); localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings)); updateAllMessages(); } ) }, { text: '封', className: `ytcm-control-btn ${featureSettings.blockEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.block( featureSettings.blockMode === BLOCK_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') ), onClick: () => handleButtonClick( '封', () => { featureSettings.blockEnabled = !featureSettings.blockEnabled; updateButtonState('封', featureSettings.blockEnabled); updateButtonTitle('封', LANG[currentLang].tooltips.block( featureSettings.blockMode === BLOCK_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') )); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); updateAllMessages(); }, () => { featureSettings.blockMode = (featureSettings.blockMode + 1) % 2; updateButtonTitle('封', LANG[currentLang].tooltips.block( featureSettings.blockMode === BLOCK_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') )); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); updateAllMessages(); } ) }, { text: '@', className: `ytcm-control-btn ${featureSettings.mentionHighlightEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)), onClick: () => handleButtonClick( '@', () => { featureSettings.mentionHighlightEnabled = !featureSettings.mentionHighlightEnabled; updateButtonState('@', featureSettings.mentionHighlightEnabled); updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode))); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); if (!featureSettings.mentionHighlightEnabled) { tempUsers = {}; tempUserCache.clear(); localStorage.setItem('tempUsers', JSON.stringify(tempUsers)); } updateAllMessages(); }, () => { highlightSettings.tempMode = (highlightSettings.tempMode + 1) % 3; updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode))); localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings)); updateAllMessages(); } ) }, { text: '洗', className: `ytcm-control-btn ${featureSettings.spamFilterEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.spam( featureSettings.spamMode === SPAM_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') ), onClick: () => handleButtonClick( '洗', () => { featureSettings.spamFilterEnabled = !featureSettings.spamFilterEnabled; updateButtonState('洗', featureSettings.spamFilterEnabled); updateButtonTitle('洗', LANG[currentLang].tooltips.spam( featureSettings.spamMode === SPAM_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') )); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); if (!featureSettings.spamFilterEnabled) messageCache.clear(); updateAllMessages(); }, () => { featureSettings.spamMode = (featureSettings.spamMode + 1) % 2; updateButtonTitle('洗', LANG[currentLang].tooltips.spam( featureSettings.spamMode === SPAM_MODES.MARK ? (currentLang === 'zh-TW' ? '標記' : 'Mark') : (currentLang === 'zh-TW' ? '清除' : 'Clear') )); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); updateAllMessages(); } ) }, { text: '數', className: `ytcm-control-btn ${featureSettings.counterEnabled ? 'active' : 'inactive'}`, title: LANG[currentLang].tooltips.counter, onClick: () => handleButtonClick('數', () => { featureSettings.counterEnabled = !featureSettings.counterEnabled; updateButtonState('數', featureSettings.counterEnabled); localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); if (!featureSettings.counterEnabled) { document.querySelectorAll('.ytcm-message-count').forEach(el => el.remove()); } else { updateAllMessages(); } }) } ]; buttons.forEach(btn => { const button = document.createElement('div'); button.className = btn.className; button.textContent = btn.text; button.title = btn.title; button.dataset.action = btn.text; button.addEventListener('click', btn.onClick); mainButtons.appendChild(button); }); const toggleBtn = document.createElement('div'); toggleBtn.className = 'ytcm-toggle-btn'; toggleBtn.textContent = '☑'; toggleBtn.title = currentLang === 'zh-TW' ? '顯示/隱藏控制按鈕' : 'Show/Hide Controls'; toggleBtn.addEventListener('click', () => { featureSettings.buttonsVisible = !featureSettings.buttonsVisible; mainButtons.style.display = featureSettings.buttonsVisible ? 'flex' : 'none'; localStorage.setItem('featureSettings', JSON.stringify(featureSettings)); }); panel.appendChild(mainButtons); panel.appendChild(toggleBtn); document.body.appendChild(panel); return panel; } function handleButtonClick(btnText, toggleAction, modeAction) { const now = Date.now(); if (now - lastClickTime < DOUBLE_CLICK_DELAY) { clickCount++; if (clickCount === 2 && modeAction) { modeAction(); clickCount = 0; } } else { clickCount = 1; setTimeout(() => { if (clickCount === 1) toggleAction(); clickCount = 0; }, DOUBLE_CLICK_DELAY); } lastClickTime = now; } function getHighlightModeName(mode) { switch (mode) { case HIGHLIGHT_MODES.BOTH: return currentLang === 'zh-TW' ? "全部高亮" : "Both"; case HIGHLIGHT_MODES.NAME_ONLY: return currentLang === 'zh-TW' ? "僅暱稱" : "Name Only"; case HIGHLIGHT_MODES.MESSAGE_ONLY: return currentLang === 'zh-TW' ? "僅對話" : "Message Only"; default: return ""; } } function updateButtonState(btnText, isActive) { const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`); if (btn) btn.className = `ytcm-control-btn ${isActive ? 'active' : 'inactive'}`; } function updateButtonTitle(btnText, title) { const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`); if (btn) { btn.title = title; } } function throttle(func, limit) { let lastFunc; let lastRan; return function() { const context = this; const args = arguments; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function() { if ((Date.now() - lastRan) >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } function cleanupProcessedMessages() { requestIdleCallback(() => { const allMessages = new Set(document.querySelectorAll('yt-live-chat-text-message-renderer')); const toDelete = []; processedMessages.cache.forEach((_, msg) => { if (!allMessages.has(msg)) { toDelete.push(msg); } }); toDelete.forEach(msg => { processedMessages.delete(msg); styleCache.delete(msg); }); }); } function processMentionedUsers(messageText, authorName, authorColor) { if (!featureSettings.mentionHighlightEnabled || !authorColor) return; const mentionRegex = /@([^\s].*?(?=\s|$|@|[\u200b]))/g; let match; const mentionedUsers = new Set(); while ((match = mentionRegex.exec(messageText)) !== null) { const mentionedUser = match[1].trim(); if (mentionedUser) { mentionedUsers.add(mentionedUser); } } if (mentionedUsers.size !== 1) return; const mentionedUser = Array.from(mentionedUsers)[0]; const allUsers = Array.from(document.querySelectorAll('#author-name')); const existingUsers = allUsers.map(el => el.textContent.trim()); const isExistingUser = existingUsers.some(user => user.toLowerCase() === mentionedUser.toLowerCase()); if (isExistingUser && !userColorCache.has(mentionedUser) && !tempUserCache.has(mentionedUser)) { tempUsers[mentionedUser] = { color: authorColor, expireTime: Date.now() + TEMP_USER_EXPIRE_TIME }; tempUserCache.set(mentionedUser, { color: authorColor, expireTime: Date.now() + TEMP_USER_EXPIRE_TIME }); updateAllMessages(mentionedUser); localStorage.setItem('tempUsers', JSON.stringify(tempUsers)); } } function cleanupExpiredTempUsers() { const now = Date.now(); if (now - lastTempUserCleanupTime < CLEANUP_INTERVAL) return; lastTempUserCleanupTime = now; let changed = false; for (const [user, data] of tempUserCache.entries()) { if (data.expireTime <= now) { tempUserCache.delete(user); if (tempUsers.hasOwnProperty(user)) { delete tempUsers[user]; } changed = true; updateAllMessages(user); } } if (changed) { localStorage.setItem('tempUsers', JSON.stringify(tempUsers)); } } function cleanupExpiredFlags() { const now = Date.now(); let changed = false; for (const user in flaggedUsers) { if (flaggedUsers[user] <= now) { delete flaggedUsers[user]; changed = true; updateAllMessages(user); } } } function removePinnedMessage() { if (!featureSettings.pinEnabled) return; const now = Date.now(); if (now - lastPinCheckTime < PIN_CHECK_INTERVAL) return; lastPinCheckTime = now; requestAnimationFrame(() => { const pinnedMessage = document.querySelector('yt-live-chat-banner-renderer'); if (pinnedMessage) { pinnedMessage.style.display = 'none'; } }); } function closeMenu() { if (currentMenu) { document.body.removeChild(currentMenu); currentMenu = null; clearTimeout(menuTimeoutId); } } function createColorMenu(targetElement, event) { closeMenu(); const menu = document.createElement('div'); menu.className = 'ytcm-menu'; menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`; menu.style.width = '220px'; const colorGrid = document.createElement('div'); colorGrid.className = 'ytcm-grid'; Object.entries(COLOR_OPTIONS).forEach(([colorName, colorValue]) => { const colorItem = document.createElement('div'); colorItem.className = 'ytcm-color-item'; colorItem.title = colorName; colorItem.style.backgroundColor = colorValue; colorItem.addEventListener('click', () => { if (targetElement.type === 'user') { userColorSettings[targetElement.name] = colorValue; userColorCache.set(targetElement.name, colorValue); updateAllMessages(targetElement.name); localStorage.setItem('userColorSettings', JSON.stringify(userColorSettings)); } else if (targetElement.type === 'temp') { tempUsers[targetElement.name] = { color: colorValue, expireTime: Date.now() + TEMP_USER_EXPIRE_TIME }; tempUserCache.set(targetElement.name, { color: colorValue, expireTime: Date.now() + TEMP_USER_EXPIRE_TIME }); updateAllMessages(targetElement.name); } closeMenu(); }); colorGrid.appendChild(colorItem); }); const buttonRow = document.createElement('div'); buttonRow.className = 'ytcm-button-row'; const buttons = [ { text: LANG[currentLang].buttons.封鎖, className: 'ytcm-button', onClick: () => { if (targetElement.type === 'user') { blockedUsers.push(targetElement.name); blockedUsersSet.add(targetElement.name); localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers)); updateAllMessages(targetElement.name); } closeMenu(); } }, { text: LANG[currentLang].buttons.編輯, className: 'ytcm-button', onClick: (e) => { e.stopPropagation(); createEditMenu(targetElement, event); } }, { text: LANG[currentLang].buttons.刪除, className: 'ytcm-button', onClick: () => { const userName = targetElement.name; let foundInList = false; if (userColorSettings[userName]) { delete userColorSettings[userName]; userColorCache.delete(userName); foundInList = true; } if (blockedUsersSet.has(userName)) { blockedUsers = blockedUsers.filter(u => u !== userName); blockedUsersSet.delete(userName); localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers)); foundInList = true; } if (tempUsers[userName]) { delete tempUsers[userName]; tempUserCache.delete(userName); localStorage.setItem('tempUsers', JSON.stringify(tempUsers)); foundInList = true; } if (flaggedUsers[userName]) { delete flaggedUsers[userName]; localStorage.setItem('flaggedUsers', JSON.stringify(flaggedUsers)); foundInList = true; } localStorage.setItem('userColorSettings', JSON.stringify(userColorSettings)); const messages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer')).filter(msg => { const nameElement = msg.querySelector('#author-name'); return nameElement && nameElement.textContent.trim() === userName; }); messages.forEach(msg => { if (foundInList) { msg.removeAttribute('data-highlight'); msg.removeAttribute('data-flagged'); msg.removeAttribute('data-blocked'); msg.removeAttribute('data-spam'); msg.style.removeProperty('--highlight-color'); msg.style.removeProperty('--flagged-color'); msg.querySelector('.ytcm-message-count')?.remove(); } else { msg.style.display = 'none'; } }); closeMenu(); } }, { text: LANG[currentLang].buttons.清除, className: 'ytcm-button', onClick: () => { const confirmMenu = document.createElement('div'); confirmMenu.className = 'ytcm-menu'; confirmMenu.style.top = `${event.clientY}px`; confirmMenu.style.left = `${event.clientX}px`; const confirmText = document.createElement('div'); confirmText.textContent = LANG[currentLang].tooltips.clearConfirm; const confirmButton = document.createElement('button'); confirmButton.className = 'ytcm-button'; confirmButton.textContent = LANG[currentLang].tooltips.clearButton; confirmButton.addEventListener('click', () => { localStorage.removeItem('userColorSettings'); localStorage.removeItem('blockedUsers'); localStorage.removeItem('featureSettings'); localStorage.removeItem('highlightSettings'); localStorage.removeItem('tempUsers'); localStorage.removeItem('flaggedUsers'); window.location.reload(); }); confirmMenu.appendChild(confirmText); confirmMenu.appendChild(confirmButton); document.body.appendChild(confirmMenu); setTimeout(() => { if (document.body.contains(confirmMenu)) document.body.removeChild(confirmMenu); }, 5000); } } ]; buttons.forEach(btn => { const button = document.createElement('button'); button.className = btn.className; button.textContent = btn.text; button.addEventListener('click', btn.onClick); buttonRow.appendChild(button); }); menu.appendChild(colorGrid); menu.appendChild(buttonRow); document.body.appendChild(menu); currentMenu = menu; menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY); } function createEditMenu(targetElement, event) { closeMenu(); const menu = document.createElement('div'); menu.className = 'ytcm-menu'; menu.style.top = '10px'; menu.style.left = '10px'; menu.style.width = '90%'; menu.style.maxHeight = '80vh'; menu.style.overflowY = 'auto'; const closeButton = document.createElement('button'); closeButton.className = 'ytcm-button'; closeButton.textContent = currentLang === 'zh-TW' ? '關閉' : 'Close'; closeButton.style.width = '100%'; closeButton.style.marginBottom = '10px'; closeButton.addEventListener('click', closeMenu); menu.appendChild(closeButton); const importExportRow = document.createElement('div'); importExportRow.className = 'ytcm-button-row'; const exportButton = document.createElement('button'); exportButton.className = 'ytcm-button'; exportButton.textContent = currentLang === 'zh-TW' ? '匯出設定' : 'Export'; exportButton.addEventListener('click', () => { const data = { userColorSettings, blockedUsers, featureSettings, highlightSettings, tempUsers: JSON.parse(localStorage.getItem('tempUsers')) || {} }; const blob = new Blob([JSON.stringify(data)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'yt_chat_settings.json'; a.click(); URL.revokeObjectURL(url); }); const importButton = document.createElement('input'); importButton.type = 'file'; importButton.className = 'ytcm-button'; importButton.accept = '.json'; importButton.style.width = '100px'; importButton.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target.result); localStorage.setItem('userColorSettings', JSON.stringify(data.userColorSettings)); localStorage.setItem('blockedUsers', JSON.stringify(data.blockedUsers)); localStorage.setItem('featureSettings', JSON.stringify(data.featureSettings)); localStorage.setItem('highlightSettings', JSON.stringify(data.highlightSettings)); localStorage.setItem('tempUsers', JSON.stringify(data.tempUsers)); window.location.reload(); } catch (err) { alert(currentLang === 'zh-TW' ? '檔案格式錯誤' : 'Invalid file format'); } }; reader.readAsText(file); }); importExportRow.appendChild(exportButton); importExportRow.appendChild(importButton); menu.appendChild(importExportRow); const blockedUserList = document.createElement('div'); blockedUserList.textContent = currentLang === 'zh-TW' ? '封鎖用戶名單:' : 'Blocked Users:'; blockedUserList.className = 'ytcm-flex-wrap'; blockedUsers.forEach(user => { const userItem = document.createElement('div'); userItem.className = 'ytcm-list-item'; userItem.textContent = user; userItem.addEventListener('click', () => { blockedUsers = blockedUsers.filter(u => u !== user); blockedUsersSet.delete(user); localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers)); userItem.remove(); updateAllMessages(user); }); blockedUserList.appendChild(userItem); }); menu.appendChild(blockedUserList); const coloredUserList = document.createElement('div'); coloredUserList.textContent = currentLang === 'zh-TW' ? '被上色用戶名單:' : 'Colored Users:'; coloredUserList.className = 'ytcm-flex-wrap'; Object.keys(userColorSettings).forEach(user => { const userItem = document.createElement('div'); userItem.className = 'ytcm-list-item'; userItem.textContent = user; userItem.addEventListener('click', () => { delete userColorSettings[user]; userColorCache.delete(user); localStorage.setItem('userColorSettings', JSON.stringify(userColorSettings)); userItem.remove(); updateAllMessages(user); }); coloredUserList.appendChild(userItem); }); menu.appendChild(coloredUserList); document.body.appendChild(menu); currentMenu = menu; menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY); } function checkForSpam(msg) { if (!featureSettings.spamFilterEnabled) return; const nameElement = msg.querySelector('#author-name'); if (!nameElement) return; const userName = nameElement.textContent.trim(); if (userColorCache.has(userName) || tempUserCache.has(userName) || flaggedUsers[userName]) return; const messageElement = msg.querySelector('#message'); if (!messageElement) return; const textNodes = Array.from(messageElement.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE && !node.parentElement.classList.contains('emoji')); const messageText = textNodes .map(node => node.textContent.trim()) .join(' '); if (messageCache.has(messageText)) { if (featureSettings.spamMode === SPAM_MODES.MARK) { msg.setAttribute('data-spam', 'true'); messageElement.style.textDecoration = 'line-through'; } else { msg.style.display = 'none'; } return; } messageCache.set(messageText, true); } function updateMessageCounter(msg) { if (!featureSettings.counterEnabled) return; const nameElement = msg.querySelector('#author-name'); if (!nameElement) return; const userName = nameElement.textContent.trim(); if (!userMessageCounts[userName]) userMessageCounts[userName] = 0; userMessageCounts[userName]++; const existingCounter = msg.querySelector('.ytcm-message-count'); if (existingCounter) existingCounter.remove(); const counterSpan = document.createElement('span'); counterSpan.className = 'ytcm-message-count'; counterSpan.textContent = userMessageCounts[userName]; const messageElement = msg.querySelector('#message'); if (messageElement) messageElement.appendChild(counterSpan); } function processMessage(msg, isInitialLoad = false) { if (styleCache.has(msg)) return; const authorName = msg.querySelector('#author-name'); const messageElement = msg.querySelector('#message'); if (!authorName || !messageElement) return; const userName = authorName.textContent.trim(); if (featureSettings.spamFilterEnabled) checkForSpam(msg); msg.removeAttribute('data-spam'); if (featureSettings.blockEnabled && blockedUsersSet.has(userName)) { msg.setAttribute('data-blocked', 'true'); msg.setAttribute('data-block-mode', featureSettings.blockMode === BLOCK_MODES.MARK ? 'mark' : 'hide'); if (featureSettings.blockMode === BLOCK_MODES.HIDE) { msg.style.display = 'none'; } styleCache.set(msg, true); return; } if (msg.hasAttribute('data-blocked')) { styleCache.set(msg, true); return; } msg.removeAttribute('data-highlight'); msg.removeAttribute('data-flagged'); if (featureSettings.flagMode && flaggedUsers[userName]) { msg.setAttribute('data-flagged', 'true'); msg.style.setProperty('--highlight-color', userColorCache.get(userName) || COLOR_OPTIONS.紅色); } if (featureSettings.highlightEnabled && (tempUserCache.has(userName) || userColorCache.get(userName))) { const color = tempUserCache.has(userName) ? tempUserCache.get(userName).color : userColorCache.get(userName); const mode = tempUserCache.has(userName) ? highlightSettings.tempMode : highlightSettings.defaultMode; msg.style.setProperty('--highlight-color', color); if (mode !== HIGHLIGHT_MODES.BOTH && mode !== HIGHLIGHT_MODES.NAME_ONLY && mode !== HIGHLIGHT_MODES.MESSAGE_ONLY) return; msg.setAttribute('data-highlight', mode === HIGHLIGHT_MODES.BOTH ? 'both' : mode === HIGHLIGHT_MODES.NAME_ONLY ? 'name' : 'message'); } updateMessageCounter(msg); if (featureSettings.mentionHighlightEnabled) { const textNodes = Array.from(messageElement.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE && !node.parentElement.classList.contains('emoji')); const messageText = textNodes .map(node => node.textContent.trim()) .join(' '); processMentionedUsers( messageText, userName, tempUserCache.has(userName) ? tempUserCache.get(userName).color : userColorCache.get(userName) ); } styleCache.set(msg, true); } function highlightMessages(mutations) { cleanupProcessedMessages(); const messages = []; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.matches('yt-live-chat-text-message-renderer') && !processedMessages.has(node)) { messages.push(node); processedMessages.set(node, true); } }); }); if (messages.length === 0) { const allMessages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer')) .slice(-MAX_MESSAGE_CACHE_SIZE); allMessages.forEach(msg => { if (!processedMessages.has(msg)) { messages.push(msg); processedMessages.set(msg, true); } }); } requestAnimationFrame(() => { messages.forEach(msg => processMessage(msg)); cleanupExpiredFlags(); cleanupExpiredTempUsers(); removePinnedMessage(); }); } function handleClick(event) { if (event.button !== 0) return; const msgElement = event.target.closest('yt-live-chat-text-message-renderer'); if (!msgElement) return; const messageElement = msgElement.querySelector('#message'); if (messageElement) { const rect = messageElement.getBoundingClientRect(); const isRightEdge = event.clientX > rect.right - 30; if (isRightEdge) return; } event.stopPropagation(); event.preventDefault(); if (currentMenu && !currentMenu.contains(event.target)) closeMenu(); const authorName = msgElement.querySelector('#author-name'); const authorImg = msgElement.querySelector('#author-photo img'); if (authorImg && authorImg.contains(event.target)) { if (event.ctrlKey) { const URL = authorName?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId || authorName?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId; URL && window.open("https://www.youtube.com/channel/" + URL + "/about", "_blank"); } else { if (featureSettings.flagMode) { createColorMenu({ type: 'temp', name: authorName.textContent.trim() }, event); } else { createColorMenu({ type: 'user', name: authorName.textContent.trim() }, event); } } } if (authorName && authorName.contains(event.target)) { const inputField = document.querySelector('yt-live-chat-text-input-field-renderer [contenteditable]'); if (inputField) { setTimeout(() => { const userName = authorName.textContent.trim(); const mentionText = `@${userName}\u2009`; const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(inputField); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); inputField.focus(); document.execCommand('insertText', false, mentionText); range.setStartAfter(inputField.lastChild); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); }, 200); } } } function overrideNativeListeners() { document.querySelectorAll('yt-live-chat-text-message-renderer').forEach(msg => { msg.addEventListener('click', handleClick, { capture: true }); }); } function init() { initializeCaches(); overrideNativeListeners(); const observer = new MutationObserver(mutations => { highlightMessages(mutations); document.querySelectorAll('yt-live-chat-text-message-renderer:not([data-ytcm-handled])').forEach(msg => { msg.setAttribute('data-ytcm-handled', 'true'); msg.addEventListener('click', handleClick, { capture: true }); if (!processedMessages.has(msg)) { processedMessages.set(msg, true); processMessage(msg, true); } }); }); const chatContainer = document.querySelector('#chat'); if (chatContainer) { observer.observe(chatContainer, { childList: true, subtree: true }); const existingMessages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')); existingMessages.forEach(msg => { msg.setAttribute('data-ytcm-handled', 'true'); msg.addEventListener('click', handleClick, { capture: true }); if (!processedMessages.has(msg)) { processedMessages.set(msg, true); processMessage(msg, true); } }); } const controlPanel = createControlPanel(); return () => { observer.disconnect(); if (controlPanel) controlPanel.remove(); closeMenu(); }; } let cleanup = init(); const checkChatContainer = setInterval(() => { if (document.querySelector('#chat') && !cleanup) { cleanup = init(); } }, 1000); window.addEventListener('beforeunload', () => { clearInterval(checkChatContainer); cleanup?.(); }); })();