YouTube捐款统计器

自动统计YouTube评论区的捐款信息,支持多种货币

// ==UserScript==
// @name         YouTube捐款统计器
// @name:en     YouTube Donation Tracker
// @namespace    https://github.com/yourusername/yt-donation-tracker
// @version      1.0.3
// @description  自动统计YouTube评论区的捐款信息,支持多种货币
// @description:en  Automatically track donation information from YouTube comment sections, supporting multiple currencies
// @author       Jorkey Liu
// @match        https://www.youtube.com/watch*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // =========================
    // 常量定义
    // =========================
    const CONFIG = {
        debug: true,                 // 调试模式
        scanInterval: 1000,          // 扫描间隔(毫秒)
        language: 'zh'               // 默认语言:'zh'中文,'en'英文
    };
    
    const SELECTORS = {
        commentsSection: 'ytd-comments#comments',
        commentThreads: 'ytd-comment-thread-renderer',
        commentContent: '#content-text',
        authorText: '#author-text',
        loadingIndicator: 'yt-next-continuation',
        
        // 捐款相关选择器
        donationChipPrice: 'span#comment-chip-price, .yt-spec-button-shape-next__button-text-content',  // 捐款价格元素,包括一般超级留言和其他按钮文本
        priceElements: '[id*="price"], [class*="price"], span.style-scope.yt-formatted-string'  // 通用价格元素选择器
    };
    
    const UI_TEXT = {
        zh: {
            panelTitle: 'YouTube捐款统计',
            toggleButton: '捐款统计',
            startButton: '开始统计',
            stopButton: '停止',
            exportButton: '导出数据',
            detailsButton: '查看详情',
            statusReady: '就绪 (请点击开始)',
            statusScanning: '正在扫描中...请手动滚动页面加载评论',
            noDataTip: '暂未检测到捐款',
            statsTitle: '捐款统计:',
            noStatsData: '暂无捐款数据',
            scannedComments: '已扫描评论: ',
            detectedDonations: '检测到捐款: ',
            donationDetailsTitle: '捐款详情',
            noDonationData: '暂无捐款数据',
            donationSummary: '总计: {total}笔捐款,来自{comments}条评论',
            donationTableHeaders: ['用户', '捐款金额', '评论内容'],
            exportNoData: '暂无数据可导出',
            errorPrefix: '发生错误: ',
            csvHeaders: '用户,捐款金额,货币,评论内容'
        },
        en: {
            panelTitle: 'YouTube Donation Tracker',
            toggleButton: 'Donations',
            startButton: 'Start Scanning',
            stopButton: 'Stop',
            exportButton: 'Export Data',
            detailsButton: 'View Details',
            statusReady: 'Ready (click to start)',
            statusScanning: 'Scanning...please scroll to load more comments',
            noDataTip: 'No donations detected yet',
            statsTitle: 'Donation Statistics:',
            noStatsData: 'No donation data',
            scannedComments: 'Comments Scanned: ',
            detectedDonations: 'Donations Detected: ',
            donationDetailsTitle: 'Donation Details',
            noDonationData: 'No donation data available',
            donationSummary: 'Total: {total} donations from {comments} comments',
            donationTableHeaders: ['User', 'Amount', 'Comment'],
            exportNoData: 'No data to export',
            errorPrefix: 'Error: ',
            csvHeaders: 'User,Amount,Currency,Comment'
        }
    };
    
    // =========================
    // 应用状态
    // =========================
    const AppState = {
        isScanning: false,      // 是否正在扫描
        totalProcessed: 0,      // 已处理评论数
        scanInterval: null,     // 扫描定时器
        scanStartTime: null,    // 扫描开始时间
        scanCount: 0,           // 当前扫描次数
        processedCommentIds: new Set(), // 已处理评论ID集合
        
        // 统计数据
        statistics: {
            totalDonations: 0,       // 总捐款数
            totalComments: 0,        // 总评论数
            currencyStats: {},       // 按币种统计: { "US$": { count: 0, total: 0, originalTexts: [] }, ... }
            donationComments: []     // 包含捐款的评论列表
        },
        
        // 重置统计数据
        resetStats() {
            this.statistics = {
                totalDonations: 0,
                totalComments: 0,
                currencyStats: {},
                donationComments: []
            };
            return this.statistics;
        },
        
        // 更新状态
        updateState(changes) {
            Object.assign(this, changes);
        }
    };
    
    // 初始化状态
    AppState.resetStats();
    
    // =========================
    // 数据模型
    // =========================
    
    /**
     * 评论数据模型
     * @typedef {Object} CommentData
     * @property {string} id - 评论ID
     * @property {string} author - 评论作者
     * @property {string} content - 评论内容
     * @property {Array<DonationData>} donations - 捐款数据列表
     */
    
    /**
     * 捐款数据模型
     * @typedef {Object} DonationData
     * @property {string} currencySymbol - 币种符号(原始文本格式)
     * @property {string} amount - 金额(原始文本格式)
     * @property {string} rawText - 原始完整文本
     */
    
    /**
     * 创建评论数据对象
     * @param {Element} commentElement - 评论DOM元素
     * @returns {CommentData}
     */
    function createCommentData(commentElement) {
        // 生成唯一ID
        const id = 'comment_' + Math.random().toString(36).substr(2, 9);
        
        // 获取作者信息
        const authorElement = commentElement.querySelector(SELECTORS.authorText);
        const author = authorElement ? authorElement.textContent.trim() : '未知用户';
        
        // 获取评论内容
        const contentElement = commentElement.querySelector(SELECTORS.commentContent);
        const content = contentElement ? contentElement.textContent.trim() : '';
        
        return {
            id,
            author,
            content,
            donations: [] // 初始为空数组,后续处理时添加
        };
    }
    
    // =========================
    // 捐款提取模块
    // =========================
    
    /**
     * 从捐款文本中提取币种和金额
     * @param {string} text - 捐款文本 (如 "US$199.99", "JP¥10,000", "€20.00" 等)
     * @returns {Object|null} 包含currencySymbol和amount的对象,或null
     */
    function parseDonationText(text) {
        // 清理文本
        const cleanText = text.trim();
        
        // 使用正则表达式找出数字部分(包括逗号和小数点)
        const numberMatch = /([\d,]+(\.\d{1,2})?)/g.exec(cleanText);
        
        if (!numberMatch) {
            return null; // 没有找到数字,不是有效的捐款格式
        }
        
        // 提取数字部分(保持原始格式,包括逗号)
        const amount = numberMatch[0];
        
        // 提取币种部分(数字之前的所有非空白字符)
        const startPos = cleanText.indexOf(amount);
        const currencySymbol = startPos > 0 ? cleanText.substring(0, startPos).trim() : '';
        
        // 如果没有币种标识,则不是有效捐款
        if (!currencySymbol) {
            return null;
        }
        
        return { 
            currencySymbol, 
            amount,
            rawText: cleanText
        };
    }
    
    /**
     * 从评论中查找并处理捐款信息
     * @param {Element} commentElement - 评论DOM元素
     * @returns {Array} 捐款数据数组
     */
    function extractDonationsFromComment(commentElement) {
        const donations = [];
        
        // 直接查找捐款价格元素 - 先尝试精确选择器
        let priceElements = commentElement.querySelectorAll(SELECTORS.donationChipPrice);
        
        // 如果没有找到,尝试使用更广泛的选择器
        if (!priceElements || priceElements.length === 0) {
            priceElements = commentElement.querySelectorAll(SELECTORS.priceElements);
        }
        
        if (priceElements && priceElements.length > 0) {
            // 处理每个价格元素
            priceElements.forEach(priceElement => {
                const priceText = priceElement.textContent.trim();
                
                // 解析捐款文本获取币种和金额
                const donationInfo = parseDonationText(priceText);
                if (donationInfo) {
                    donations.push({
                        currencySymbol: donationInfo.currencySymbol,
                        amount: donationInfo.amount,
                        rawText: donationInfo.rawText
                    });
                    
                    log(`检测到捐款: ${donationInfo.currencySymbol} ${donationInfo.amount}`, priceText);
                }
            });
        }
        
        return donations;
    }
    
    /**
     * 分析评论内容并更新统计
     * @param {CommentData} commentData - 评论数据
     * @param {Element} commentElement - 评论DOM元素
     * @returns {boolean} 是否检测到捐款
     */
    function analyzeComment(commentData, commentElement) {
        // 从评论DOM中提取捐款信息
        const donations = extractDonationsFromComment(commentElement);
        
        // 如果没有检测到捐款,返回false
        if (donations.length === 0) {
            return false;
        }
        
        // 将捐款信息添加到评论数据
        commentData.donations = donations;
        
        // 更新统计数据
        updateDonationStats(commentData);
        
        return true;
    }
    
    /**
     * 更新捐款统计数据
     * @param {CommentData} commentData - 包含捐款信息的评论数据
     */
    function updateDonationStats(commentData) {
        const { statistics } = AppState;
        
        // 如果评论包含捐款,添加到捐款评论列表
        if (commentData.donations.length > 0) {
            statistics.donationComments.push(commentData);
            statistics.totalDonations += commentData.donations.length;
            
            // 按币种更新统计
            commentData.donations.forEach(donation => {
                const { currencySymbol, amount, rawText } = donation;
                
                // 如果这个币种还没有统计记录,创建一个
                if (!statistics.currencyStats[currencySymbol]) {
                    statistics.currencyStats[currencySymbol] = {
                        count: 0,
                        total: 0,
                        originalTexts: []
                    };
                }
                
                // 更新统计
                const currencyStat = statistics.currencyStats[currencySymbol];
                currencyStat.count++;
                currencyStat.originalTexts.push(rawText);
                
                // 提取数值部分用于累加(去除逗号)
                const numericAmount = parseFloat(amount.replace(/,/g, ''));
                if (!isNaN(numericAmount)) {
                    currencyStat.total += numericAmount;
                }
            });
        }
    }
    
    /**
     * 格式化捐款金额显示
     * @param {string} currencySymbol - 币种符号
     * @param {string} amount - 金额
     * @returns {string} 格式化后的金额字符串
     */
    function formatDonation(currencySymbol, amount) {
        return `${currencySymbol}${amount}`;
    }
    
    // =========================
    // 工具函数
    // =========================
    
    /**
     * 调试日志输出
     * @param {string} message - 日志消息
     * @param {any} data - 相关数据(可选)
     */
    function log(message, data) {
        if (CONFIG.debug) {
            if (data !== undefined) {
                console.log(`[YT捐款统计] ${message}`, data);
            } else {
                console.log(`[YT捐款统计] ${message}`);
            }
        }
    }
    
    /**
     * 检查当前是否为YouTube视频页面
     * @returns {boolean}
     */
    function isYouTubeVideoPage() {
        return window.location.href.includes('youtube.com/watch');
    }
    
    /**
     * 等待元素出现在DOM中
     * @param {string} selector - CSS选择器
     * @param {number} timeout - 超时时间(毫秒)
     * @returns {Promise<Element>}
     */
    function waitForElement(selector, timeout = 30000) {
        return new Promise((resolve, reject) => {
            const element = document.querySelector(selector);
            if (element) {
                return resolve(element);
            }
            
            const observer = new MutationObserver((mutations, observer) => {
                const element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    resolve(element);
                }
            });
            
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
            
            // 设置超时
            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`等待元素 ${selector} 超时`));
            }, timeout);
        });
    }
    
    /**
     * 防抖函数
     * @param {Function} func - 要执行的函数
     * @param {number} wait - 等待时间(毫秒)
     * @returns {Function} 防抖处理后的函数
     */
    function debounce(func, wait = 300) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
    
    /**
     * 节流函数
     * @param {Function} func - 要执行的函数
     * @param {number} limit - 限制时间(毫秒)
     * @returns {Function} 节流处理后的函数
     */
    function throttle(func, limit = 300) {
        let inThrottle;
        return function executedFunction(...args) {
            if (!inThrottle) {
                func(...args);
                inThrottle = true;
                setTimeout(() => {
                    inThrottle = false;
                }, limit);
            }
        };
    }
    
    /**
     * 获取当前语言的UI文本
     * @param {string} key - 文本键名
     * @returns {string|array} - 对应语言的文本
     */
    function getText(key) {
        const lang = CONFIG.language;
        return UI_TEXT[lang][key] || UI_TEXT['en'][key]; // 如果找不到则使用中文作为后备
    }
    
    /**
     * 错误处理器
     * @param {Error} error - 错误对象
     * @param {string} context - 错误上下文
     */
    function handleError(error, context) {
        log(`错误 [${context}]:`, error);
        
        // 在UI上显示错误
        const status = document.getElementById('yt-donation-status');
        if (status) {
            status.textContent = `${getText('errorPrefix')}${error.message}`;
            status.style.color = '#f44336';
        }
        
        // 如果正在扫描,尝试恢复
        if (AppState.isScanning) {
            log('尝试恢复扫描...');
            // 短暂延迟后尝试继续
            setTimeout(() => {
                if (AppState.isScanning) {
                    processNewComments();
                }
            }, 2000);
        }
    }
    
    /**
     * 等待评论区加载完成
     * @returns {Promise<Element>}
     */
    async function waitForComments() {
        log('等待评论区加载...');
        try {
            const commentsSection = await waitForElement(SELECTORS.commentsSection);
            log('评论区已加载');
            return commentsSection;
        } catch (error) {
            log('评论区加载失败', error);
            throw error;
        }
    }
    
    /**
     * 监听页面URL变化
     * @param {Function} callback - URL变化时的回调函数
     */
    function watchPageChanges(callback) {
        let lastUrl = location.href;
        
        // 创建MutationObserver监听URL变化
        const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                callback(location.href);
            }
        });
        
        // 开始观察
        observer.observe(document.querySelector('body'), {
            childList: true,
            subtree: true
        });
        
        return observer;
    }
    
    // =========================
    // DOM分析相关函数
    // =========================
    
    /**
     * 初始化评论区监视器
     * @param {Element} commentsSection - 评论区容器元素
     */
    function initObservers(commentsSection) {
        log('初始化评论区监视器');
        
        // 创建MutationObserver监听评论区变化
        const commentObserver = new MutationObserver((mutations) => {
            if (AppState.isScanning) {
                // 如果正在扫描中,检查新增评论
                const newComments = getUnprocessedComments();
                if (newComments.length > 0) {
                    log(`检测到${newComments.length}条新评论`);
                    // 重置无新评论计数
                    AppState.noNewCommentsCount = 0;
                }
            }
        });
        
        // 开始观察评论区变化
        commentObserver.observe(commentsSection, {
            childList: true,
            subtree: true
        });
        
        log('评论区监视器已初始化');
    }
    
    /**
     * 获取未处理的评论元素
     * @returns {NodeList}
     */
    function getUnprocessedComments() {
        // 获取所有评论元素
        const allComments = document.querySelectorAll(SELECTORS.commentThreads);
        // 过滤出未处理的评论
        return Array.from(allComments).filter(comment => {
            // 为每个评论生成一个唯一ID
            const commentId = generateCommentId(comment);
            // 检查是否已经处理过
            return !AppState.processedCommentIds.has(commentId);
        });
    }
    
    /**
     * 为评论生成唯一ID
     * @param {Element} commentElement - 评论DOM元素
     * @returns {string} 唯一ID
     */
    function generateCommentId(commentElement) {
        // 获取作者和内容作为ID的组成部分
        const authorElement = commentElement.querySelector(SELECTORS.authorText);
        const contentElement = commentElement.querySelector(SELECTORS.commentContent);
        
        const author = authorElement ? authorElement.textContent.trim() : '';
        const content = contentElement ? contentElement.textContent.trim() : '';
        
        // 组合作者和内容的简短摘要作为ID
        return `${author.slice(0, 20)}_${content.slice(0, 30)}`;
    }
    
    /**
     * 处理所有未处理的评论
     * @returns {number} 处理的评论数量
     */
    function processNewComments() {
        // 获取所有未处理评论
        const newComments = getUnprocessedComments();
        let processedCount = 0;
        
        if (newComments.length === 0) {
            log(`未找到新评论,已处理总数: ${AppState.totalProcessed}`);
            return 0;
        }
        
        // 处理每条评论
        newComments.forEach(commentElement => {
            try {
                // 创建评论数据
                const commentData = createCommentData(commentElement);
                
                // 生成并记录评论ID
                const commentId = generateCommentId(commentElement);
                AppState.processedCommentIds.add(commentId);
                
                // 更新统计
                AppState.statistics.totalComments++;
                AppState.totalProcessed++;
                
                // 分析评论
                analyzeComment(commentData, commentElement);
                
                processedCount++;
            } catch (error) {
                log('处理评论时出错', error);
            }
        });
        
        if (processedCount > 0) {
            log(`已处理${processedCount}条新评论,累计: ${AppState.totalProcessed}`);
            updateUI(); // 更新UI显示
        }
        
        return processedCount;
    }
    
    /**
     * 开始扫描评论
     */
    function startScanning() {
        if (AppState.isScanning) {
            log('已经在扫描中');
            return;
        }
        
        log('开始扫描评论');
        
        // 更新状态
        AppState.updateState({
            isScanning: true,
            scanStartTime: Date.now(),
            scanCount: 0,
            totalProcessed: 0,
            processedCommentIds: new Set()
        });
        
        // 重置统计
        AppState.resetStats();
        
        // 初始扫描一次
        processNewComments();
        
        // 设置定时扫描
        AppState.scanInterval = setInterval(() => {
            // 更新扫描计数
            AppState.scanCount++;
            
            // 处理新评论
            processNewComments();
            
            
            // 更新状态显示
            updateUIState();
        }, CONFIG.scanInterval);
        
        // 更新UI状态
        updateUIState();
    }
    
    /**
     * 停止扫描评论
     */
    function stopScanning() {
        if (!AppState.isScanning) {
            return;
        }
        
        log('停止扫描评论');
        
        // 清除定时器
        if (AppState.scanInterval) {
            clearInterval(AppState.scanInterval);
            AppState.scanInterval = null;
        }
        
        // 更新状态
        AppState.updateState({
            isScanning: false
        });
        
        // 更新UI状态
        updateUIState();
        
        // 显示最终统计结果
        log('扫描完成,最终统计', AppState.statistics);
    }
    
    // =========================
    // UI相关函数
    // =========================
    
    /**
     * 创建UI控制面板
     */
    function createUI() {
        log('创建UI控制面板');
        
        // 创建主容器
        const panel = document.createElement('div');
        panel.id = 'yt-donation-tracker-panel';
        panel.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 280px;
            background: white;
            border: 1px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 9999;
            font-family: Arial, sans-serif;
            padding: 12px;
            color: #333;
            display: none;
        `;
        
        // 创建标题容器
        const title = document.createElement('div');
        title.style.cssText = `
            font-weight: bold;
            font-size: 14px;
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;
        
        // 创建标题文本元素
        const titleText = document.createElement('span');
        titleText.id = 'yt-donation-title-text';
        titleText.textContent = getText('panelTitle');
        title.appendChild(titleText);
        
        // 添加关闭按钮
        const closeBtn = document.createElement('span');
        closeBtn.textContent = '×';
        closeBtn.style.cssText = `
            cursor: pointer;
            padding: 0 5px;
            font-size: 16px;
        `;
        closeBtn.addEventListener('click', () => {
            // 如果正在扫描,先停止扫描
            if (AppState.isScanning) {
                stopScanning();
            }
            // 然后隐藏面板
            panel.style.display = 'none';
            toggleButton.style.display = 'block';
        });
        title.appendChild(closeBtn);
        
        // 创建内容容器
        const content = document.createElement('div');
        content.id = 'yt-donation-tracker-content';
        
        // 创建按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            gap: 8px;
            margin-bottom: 10px;
        `;
        
        // 创建操作按钮(开始/停止)
        const actionBtn = document.createElement('button');
        actionBtn.id = 'yt-donation-action-btn';
        actionBtn.textContent = getText('startButton');
        actionBtn.style.cssText = buttonStyle();
        actionBtn.addEventListener('click', () => {
            if (AppState.isScanning) {
                stopScanning();
            } else {
                startScanning();
            }
        });
        
        // 添加按钮到容器
        buttonContainer.appendChild(actionBtn);
        
        // 创建状态显示
        const status = document.createElement('div');
        status.id = 'yt-donation-status';
        status.style.cssText = `
            font-size: 12px;
            margin: 8px 0;
            color: #666;
        `;
        status.textContent = getText('statusReady');
        
        // 创建统计结果容器
        const results = document.createElement('div');
        results.id = 'yt-donation-results';
        results.style.cssText = `
            margin-top: 10px;
            font-size: 13px;
        `;
        
        // 创建导出按钮
        const exportBtn = document.createElement('button');
        exportBtn.id = 'yt-donation-export-btn';
        exportBtn.textContent = getText('exportButton');
        exportBtn.style.cssText = buttonStyle('#2196f3');
        exportBtn.style.display = 'none';
        exportBtn.addEventListener('click', exportData);
        
        // 创建详情按钮
        const detailsBtn = document.createElement('button');
        detailsBtn.id = 'yt-donation-details-btn';
        detailsBtn.textContent = getText('detailsButton');
        detailsBtn.style.cssText = buttonStyle('#ff9800');
        detailsBtn.style.display = 'none';
        detailsBtn.addEventListener('click', showDetailsModal);
        
        // 创建操作按钮容器
        const actionContainer = document.createElement('div');
        actionContainer.style.cssText = `
            display: flex;
            gap: 8px;
            margin-top: 10px;
        `;
        actionContainer.appendChild(exportBtn);
        actionContainer.appendChild(detailsBtn);
        
        // 创建语言切换按钮
        const langBtn = document.createElement('button');
        langBtn.id = 'yt-donation-lang-btn';
        langBtn.textContent = CONFIG.language === 'zh' ? 'EN' : '中';
        langBtn.style.cssText = `
            position: absolute;
            top: 10px;
            right: 48px;
            padding: 2px 4px;
            font-size: 10px;
            background: #eee;
            border: none;
            border-radius: 2px;
            cursor: pointer;
            color: #666;
        `;
        langBtn.addEventListener('click', () => {
            // 切换语言
            CONFIG.language = CONFIG.language === 'zh' ? 'en' : 'zh';
            langBtn.textContent = CONFIG.language === 'zh' ? 'EN' : '中';
            
            // 更新UI文本
            const titleText = document.getElementById('yt-donation-title-text');
            if (titleText) {
                titleText.textContent = getText('panelTitle');
            }
            
            status.textContent = AppState.isScanning ? 
                getText('statusScanning').replace('{count}', AppState.totalProcessed)
                    .replace('{time}', Math.round((Date.now() - AppState.scanStartTime) / 1000)) : 
                getText('statusReady');
            actionBtn.textContent = AppState.isScanning ? 
                getText('stopButton') : getText('startButton');
            exportBtn.textContent = getText('exportButton');
            detailsBtn.textContent = getText('detailsButton');
            toggleButton.textContent = getText('toggleButton');
            
            // 更新统计显示
            updateUI();
        });
        
        // 组装UI
        content.appendChild(buttonContainer);
        content.appendChild(status);
        content.appendChild(results);
        content.appendChild(actionContainer);
        
        panel.appendChild(title);
        panel.appendChild(langBtn);
        panel.appendChild(content);
        
        // 创建侧边呼出按钮
        const toggleButton = document.createElement('div');
        toggleButton.id = 'yt-donation-toggle';
        toggleButton.textContent = getText('toggleButton');
        toggleButton.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: #4caf50;
            color: white;
            padding: 8px 16px;
            font-size: 13px;
            border-radius: 4px;
            cursor: pointer;
            z-index: 9998;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
            text-align: center;
            font-weight: bold;
        `;
        toggleButton.addEventListener('click', () => {
            panel.style.display = 'block';
            toggleButton.style.display = 'none';
        });
        
        // 添加到页面
        document.body.appendChild(panel);
        document.body.appendChild(toggleButton);
        
        // 初始化UI
        updateUI();
        
        log('UI控制面板已创建');
        
        return { panel, toggleButton };
    }
    
    /**
     * 按钮样式生成器
     * @param {string} bgColor - 背景颜色
     * @returns {string} CSS样式字符串
     */
    function buttonStyle(bgColor = '#4caf50') {
        return `
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            background: ${bgColor};
            color: white;
            font-size: 12px;
            cursor: pointer;
            flex: 1;
        `;
    }
    
    /**
     * 更新UI显示
     */
    function updateUI() {
        const results = document.getElementById('yt-donation-results');
        if (!results) return;
        
        const { statistics } = AppState;
        
        // 清空结果容器
        while (results.firstChild) {
            results.removeChild(results.firstChild);
        }
        
        // 创建扫描计数元素
        const scanCountDiv = document.createElement('div');
        scanCountDiv.style.marginBottom = '5px';
        scanCountDiv.appendChild(document.createTextNode(getText('scannedComments')));
        const scanCountStrong = document.createElement('strong');
        scanCountStrong.textContent = statistics.totalComments;
        scanCountDiv.appendChild(scanCountStrong);
        results.appendChild(scanCountDiv);
        
        // 创建捐款计数元素
        const donationCountDiv = document.createElement('div');
        donationCountDiv.style.marginBottom = '8px';
        donationCountDiv.appendChild(document.createTextNode(getText('detectedDonations')));
        const donationCountStrong = document.createElement('strong');
        donationCountStrong.textContent = statistics.totalDonations;
        donationCountDiv.appendChild(donationCountStrong);
        results.appendChild(donationCountDiv);
        
        // 没有捐款时显示提示
        if (statistics.totalDonations === 0) {
            const noDataDiv = document.createElement('div');
            noDataDiv.style.color = '#666';
            noDataDiv.style.fontStyle = 'italic';
            noDataDiv.textContent = getText('noDataTip');
            results.appendChild(noDataDiv);
        } else {
            // 创建捐款统计标题
            const statsTitle = document.createElement('div');
            statsTitle.style.fontWeight = 'bold';
            statsTitle.style.marginTop = '5px';
            statsTitle.textContent = getText('statsTitle');
            results.appendChild(statsTitle);
            
            // 创建列表
            const statsList = document.createElement('ul');
            statsList.style.margin = '5px 0';
            statsList.style.paddingLeft = '20px';
            
            let hasDonations = false;
            
            // 遍历所有币种统计
            Object.keys(statistics.currencyStats).forEach(symbol => {
                const stat = statistics.currencyStats[symbol];
                // 只显示有捐款的币种
                if (stat.count > 0) {
                    hasDonations = true;
                    const listItem = document.createElement('li');
                    
                    // 添加币种信息
                    listItem.appendChild(document.createTextNode(`${symbol}: `));
                    
                    // 添加金额
                    const amountStrong = document.createElement('strong');
                    // 显示两位小数,除非是整数
                    let totalDisplay = stat.total;
                    if (Math.floor(stat.total) !== stat.total) {
                        totalDisplay = stat.total.toFixed(2);
                    }
                    amountStrong.textContent = totalDisplay;
                    listItem.appendChild(amountStrong);
                    
                    // 添加笔数
                    listItem.appendChild(document.createTextNode(` (${stat.count}${CONFIG.language === 'zh' ? '笔' : ''})`));
                    
                    statsList.appendChild(listItem);
                }
            });
            
            if (!hasDonations) {
                const listItem = document.createElement('li');
                listItem.textContent = getText('noStatsData');
                statsList.appendChild(listItem);
            }
            
            results.appendChild(statsList);
            
            // 显示操作按钮
            const exportBtn = document.getElementById('yt-donation-export-btn');
            const detailsBtn = document.getElementById('yt-donation-details-btn');
            
            if (exportBtn) exportBtn.style.display = 'block';
            if (detailsBtn) detailsBtn.style.display = 'block';
        }
    }
    
    /**
     * 更新UI状态
     */
    function updateUIState() {
        // 更新按钮状态
        const actionBtn = document.getElementById('yt-donation-action-btn');
        const panel = document.getElementById('yt-donation-tracker-panel');
        const toggleButton = document.getElementById('yt-donation-toggle');
        
        if (actionBtn) {
            if (AppState.isScanning) {
                actionBtn.textContent = getText('stopButton');
                actionBtn.style.background = '#f44336'; // 红色
            } else {
                actionBtn.textContent = getText('startButton');
                actionBtn.style.background = '#4caf50'; // 绿色
            }
        }
        
        // 如果正在扫描,确保面板显示
        if (AppState.isScanning && panel && toggleButton) {
            panel.style.display = 'block';
            toggleButton.style.display = 'none';
        }
        
        // 更新状态文本
        const status = document.getElementById('yt-donation-status');
        if (status) {
            if (AppState.isScanning) {
                const scannedTime = Math.round((Date.now() - AppState.scanStartTime) / 1000);
                status.textContent = getText('statusScanning')
                    .replace('{count}', AppState.totalProcessed)
                    .replace('{time}', scannedTime);
            } else {
                status.textContent = getText('statusReady');
            }
        }
    }
    
    /**
     * 创建详情模态窗口
     */
    function showDetailsModal() {
        // 移除已有的模态窗口
        const existingModal = document.getElementById('yt-donation-details-modal');
        if (existingModal) {
            existingModal.remove();
        }
        
        const { statistics } = AppState;
        
        // 创建模态窗口容器
        const modal = document.createElement('div');
        modal.id = 'yt-donation-details-modal';
        modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        `;
        
        // 添加点击空白处关闭功能
        modal.addEventListener('click', (event) => {
            // 如果点击的是模态窗口背景(而不是内容区域),关闭窗口
            if (event.target === modal) {
                modal.remove();
            }
        });
        
        // 创建模态窗口内容
        const modalContent = document.createElement('div');
        modalContent.style.cssText = `
            background: white;
            border-radius: 8px;
            max-width: 800px;
            width: 80%;
            max-height: 80vh;
            overflow-y: auto;
            padding: 20px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
        `;
        
        // 创建标题和关闭按钮
        const titleRow = document.createElement('div');
        titleRow.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        `;
        
        // 创建标题文本
        const title = document.createElement('h2');
        title.style.cssText = `
            margin: 0;
            font-size: 18px;
        `;
        title.textContent = getText('donationDetailsTitle');
        titleRow.appendChild(title);
        
        // 创建关闭按钮
        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #666;
            padding: 0 5px;
        `;
        closeBtn.addEventListener('click', () => modal.remove());
        titleRow.appendChild(closeBtn);
        
        // 创建内容
        const contentDiv = document.createElement('div');
        
        // 构建捐款详情
        if (statistics.donationComments.length === 0) {
            const noDataP = document.createElement('p');
            noDataP.textContent = getText('noDonationData');
            contentDiv.appendChild(noDataP);
        } else {
            // 创建总计信息
            const summaryDiv = document.createElement('div');
            summaryDiv.style.marginBottom = '15px';
            
            const summaryText = getText('donationSummary')
                .replace('{total}', statistics.totalDonations)
                .replace('{comments}', statistics.donationComments.length);
            summaryDiv.textContent = summaryText;
            
            contentDiv.appendChild(summaryDiv);
            
            // 创建表格
            const table = document.createElement('table');
            table.style.width = '100%';
            table.style.borderCollapse = 'collapse';
            
            // 创建表头
            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            
            const headers = getText('donationTableHeaders');
            headers.forEach(headerText => {
                const th = document.createElement('th');
                th.style.textAlign = 'left';
                th.style.padding = '8px';
                th.style.borderBottom = '1px solid #ddd';
                th.textContent = headerText;
                headerRow.appendChild(th);
            });
            
            thead.appendChild(headerRow);
            table.appendChild(thead);
            
            // 创建表格内容
            const tbody = document.createElement('tbody');
            
            // 添加每条捐款评论
            statistics.donationComments.forEach(comment => {
                const row = document.createElement('tr');
                
                // 用户单元格
                const userCell = document.createElement('td');
                userCell.style.padding = '8px';
                userCell.style.borderBottom = '1px solid #eee';
                userCell.textContent = comment.author;
                row.appendChild(userCell);
                
                // 捐款金额单元格
                const donationCell = document.createElement('td');
                donationCell.style.padding = '8px';
                donationCell.style.borderBottom = '1px solid #eee';
                
                // 构建该评论的所有捐款文本
                const donationsText = comment.donations.map(d => 
                    formatDonation(d.currencySymbol, d.amount)
                ).join(', ');
                donationCell.textContent = donationsText;
                
                row.appendChild(donationCell);
                
                // 评论内容单元格
                const contentCell = document.createElement('td');
                contentCell.style.padding = '8px';
                contentCell.style.borderBottom = '1px solid #eee';
                contentCell.textContent = comment.content;
                row.appendChild(contentCell);
                
                tbody.appendChild(row);
            });
            
            table.appendChild(tbody);
            contentDiv.appendChild(table);
        }
        
        // 组装模态窗口
        modalContent.appendChild(titleRow);
        modalContent.appendChild(contentDiv);
        modal.appendChild(modalContent);
        
        // 添加到页面
        document.body.appendChild(modal);
    }
    
    /**
     * 导出数据为CSV文件
     */
    function exportData() {
        const { statistics } = AppState;
        
        if (statistics.donationComments.length === 0) {
            alert(getText('exportNoData'));
            return;
        }
        
        // 创建CSV内容
        let csvContent = getText('csvHeaders') + '\n';
        
        statistics.donationComments.forEach(comment => {
            comment.donations.forEach(donation => {
                // 格式化CSV行
                const row = [
                    csvEscape(comment.author),
                    donation.amount,
                    donation.currencySymbol,
                    csvEscape(comment.content)
                ];
                
                csvContent += row.join(',') + '\n';
            });
        });
        
        // 使用Blob创建CSV文件
        const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        
        // 创建下载链接
        const link = document.createElement('a');
        link.setAttribute('href', url);
        link.setAttribute('download', `youtube_donations_${Date.now()}.csv`);
        link.style.display = 'none';
        document.body.appendChild(link);
        
        // 触发下载
        link.click();
        
        // 清理
        document.body.removeChild(link);
        URL.revokeObjectURL(url); // 释放URL对象
    }
    
    /**
     * 转义CSV中的文本
     * @param {string} text - 原始文本
     * @returns {string} 转义后的文本
     */
    function csvEscape(text) {
        // 如果文本中包含逗号、引号或换行符,需要用引号包裹
        if (/[",\n\r]/.test(text)) {
            // 将文本中的引号替换为两个引号
            return '"' + text.replace(/"/g, '""') + '"';
        }
        return text;
    }
    
    /**
     * 转义HTML特殊字符
     * @param {string} text - 原始文本
     * @returns {string} 转义后的文本
     */
    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }
    
    // =========================
    // 主逻辑
    // =========================
    
    /**
     * 脚本初始化
     */
    async function init() {
        log('YouTube捐款统计器初始化...');
        
        // 检查是否在YouTube视频页面
        if (!isYouTubeVideoPage()) {
            log('不是YouTube视频页面,等待页面变化');
            // 监听页面变化
            watchPageChanges(url => {
                if (url.includes('youtube.com/watch')) {
                    log('检测到视频页面,开始初始化');
                    initOnVideoPage();
                }
            });
            return;
        }
        
        await initOnVideoPage();
    }
    
    /**
     * 在视频页面上初始化
     */
    async function initOnVideoPage() {
        try {
            // 清理旧的UI和状态
            cleanup();
            
            // 等待评论区加载
            const commentsSection = await waitForComments();
            
            // 创建UI
            const { panel, toggleButton } = createUI();
            
            // 初始化观察器
            initObservers(commentsSection);
            
            log('初始化完成');
        } catch (error) {
            handleError(error, '初始化');
            
            // 如果是评论区加载失败,可能是YouTube的SPA导航,尝试延迟重试
            setTimeout(() => {
                if (isYouTubeVideoPage()) {
                    log('尝试重新初始化...');
                    initOnVideoPage();
                }
            }, 3000);
        }
    }
    
    /**
     * 清理旧的UI和状态
     */
    function cleanup() {
        log('清理旧状态...');
        
        // 停止扫描
        stopScanning();
        
        // 移除旧UI
        const oldPanel = document.getElementById('yt-donation-tracker-panel');
        if (oldPanel) {
            oldPanel.remove();
        }
        
        // 移除旧的模态窗口
        const oldModal = document.getElementById('yt-donation-details-modal');
        if (oldModal) {
            oldModal.remove();
        }
        
        // 重置状态
        AppState.resetStats();
        AppState.updateState({
            isScanning: false,
            totalProcessed: 0,
            scanInterval: null,
            scanStartTime: null,
            scanCount: 0,
            processedCommentIds: new Set()
        });
    }
    
    // 页面加载完成后执行初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})(); 

QingJ © 2025

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