YouTube 超級留言金額統計

支援多分頁獨立統計的簡潔版貨幣統計工具,最多5個實例

目前為 2025-04-02 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube 超級留言金額統計
// @namespace    http://tampermonkey.net/
// @version      4.2
// @description  支援多分頁獨立統計的簡潔版貨幣統計工具,最多5個實例
// @match        *://www.youtube.com/live_chat*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 貨幣匯率設定
    const CURRENCY_RATES = {
        '$': 1,       // 新台幣
        'US$': 33,    // 美元
        'SGD': 24.7,  // 新加坡幣
        'HK$': 4.25,  // 港幣
        '¥': 0.22,    // 日圓
        '£': 42,      // 英鎊
        '€': 35,      // 歐元
        'AU$': 20     // 澳幣
    };

    // 最大實例數量
    const MAX_INSTANCES = 5;

    // 生成隨機ID
    function generateId() {
        return 'sc-' + Math.random().toString(36).substr(2, 9);
    }

    // 獲取或創建實例ID
    function getInstanceId() {
        let instanceId = sessionStorage.getItem('sc-instance-id');

        if (!instanceId) {
            instanceId = generateId();
            sessionStorage.setItem('sc-instance-id', instanceId);
            cleanupOldInstances();
        }

        return instanceId;
    }

    // 清理舊實例數據
    function cleanupOldInstances() {
        try {
            const allKeys = GM_listValues();
            const instanceKeys = allKeys.filter(key => key.startsWith('sc-instance-'));

            if (instanceKeys.length > MAX_INSTANCES) {
                const sorted = instanceKeys.map(key => {
                    const data = JSON.parse(GM_getValue(key, '{}'));
                    return { key, timestamp: data.timestamp || 0 };
                }).sort((a, b) => a.timestamp - b.timestamp);

                for (let i = 0; i < instanceKeys.length - MAX_INSTANCES; i++) {
                    GM_deleteValue(sorted[i].key);
                }
            }
        } catch (e) {
            console.error('清理實例數據失敗:', e);
        }
    }

    // 狀態管理
    function loadState(instanceId) {
        const saved = GM_getValue(`sc-instance-${instanceId}`, '{}');
        const parsed = JSON.parse(saved);

        return {
            totalAmount: parsed.totalAmount || 0,
            totalCount: parsed.totalCount || 0,
            isActive: parsed.isActive !== undefined ? parsed.isActive : true,
            processedIds: new Set(parsed.processedIds || []),
            timestamp: Date.now()
        };
    }

    function saveState(instanceId, state) {
        const data = {
            totalAmount: state.totalAmount,
            totalCount: state.totalCount,
            isActive: state.isActive,
            processedIds: Array.from(state.processedIds),
            timestamp: Date.now()
        };

        GM_setValue(`sc-instance-${instanceId}`, JSON.stringify(data));
    }

    // 創建UI
    function createUI(instanceId, state) {
        const style = document.createElement('style');
        style.textContent = `
            .sc-stats-container {
                position: fixed;
                top: 30px;
                left: 0;
                z-index: 9999;
                display: flex;
                gap: 10px;
                align-items: center;
                font: bold 14px Arial, sans-serif;
            }
            .sc-stats-window {
                background: transparent;
                border-radius: 5px;
                padding: 8px;
                display: ${state.isActive ? 'flex' : 'none'};
                align-items: center;
                gap: 10px;
            }
            .sc-stats-content {
                color: white;
                text-shadow:
                    -1px -1px 0 #000,
                    1px -1px 0 #000,
                    -1px 1px 0 #000,
                    1px 1px 0 #000;
                white-space: nowrap;
            }
            .sc-stats-btn {
                cursor: pointer;
                opacity: 0.8;
                color: white;
                text-shadow: inherit;
            }
            .sc-stats-btn:hover {
                opacity: 1;
            }
            .sc-stats-toggle {
                width: 24px;
                height: 24px;
                background: transparent;
                border-radius: 3px;
                display: ${state.isActive ? 'none' : 'flex'};
                align-items: center;
                justify-content: center;
                cursor: pointer;
            }
        `;
        document.head.appendChild(style);

        // 容器
        const container = document.createElement('div');
        container.className = 'sc-stats-container';
        container.id = `sc-container-${instanceId}`;

        // 主視窗
        const window = document.createElement('div');
        window.className = 'sc-stats-window';
        window.id = `sc-window-${instanceId}`;

        const dollarBtn = document.createElement('span');
        dollarBtn.className = 'sc-stats-btn';
        dollarBtn.textContent = '$';
        dollarBtn.addEventListener('click', () => closeStats(instanceId));

        const content = document.createElement('div');
        content.className = 'sc-stats-content';
        content.id = `sc-content-${instanceId}`;
        content.textContent = `${state.totalAmount.toFixed(2)} (${state.totalCount})`;

        const resetBtn = document.createElement('span');
        resetBtn.className = 'sc-stats-btn';
        resetBtn.textContent = '重置';
        resetBtn.addEventListener('click', () => resetStats(instanceId));

        window.append(dollarBtn, content, resetBtn);

        // 切換按鈕
        const toggle = document.createElement('div');
        toggle.className = 'sc-stats-toggle';
        toggle.id = `sc-toggle-${instanceId}`;
        toggle.textContent = '$';
        toggle.addEventListener('click', () => openStats(instanceId));

        container.append(window, toggle);
        document.body.appendChild(container);
    }

    // 金額解析器
    function parseAmount(text) {
        const cleanText = text.replace(/,|\s+/g, '');
        const pattern = new RegExp(`(SGD|US\\$|HK\\$|AU\\$|\\$|¥|£|€)(\\d+\\.?\\d*)|(\\d+\\.?\\d*)(SGD|US\\$|HK\\$|AU\\$|\\$|¥|£|€)`);
        const match = cleanText.match(pattern);

        if (!match) return null;

        const currency = match[1] || match[4];
        const amount = parseFloat(match[2] || match[3]);

        return currency in CURRENCY_RATES ? {
            amount: amount,
            currency: currency,
            converted: amount * CURRENCY_RATES[currency]
        } : null;
    }

    // 處理超級留言
    function processSuperChats(instanceId, state) {
        if (!state.isActive) return;

        const elements = document.querySelectorAll(`
            yt-live-chat-paid-message-renderer #purchase-amount,
            yt-live-chat-paid-sticker-renderer #purchase-amount-chip
        `);

        let updated = false;

        elements.forEach(el => {
            const parent = el.closest('yt-live-chat-paid-message-renderer, yt-live-chat-paid-sticker-renderer');
            if (!parent || state.processedIds.has(parent.id)) return;

            const parsed = parseAmount(el.textContent);
            if (!parsed) return;

            state.processedIds.add(parent.id);
            state.totalAmount += parsed.converted;
            state.totalCount++;
            updated = true;
        });

        if (updated) {
            updateUI(instanceId, state);
            saveState(instanceId, state);
        }

        // 清理記憶體
        if (state.processedIds.size > 1000) {
            state.processedIds = new Set([...state.processedIds].slice(-500));
        }
    }

    function updateUI(instanceId, state) {
        const content = document.getElementById(`sc-content-${instanceId}`);
        if (content) content.textContent = `${state.totalAmount.toFixed(2)} (${state.totalCount})`;
    }

    function resetStats(instanceId) {
        const state = loadState(instanceId);
        state.totalAmount = 0;
        state.totalCount = 0;
        state.processedIds.clear();
        updateUI(instanceId, state);
        saveState(instanceId, state);
    }

    function closeStats(instanceId) {
        const state = loadState(instanceId);
        state.isActive = false;
        document.getElementById(`sc-window-${instanceId}`).style.display = 'none';
        document.getElementById(`sc-toggle-${instanceId}`).style.display = 'flex';
        saveState(instanceId, state);
    }

    function openStats(instanceId) {
        const state = loadState(instanceId);
        state.isActive = true;
        document.getElementById(`sc-window-${instanceId}`).style.display = 'flex';
        document.getElementById(`sc-toggle-${instanceId}`).style.display = 'none';
        saveState(instanceId, state);
    }

    function init() {
        const instanceId = getInstanceId();
        const state = loadState(instanceId);

        createUI(instanceId, state);

        const observer = new MutationObserver(() => processSuperChats(instanceId, state));
        const chatContainer = document.querySelector('#chat');

        if (chatContainer) {
            observer.observe(chatContainer, {
                childList: true,
                subtree: true
            });
        }

        // 定期檢查以確保不會漏掉任何訊息
        setInterval(() => processSuperChats(instanceId, state), 250);

        // 頁面卸載時保存狀態
        window.addEventListener('beforeunload', () => {
            saveState(instanceId, state);
        });
    }

    // 啟動腳本
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();