YouTube捐款统计器

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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