// ==UserScript==
// @name Youtube聊天室管理+統計
// @namespace http://tampermonkey.net/
// @version 11.1
// @description 提供高亮訊息、封鎖用戶、編輯顏色名單、移除聊天室置頂功能,並統計超級留言金額,美化統計視窗。
// @match *://www.youtube.com/live_chat*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js
// @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 DEFAULT_CURRENCY = '$';
const HIGHLIGHT_DURATION = 4000; // 高亮持續時間
// 初始化設定
let userColorSettings = loadSettings('userColorSettings', {});
let keywordColorSettings = loadSettings('keywordColorSettings', {});
let blockedUsers = loadSettings('blockedUsers', []);
let superChatStats = {
amount: 0,
count: 0
};
let currentMenu = null;
let menuTimeoutId = null;
let lastDuplicateHighlightTime = 0;
let statsWindowVisible = true;
let statsWindow = null;
let isTrackingEnabled = true;
let highlightTimeout = null;
let toggleCircle = null;
const chatContainer = document.querySelector('#chat');
// 創建SVG圖標
function createSVGIcon(iconName) {
const svgNS = "http://www.w3.org/2000/svg";
const icon = document.createElementNS(svgNS, "svg");
icon.setAttribute("viewBox", "0 0 24 24");
icon.setAttribute("width", "16");
icon.setAttribute("height", "16");
icon.style.fill = "currentColor";
let path;
switch(iconName) {
case 'close':
path = document.createElementNS(svgNS, "path");
path.setAttribute("d", "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z");
break;
case 'reset':
path = document.createElementNS(svgNS, "path");
path.setAttribute("d", "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z");
break;
case 'stats':
path = document.createElementNS(svgNS, "path");
path.setAttribute("d", "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z");
break;
}
icon.appendChild(path);
return icon;
}
// 創建切換圓圈
function createToggleCircle() {
if (toggleCircle) return;
toggleCircle = document.createElement('div');
toggleCircle.style.position = 'fixed';
toggleCircle.style.top = '60px';
toggleCircle.style.left = '10px';
toggleCircle.style.width = '20px';
toggleCircle.style.height = '20px';
toggleCircle.style.borderRadius = '50%';
toggleCircle.style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; // 改為白色半透明
toggleCircle.style.display = 'none';
toggleCircle.style.zIndex = '9998';
toggleCircle.style.cursor = 'pointer';
toggleCircle.style.justifyContent = 'center';
toggleCircle.style.alignItems = 'center';
toggleCircle.style.transition = 'all 0.3s ease';
toggleCircle.style.border = '1px solid rgba(0, 0, 0, 0.1)'; // 添加淺色邊框
const icon = createSVGIcon('stats');
icon.style.width = '12px';
icon.style.height = '12px';
icon.style.margin = '4px';
toggleCircle.appendChild(icon);
toggleCircle.addEventListener('click', () => {
statsWindowVisible = true;
isTrackingEnabled = true;
if (statsWindow) {
statsWindow.style.display = 'block';
} else {
createStatsWindow();
}
toggleCircle.style.display = 'none';
});
document.body.appendChild(toggleCircle);
}
// 高亮統計視窗
function highlightStatsWindow() {
if (!statsWindow) return;
if (highlightTimeout) {
clearTimeout(highlightTimeout);
}
statsWindow.style.backgroundColor = 'rgba(255, 255, 255, 1)'; // 高亮時改為完全不透明白色
statsWindow.style.transition = 'background-color 0.3s ease';
highlightTimeout = setTimeout(() => {
statsWindow.style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; // 恢復白色半透明
}, HIGHLIGHT_DURATION);
}
// 創建統計視窗
function createStatsWindow() {
if (statsWindow) {
statsWindow.remove();
}
statsWindow = document.createElement('div');
statsWindow.id = 'superchat-stats-window';
statsWindow.style.position = 'fixed';
statsWindow.style.top = '60px';
statsWindow.style.left = '40px'; // 為圓圈留出空間
statsWindow.style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; // 改為白色半透明背景
statsWindow.style.color = '#333'; // 文字顏色改為深色
statsWindow.style.padding = '8px';
statsWindow.style.borderRadius = '5px';
statsWindow.style.zIndex = '9998';
statsWindow.style.fontFamily = 'Arial, sans-serif';
statsWindow.style.fontSize = '12px';
statsWindow.style.minWidth = '150px';
statsWindow.style.display = statsWindowVisible ? 'block' : 'none';
statsWindow.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
statsWindow.style.border = '1px solid #ddd'; // 邊框改為淺灰色
statsWindow.style.transition = 'all 0.3s ease';
// 關閉按鈕 (使用SVG圖標)
const closeButton = document.createElement('div');
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.style.cursor = 'pointer';
closeButton.style.width = '16px';
closeButton.style.height = '16px';
const closeIcon = createSVGIcon('close');
closeButton.appendChild(closeIcon);
closeButton.onclick = () => {
statsWindowVisible = false;
statsWindow.style.display = 'none';
isTrackingEnabled = false;
createToggleCircle();
toggleCircle.style.display = 'block';
};
statsWindow.appendChild(closeButton);
// 統計內容
const content = document.createElement('div');
content.style.marginTop = '5px';
// 預設貨幣統計
const statsLabel = document.createElement('div');
statsLabel.id = 'stats-label';
statsLabel.textContent = `金額: ${superChatStats.amount.toFixed(2)} (${superChatStats.count})`;
content.appendChild(statsLabel);
statsWindow.appendChild(content);
// 重置按鈕 (使用SVG圖標)
const resetButton = document.createElement('div');
resetButton.style.position = 'absolute';
resetButton.style.bottom = '5px';
resetButton.style.right = '5px';
resetButton.style.cursor = 'pointer';
resetButton.style.width = '16px';
resetButton.style.height = '16px';
const resetIcon = createSVGIcon('reset');
resetButton.appendChild(resetIcon);
resetButton.onclick = () => {
superChatStats = {
amount: 0,
count: 0
};
updateStatsWindow();
};
statsWindow.appendChild(resetButton);
document.body.appendChild(statsWindow);
}
// 更新統計視窗內容
function updateStatsWindow() {
if (!statsWindow) return;
const statsLabel = statsWindow.querySelector('#stats-label');
if (statsLabel) {
statsLabel.textContent = `金額: ${superChatStats.amount.toFixed(2)} (${superChatStats.count})`;
}
}
// 防抖函數
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 trackSuperChats() {
if (!isTrackingEnabled) return;
const superChats = Array.from(chatContainer.querySelectorAll('yt-live-chat-paid-message-renderer'));
superChats.forEach(superChat => {
if (superChat.dataset.processed) return;
superChat.dataset.processed = 'true';
try {
const amountElement = superChat.querySelector('#purchase-amount');
if (amountElement) {
const amountText = amountElement.textContent.trim();
const amountMatch = amountText.match(new RegExp(`^\\${DEFAULT_CURRENCY}([\\d,.]+)`));
if (amountMatch) {
const amount = parseFloat(amountMatch[1].replace(',', ''));
superChatStats.amount += amount;
superChatStats.count += 1;
updateStatsWindow();
highlightStatsWindow();
}
}
} catch (error) {
console.error('Error processing super chat:', error);
}
});
}
// 創建顏色選單
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);
}
}
});
// 初始化
createStatsWindow();
createToggleCircle();
// MutationObserver 監聽
const observer = new MutationObserver(debounce(() => {
highlightMessages();
markDuplicateMessages();
handleBlockedUsers();
removePinnedMessage();
trackSuperChats();
}, 300));
observer.observe(chatContainer, { childList: true, subtree: true });
})();