// ==UserScript== // @name YouTube 聊天室管理 // @namespace http://tampermonkey.net/ // @version 9.1 // @description 提供高亮訊息、封鎖用戶、編輯顏色名單與移除聊天室置頂功能。 // @match *://www.youtube.com/live_chat* // @grant none // @license MIT // @downloadURL none // ==/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; // 重複訊息檢查間隔 // 初始化設定 let userColorSettings = loadSettings('userColorSettings', {}); let keywordColorSettings = loadSettings('keywordColorSettings', {}); let blockedUsers = loadSettings('blockedUsers', []); let currentMenu = null; let menuTimeoutId = null; let lastDuplicateHighlightTime = 0; const chatContainer = document.querySelector('#chat'); // 防抖函數 function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // 加載設定 function loadSettings(key, defaultValue) { try { return JSON.parse(localStorage.getItem(key)) || defaultValue; } catch (error) { console.error(`Failed to load ${key}:`, error); return defaultValue; } } // 保存設定 function saveSettings(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(`Failed to save ${key}:`, error); } } // 高亮訊息 function highlightMessages() { const messages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')).slice(-50); messages.forEach(msg => { const userName = msg.querySelector('#author-name').textContent.trim(); const messageElement = msg.querySelector('#message'); const messageText = messageElement.textContent.trim(); // 重置訊息顏色 messageElement.style.color = ''; // 應用用戶顏色設定 if (userColorSettings[userName]) { messageElement.style.color = userColorSettings[userName]; } // 應用關鍵字顏色設定 for (const [keyword, keywordColor] of Object.entries(keywordColorSettings)) { if (messageText.includes(keyword)) { messageElement.style.color = keywordColor; } } }); } // 標記重複訊息 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 userName = msg.querySelector('#author-name').textContent.trim(); const messageElement = msg.querySelector('#message'); const messageText = messageElement.textContent.trim(); const key = `${userName}: ${messageText}`; if (messageMap.has(key)) { messageElement.textContent = ''; // 清空重複訊息 } else { messageMap.set(key, msg); } }); } // 處理封鎖用戶 function handleBlockedUsers() { const messages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer')).slice(-50); messages.forEach(msg => { const userName = msg.querySelector('#author-name').textContent.trim(); const messageElement = msg.querySelector('#message'); if (blockedUsers.includes(userName)) { messageElement.textContent = ''; // 清空封鎖用戶的訊息 } }); } // 移除置頂訊息 function removePinnedMessage() { const pinnedMessage = document.querySelector('yt-live-chat-banner-renderer'); if (pinnedMessage) { pinnedMessage.style.display = 'none'; // 隱藏置頂訊息 } } // 創建顏色選單 function createColorMenu(targetElement, event) { if (currentMenu) { document.body.removeChild(currentMenu); clearTimeout(menuTimeoutId); } const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.backgroundColor = 'white'; menu.style.border = '1px solid black'; menu.style.padding = '5px'; menu.style.zIndex = 9999; menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`; menu.style.width = '200px'; menu.style.boxShadow = '2px 2px 5px rgba(0, 0, 0, 0.2)'; menu.style.borderRadius = '5px'; menu.addEventListener('click', (e) => e.stopPropagation()); const colorColumn = document.createElement('div'); colorColumn.style.display = 'grid'; colorColumn.style.gridTemplateColumns = 'repeat(4, 1fr)'; colorColumn.style.gap = '5px'; Object.entries(COLOR_OPTIONS).forEach(([colorName, colorValue]) => { const colorItem = document.createElement('div'); colorItem.textContent = colorName; colorItem.style.cursor = 'pointer'; colorItem.style.padding = '5px'; colorItem.style.textAlign = 'center'; colorItem.style.backgroundColor = colorValue; colorItem.style.borderRadius = '3px'; colorItem.onclick = () => { if (targetElement.type === 'user') { userColorSettings[targetElement.name] = colorValue; } else if (targetElement.type === 'keyword') { keywordColorSettings[targetElement.keyword] = colorValue; } saveSettings('userColorSettings', userColorSettings); saveSettings('keywordColorSettings', keywordColorSettings); document.body.removeChild(menu); currentMenu = null; }; colorColumn.appendChild(colorItem); }); // 添加封鎖按鈕 const blockButton = document.createElement('button'); blockButton.textContent = '封鎖'; blockButton.style.marginTop = '10px'; blockButton.style.padding = '5px'; blockButton.style.cursor = 'pointer'; blockButton.onclick = () => { if (targetElement.type === 'user') { blockedUsers.push(targetElement.name); saveSettings('blockedUsers', blockedUsers); } document.body.removeChild(menu); currentMenu = null; }; // 添加編輯按鈕 const editButton = document.createElement('button'); editButton.textContent = '編輯'; editButton.style.marginTop = '5px'; editButton.style.padding = '5px'; editButton.style.cursor = 'pointer'; editButton.onclick = () => { createEditMenu(event); document.body.removeChild(menu); currentMenu = null; }; menu.appendChild(colorColumn); menu.appendChild(blockButton); menu.appendChild(editButton); document.body.appendChild(menu); currentMenu = menu; menuTimeoutId = setTimeout(() => { if (currentMenu) { document.body.removeChild(currentMenu); currentMenu = null; } }, MENU_AUTO_CLOSE_DELAY); } // 創建編輯選單 function createEditMenu(event) { if (currentMenu) { document.body.removeChild(currentMenu); clearTimeout(menuTimeoutId); } const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.backgroundColor = 'white'; menu.style.border = '1px solid black'; menu.style.padding = '5px'; menu.style.zIndex = 9999; menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`; menu.style.width = 'auto'; // 寬度自動調整 menu.style.maxWidth = '600px'; // 最大寬度 menu.style.boxShadow = '2px 2px 5px rgba(0, 0, 0, 0.2)'; menu.style.borderRadius = '5px'; menu.style.display = 'flex'; menu.style.flexDirection = 'column'; menu.style.alignItems = 'flex-start'; // 向左對齊 menu.addEventListener('click', (e) => e.stopPropagation()); // 添加關閉按鈕 const closeButton = document.createElement('button'); closeButton.textContent = '關閉'; closeButton.style.width = '100%'; closeButton.style.padding = '5px'; closeButton.style.cursor = 'pointer'; closeButton.style.marginBottom = '10px'; closeButton.onclick = () => { document.body.removeChild(menu); currentMenu = null; }; menu.appendChild(closeButton); // 顯示被封鎖用戶名單 const blockedUserList = document.createElement('div'); blockedUserList.textContent = '封鎖用戶名單:'; blockedUserList.style.display = 'flex'; blockedUserList.style.flexWrap = 'wrap'; // 換行顯示 blockedUserList.style.gap = '5px'; // 間距 blockedUsers.forEach(user => { const userItem = document.createElement('div'); userItem.textContent = user; userItem.style.cursor = 'pointer'; userItem.style.padding = '5px'; userItem.style.backgroundColor = '#f0f0f0'; userItem.style.borderRadius = '3px'; userItem.onclick = () => { blockedUsers = blockedUsers.filter(u => u !== user); saveSettings('blockedUsers', blockedUsers); userItem.remove(); // 移除該條目 }; blockedUserList.appendChild(userItem); }); // 顯示關鍵字名單 const keywordList = document.createElement('div'); keywordList.textContent = '關鍵字名單:'; keywordList.style.display = 'flex'; keywordList.style.flexWrap = 'wrap'; // 換行顯示 keywordList.style.gap = '5px'; // 間距 Object.keys(keywordColorSettings).forEach(keyword => { const keywordItem = document.createElement('div'); keywordItem.textContent = keyword; keywordItem.style.cursor = 'pointer'; keywordItem.style.padding = '5px'; keywordItem.style.backgroundColor = '#f0f0f0'; keywordItem.style.borderRadius = '3px'; keywordItem.onclick = () => { delete keywordColorSettings[keyword]; saveSettings('keywordColorSettings', keywordColorSettings); keywordItem.remove(); // 移除該條目 }; keywordList.appendChild(keywordItem); }); // 顯示被上色用戶名單 const coloredUserList = document.createElement('div'); coloredUserList.textContent = '被上色用戶名單:'; coloredUserList.style.display = 'flex'; coloredUserList.style.flexWrap = 'wrap'; // 換行顯示 coloredUserList.style.gap = '5px'; // 間距 Object.keys(userColorSettings).forEach(user => { const userItem = document.createElement('div'); userItem.textContent = user; userItem.style.cursor = 'pointer'; userItem.style.padding = '5px'; userItem.style.backgroundColor = '#f0f0f0'; userItem.style.borderRadius = '3px'; userItem.onclick = () => { delete userColorSettings[user]; saveSettings('userColorSettings', userColorSettings); userItem.remove(); // 移除該條目 }; coloredUserList.appendChild(userItem); }); menu.appendChild(blockedUserList); menu.appendChild(keywordList); menu.appendChild(coloredUserList); document.body.appendChild(menu); currentMenu = menu; // 設置選單自動關閉 menuTimeoutId = setTimeout(() => { if (currentMenu) { document.body.removeChild(currentMenu); currentMenu = null; } }, MENU_AUTO_CLOSE_DELAY); } // 點擊事件處理 document.addEventListener('click', (event) => { if (currentMenu && !currentMenu.contains(event.target)) { document.body.removeChild(currentMenu); currentMenu = null; } 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(debounce(() => { highlightMessages(); markDuplicateMessages(); handleBlockedUsers(); removePinnedMessage(); }, 300)); observer.observe(chatContainer, { childList: true, subtree: true }); })();