// ==UserScript==
// @name YouTube 聊天室管理
// @namespace http://tampermonkey.net/
// @version 9.1
// @description 提供高亮訊息、封鎖用戶、編輯顏色名單與移除聊天室置頂功能。
// @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; // 重複訊息檢查間隔
// 初始化設定
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 });
})();