Youtube聊天室管理+統計

提供高亮訊息、封鎖用戶、編輯顏色名單、移除聊天室置頂功能,並統計超級留言金額,美化統計視窗。

目前為 2025-03-29 提交的版本,檢視 最新版本

// ==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 });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址