您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
8種預設可選色彩用以自動著色指定用戶訊息,其它功能包含封鎖用戶、簡化編輯儲存在瀏覽器的用戶清單、移除聊天室置頂消息,清理重複消息。
当前为
// ==UserScript== // @name YouTube 聊天室管理 // @namespace http://tampermonkey.net/ // @version 9.1.3 // @description 8種預設可選色彩用以自動著色指定用戶訊息,其它功能包含封鎖用戶、簡化編輯儲存在瀏覽器的用戶清單、移除聊天室置頂消息,清理重複消息。 // @match *://www.youtube.com/live_chat* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // 常數定義 const COLOR_OPTIONS = { "淺藍": "lightblue", "深藍": "blue", "淺綠": "palegreen", "綠色": "green", "淺紅": "lightcoral", "紅色": "red", "紫色": "purple", "金色": "gold" }; const MENU_AUTO_CLOSE_DELAY = 8000; const DUPLICATE_HIGHLIGHT_INTERVAL = 10000; const THROTTLE_DELAY = 150; // 初始化設定 let userColorSettings = loadSettings('userColorSettings', {}); let keywordColorSettings = loadSettings('keywordColorSettings', {}); let blockedUsers = loadSettings('blockedUsers', []); let currentMenu = null; let menuTimeoutId = null; let lastDuplicateHighlightTime = 0; // 新增快取結構 const userColorCache = new Map(Object.entries(userColorSettings)); const keywordColorCache = new Map(Object.entries(keywordColorSettings)); const blockedUsersSet = new Set(blockedUsers); // 預先注入 CSS 樣式 const style = document.createElement('style'); style.textContent = ` .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: 5px; text-align: center; border-radius: 3px; } .ytcm-list-item { cursor: pointer; padding: 5px; background-color: #f0f0f0; border-radius: 3px; margin: 2px; } .ytcm-button { cursor: pointer; padding: 5px; margin-top: 5px; } .ytcm-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; } .ytcm-flex-wrap { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; } `; document.head.appendChild(style); // DOM 元素快取 const chatContainer = document.querySelector('#chat'); let lastProcessedMessageId = ''; // 節流函數 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 loadSettings(key, defaultValue) { try { return JSON.parse(localStorage.getItem(key)) || defaultValue; } catch (error) { console.error(`Failed to load ${key}:`, error); return defaultValue; } } // 保存設定 (批次處理) let settingsSaveQueue = {}; function saveSettings(key, value) { settingsSaveQueue[key] = value; if (!window.settingsSaveTimeout) { window.settingsSaveTimeout = setTimeout(() => { try { Object.keys(settingsSaveQueue).forEach(k => { localStorage.setItem(k, JSON.stringify(settingsSaveQueue[k])); }); settingsSaveQueue = {}; } catch (error) { console.error('Batch save failed:', error); } window.settingsSaveTimeout = null; }, 1000); } } // 高亮訊息 function highlightMessages(mutations) { const messages = []; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.matches('yt-live-chat-text-message-renderer')) { messages.push(node); } }); }); if (messages.length === 0) { messages.push(...Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')).slice(-50)); } messages.forEach(msg => { const messageId = msg.id; if (!messageId || messageId === lastProcessedMessageId) return; lastProcessedMessageId = messageId; const authorName = msg.querySelector('#author-name'); const messageElement = msg.querySelector('#message'); if (!authorName || !messageElement) return; const userName = authorName.textContent.trim(); const messageText = messageElement.textContent.trim(); messageElement.style.color = ''; if (userColorCache.has(userName)) { messageElement.style.color = userColorCache.get(userName); return; } for (const [keyword, color] of keywordColorCache) { if (messageText.includes(keyword)) { messageElement.style.color = color; break; } } }); } // 標記重複訊息 function markDuplicateMessages() { const currentTime = Date.now(); if (currentTime - lastDuplicateHighlightTime < DUPLICATE_HIGHLIGHT_INTERVAL) return; lastDuplicateHighlightTime = currentTime; const messages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')).slice(-50); const messageMap = new Map(); messages.forEach(msg => { const authorName = msg.querySelector('#author-name'); const messageElement = msg.querySelector('#message'); if (!authorName || !messageElement) return; const userName = authorName.textContent.trim(); const messageText = messageElement.textContent.trim(); const key = `${userName}:${messageText}`; if (messageMap.has(key)) { messageElement.textContent = ''; } else { messageMap.set(key, msg); } }); } // 處理封鎖用戶 function handleBlockedUsers(mutations) { const messages = []; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.matches('yt-live-chat-text-message-renderer')) { messages.push(node); } }); }); if (messages.length === 0) { messages.push(...Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')).slice(-50)); } messages.forEach(msg => { const authorName = msg.querySelector('#author-name'); if (!authorName) return; const userName = authorName.textContent.trim(); if (blockedUsersSet.has(userName)) { const messageElement = msg.querySelector('#message'); if (messageElement) { messageElement.textContent = ''; } } }); } // 移除置頂訊息 function removePinnedMessage() { 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 = '200px'; const colorColumn = document.createElement('div'); colorColumn.className = 'ytcm-grid'; Object.entries(COLOR_OPTIONS).forEach(([colorName, colorValue]) => { const colorItem = document.createElement('div'); colorItem.className = 'ytcm-color-item'; colorItem.textContent = colorName; colorItem.style.backgroundColor = colorValue; colorItem.addEventListener('click', () => { if (targetElement.type === 'user') { userColorSettings[targetElement.name] = colorValue; userColorCache.set(targetElement.name, colorValue); } else if (targetElement.type === 'keyword') { keywordColorSettings[targetElement.keyword] = colorValue; keywordColorCache.set(targetElement.keyword, colorValue); } saveSettings('userColorSettings', userColorSettings); saveSettings('keywordColorSettings', keywordColorSettings); closeMenu(); }); colorColumn.appendChild(colorItem); }); const blockButton = document.createElement('button'); blockButton.className = 'ytcm-button'; blockButton.textContent = '封鎖'; blockButton.addEventListener('click', () => { if (targetElement.type === 'user') { blockedUsers.push(targetElement.name); blockedUsersSet.add(targetElement.name); saveSettings('blockedUsers', blockedUsers); } closeMenu(); }); const editButton = document.createElement('button'); editButton.className = 'ytcm-button'; editButton.textContent = '編輯'; editButton.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡 createEditMenu(event); // 使用原始事件對象 }); menu.appendChild(colorColumn); menu.appendChild(blockButton); menu.appendChild(editButton); document.body.appendChild(menu); currentMenu = menu; menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY); } // 創建編輯選單 function createEditMenu(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.maxWidth = '600px'; const closeButton = document.createElement('button'); closeButton.className = 'ytcm-button'; closeButton.textContent = '關閉'; closeButton.style.width = '100%'; closeButton.style.marginBottom = '10px'; closeButton.addEventListener('click', closeMenu); menu.appendChild(closeButton); // 封鎖用戶名單 const blockedUserList = document.createElement('div'); blockedUserList.textContent = '封鎖用戶名單:'; 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); saveSettings('blockedUsers', blockedUsers); userItem.remove(); }); blockedUserList.appendChild(userItem); }); menu.appendChild(blockedUserList); // 關鍵字名單 const keywordList = document.createElement('div'); keywordList.textContent = '關鍵字名單:'; keywordList.className = 'ytcm-flex-wrap'; Object.keys(keywordColorSettings).forEach(keyword => { const keywordItem = document.createElement('div'); keywordItem.className = 'ytcm-list-item'; keywordItem.textContent = keyword; keywordItem.addEventListener('click', () => { delete keywordColorSettings[keyword]; keywordColorCache.delete(keyword); saveSettings('keywordColorSettings', keywordColorSettings); keywordItem.remove(); }); keywordList.appendChild(keywordItem); }); menu.appendChild(keywordList); // 被上色用戶名單 const coloredUserList = document.createElement('div'); coloredUserList.textContent = '被上色用戶名單:'; 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); saveSettings('userColorSettings', userColorSettings); userItem.remove(); }); coloredUserList.appendChild(userItem); }); menu.appendChild(coloredUserList); document.body.appendChild(menu); currentMenu = menu; menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY); } // 點擊事件處理 document.addEventListener('click', (event) => { if (currentMenu && !currentMenu.contains(event.target)) { closeMenu(); } if (event.target.id === 'author-name') { const userName = event.target.textContent.trim(); createColorMenu({ type: 'user', name: userName }, event); } else { const selectedText = window.getSelection().toString(); if (selectedText) { createColorMenu({ type: 'keyword', keyword: selectedText }, event); } } }); // MutationObserver const observer = new MutationObserver(throttle((mutations) => { highlightMessages(mutations); markDuplicateMessages(); handleBlockedUsers(mutations); removePinnedMessage(); }, THROTTLE_DELAY)); if (chatContainer) { observer.observe(chatContainer, { childList: true, subtree: true }); } })();