您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
提供高亮訊息、封鎖用戶、編輯顏色名單、移除聊天室置頂功能,並統計超級留言金額,美化統計視窗。
当前为
// ==UserScript== // @name Youtube聊天式管理+統計 // @namespace http://tampermonkey.net/ // @version 11.0 // @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 }); })();