Linux Do 量子速读

帮您在Linux Do论坛中折叠无意义回复,告别水贴,光速获取信息!

// ==UserScript==
// @name         Linux Do 量子速读
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  帮您在Linux Do论坛中折叠无意义回复,告别水贴,光速获取信息!
// @author       量子咸鱼K
// @match        *://linux.do/t/topic/*
// @grant        GM_log
// @run-at       document-end
// @license      MIT
// ==/UserScript==

// 立即执行的日志,确认脚本加载
console.log('[折叠器] 脚本已加载');

(function() {
    'use strict';

    // 添加日志函数
    const DEBUG = {
        enabled: true,
        log: function(type, message, data = null) {
            if (!this.enabled) return;
            const timestamp = new Date().toISOString().split('T')[1];
            console.log(`[折叠器][${timestamp}][${type}] ${message}`, data ? data : '');
        }
    };

    // 立即执行的测试日志
    DEBUG.log('测试', '日志系统初始化成功');

    // 配置项
    const CONFIG = {
        // 判定为无意义回复的最大字符数
        MAX_CHARS: 30,
        // 连续显示的最大回复数
        MAX_VISIBLE_REPLIES: 8,
        // 用于判定无意义回复的关键词和正则表达式
        MEANINGLESS_PATTERNS: [
            // 基础表情和重复字符
            /^[。.…~~]+$/,  // 省略号
            /^.*[哈嘿呵h]{2,}$/i,  // 笑声
            /^.*[6666]{2,}$/,  // 666
            /^.*[??!!.。]{2,}$/,  // 连续的标点符号
            /^.*[::][++]1[::]$/,  // :+1:
            /^.*(\s*:[\w-]+:\s*){1,}$/,  // 纯表情符号

            //  单字重复
            /^.*(.)\1{1,}$/,  // 任何字符重复

            //  感谢类 感谢@hanhai贡献补充规则
            /^.*[谢蟹感]谢?(你|您|分享|大佬|楼主|老铁|老哥|佬友?|大神|博主)?(,|,|.|!|!|~|~|。)*.*$/i,
            /^.*感恩|感动|感激[!!~~。.]*$/,
            /^.*(thank|thanks|thx|tks)[!!~~。.]*$/i,

            //  支持类 感谢@hanhai贡献补充规则
            /.*期待.*/i,
            /^.*(支持|顶|赞|好评|mark占?位?|收藏|马克|签到|打卡|学习|关注|收藏了|路过|前来|学习了)[!!~~。.]*$/i,
            /^.*(\+1|1\+|加1|[➕+]1)[!!~~。.]*$/,
            /^.*先赞后看[!!~~。.]*$/,
            /^.*已阅[!!~~。.]*$/,
            /^.*非常好用[!!~~。.]*$/,
            /^.*好用[,,]?爱用[!!~~。.]*$/,
            /^.*爱用[,,]?喜欢[!!~~。.]*$/,
            /^.*火钳威武[!!~~。.]*$/,

            //  称赞类
            /^.*(好|棒|强|厉害|可以|不错|牛|帅|赞|妙|秒|绝|狠|太强|很强|太棒|很棒|牛逼|nb|可以的)[!!~~。.]*$/i,
            /^.*(nice|good|perfect|awesome|ok+)[!!~~。.]*$/i,
            /^.*[牛nb]{1,}[bB呀啊哇plus]{0,5}$/,  // 牛b,nbbb,牛逼plus等
            /^.*牛啊?皇[!!~~。.]*$/,

            //  楼层相关
            /^.*[第前后大小]?[1-9一二三四五六七八九十百千]{1,}[楼层名]?[!!~~。.]*$/,
            /^.*(前排|沙发|板凳|地板)[!!~~。.]*$/,
            /^.*[大小]?后排[!!~~。.]*$/,
            /^.*排队[!!~~。.]*$/,
            /^.*[前后][排队][!!~~。.]*$/,

            //  佬相关
            /^.*(佬|大佬|巨佬|巨巨|大神)[!!~~。.]*$/,
            /^.*佬(的)?分享[!!~~。.]*$/,
            /^.*始皇(大佬|陛下|老师|[vV][1-9])?[!!~~。.]*$/,
            /^.*吾皇[万岁]{2,}$/,
            /^.*伟大[~~]*[,,]?无需多[盐言][!!~~。.]*$/,

            //  其他常见短语
            /^.*(顶上去|顶上来|顶一下|帮顶|支持一下|学习了|学到了|受益了|get|学习打卡)[!!~~。.]*$/i,
            /^.*(看看|路过|潜水|冒泡|打卡|签到|留念|留名)[!!~~。.]*$/,
            /^.*[1-9一二三四五六七八九十]\s*[份分]到手[!!~~。.]*$/,
            /^.*别说话[!!~~。.]*$/,
            /^.*前排[!!~~。.]*爽[~~]*$/,
            /^.*前排[!!~~。.]*始皇[牛nb逼]{1,}[!!~~。.]*(破音)$/,

            //  表情符号组合
            /^.*(:[++]1:\s*){1,}$/,  // 连续的 :+1: 表情
            /^.*[::][^\s]{1,10}[::](\s*[::][^\s]{1,10}[::])*$/, // 任意表情符号组合

            // Custom
            "来了","太强","哈哈哈","红红火火","牛啊","好好好","重生了","来啦","cy","插眼","mark","Mark","tql","始皇"
        ]
    };

    // 判断是否为无意义回复
    function isMeaninglessReply(content) {
        const cleanContent = content.replace(/\s+/g, '');
        if (cleanContent.length <= CONFIG.MAX_CHARS) {
            const matchedPattern = CONFIG.MEANINGLESS_PATTERNS.find(pattern => {
                if (pattern instanceof RegExp) {
                    return pattern.test(cleanContent);
                } else {
                    return cleanContent.toLowerCase().includes(pattern.toLowerCase());
                }
            });

            if (matchedPattern) {
                DEBUG.log('检测', `发现无意义回复: "${content}" (匹配模式: ${matchedPattern})`);
                return true;
            }
        }
        return false;
    }

    // 创建折叠后的回复元素
    function createFoldedReply(post) {
        try {
            const userInfo = post.querySelector('.topic-meta-data');
            if (!userInfo) {
                DEBUG.log('错误', '未找到用户信息区域');
                return null;
            }

            const username = userInfo.querySelector('.username');
            const postNumber = userInfo.querySelector('.post-number, .linuxfloor');
            const cookedContent = post.querySelector('.cooked');


            let author;
            if (!username || !cookedContent) {
                author = userInfo.querySelector('.full-name').childNodes[0].getAttribute('data-user-card');
                //console.log(username,cookedContent,userInfo,author);
            }else{
                author = username.textContent;
            }
            const content = cookedContent.textContent.trim();
            const number = postNumber ? postNumber.textContent : '';

            DEBUG.log('创建', `创建折叠元素: #${number} ${author}`);

            const foldedDiv = document.createElement('div');
            foldedDiv.className = 'folded-reply';
            foldedDiv.innerHTML = `
                ${number ? `<span class="folded-post-number">${number}</span>` : ''}
                <span class="folded-author">${author}</span>:
                <span class="folded-content">${content}</span>
            `;
            foldedDiv.style.cssText = `
                padding: 5px 15px;
                margin: 5px 0;
                background-color: var(--primary-very-low);
                border-radius: 4px;
                font-size: 0.9em;
                cursor: pointer;
                display: flex;
                align-items: center;
                gap: 8px;
            `;

            foldedDiv.addEventListener('click', () => {
                DEBUG.log('点击', `展开回复: #${number}`);
                post.style.display = '';
                foldedDiv.style.display = 'none';
            });

            return foldedDiv;
        } catch (error) {
            DEBUG.log('错误', '创建折叠元素失败', error);
            return null;
        }
    }

    // 处理连续的无意义回复
    function handleConsecutiveMeaninglessReplies(replies) {
        let currentIndex = 0;
        let consecutiveGroups = [];
        let currentGroup = [];

        // 首先找出所有连续的回复组
        for (let i = 0; i < replies.length; i++) {
            if (currentGroup.length === 0) {
                currentGroup.push(replies[i]);
            } else {
                const lastPost = currentGroup[currentGroup.length - 1].post;
                const currentPost = replies[i].post;

                // 检查是否连续(通过比较帖子编号)
                const lastNumber = parseInt(lastPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, ''));
                const currentNumber = parseInt(currentPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, ''));

                if (lastNumber && currentNumber && currentNumber === lastNumber + 1) {
                    currentGroup.push(replies[i]);
                } else {
                    if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) {
                        consecutiveGroups.push([...currentGroup]);
                    }
                    currentGroup = [replies[i]];
                }
            }
        }

        // 处理最后一组
        if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) {
            consecutiveGroups.push(currentGroup);
        }

        // 处理每一组连续回复
        consecutiveGroups.forEach(group => {
            DEBUG.log('处理', `发现连续回复组: 数量=${group.length}`);

            // 显示前 MAX_VISIBLE_REPLIES 个回复
            for (let i = 0; i < CONFIG.MAX_VISIBLE_REPLIES; i++) {
                if (group[i]) {
                    group[i].foldedReply.style.display = '';
                }
            }

            // 隐藏剩余的回复
            for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) {
                group[i].foldedReply.style.display = 'none';
            }

            // 创建省略号元素
            const ellipsis = document.createElement('div');
            ellipsis.className = 'replies-ellipsis';
            ellipsis.innerHTML = `
                <span>还有 ${group.length - CONFIG.MAX_VISIBLE_REPLIES} 条类似回复</span>
                <span class="show-more">点击展开</span>
            `;
            ellipsis.style.cssText = `
                text-align: center;
                padding: 8px;
                color: var(--primary-medium);
                cursor: pointer;
                margin: 5px 0;
                background-color: var(--primary-very-low);
                border-radius: 4px;
                font-size: 0.9em;
            `;

            // 插入省略号到最后一个可见回复之后
            const lastVisibleReply = group[CONFIG.MAX_VISIBLE_REPLIES - 1].foldedReply;
            if (lastVisibleReply) {
                lastVisibleReply.parentNode.insertBefore(ellipsis, lastVisibleReply.nextSibling);
                DEBUG.log('插入', '插入省略号元素');
            }

            // 点击省略号时展开所有回复
            ellipsis.addEventListener('click', () => {
                DEBUG.log('展开', '展开连续回复');
                for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) {
                    group[i].foldedReply.style.display = '';
                }
                ellipsis.style.display = 'none';
            });
        });
    }

    // 主函数
    function foldMeaninglessReplies() {
        DEBUG.log('执行', '开始处理帖子');
        // 移除已存在的折叠元素
        document.querySelectorAll('.folded-reply, .replies-ellipsis').forEach(el => el.remove());

        const posts = Array.from(document.querySelectorAll('.post-stream article.boxed.onscreen-post')).slice(1);
        DEBUG.log('统计', `找到 ${posts.length} 个回复帖子`);
        const meaninglessReplies = [];

        posts.forEach(post => {
            try {
                const content = post.querySelector('.cooked')?.textContent.trim();
                if (!content) {
                    DEBUG.log('跳过', '帖子内容为空');
                    return;
                }

                if (isMeaninglessReply(content)) {
                    const foldedReply = createFoldedReply(post);
                    if (foldedReply) {
                        post.parentNode.insertBefore(foldedReply, post);
                        post.style.display = 'none';
                        meaninglessReplies.push({post, foldedReply});
                    }
                }
            } catch (error) {
                DEBUG.log('错误', '处理帖子时发生错误', error);
            }
        });

        DEBUG.log('统计', `本次共折叠 ${meaninglessReplies.length} 个回复`);

        if (meaninglessReplies.length > 0) {
            handleConsecutiveMeaninglessReplies(meaninglessReplies);
        }
    }

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .folded-reply {
            transition: background-color 0.2s;
        }
        .folded-reply:hover {
            background-color: var(--primary-low);
        }
        .folded-post-number {
            color: var(--primary-medium);
            font-size: 0.8em;
            min-width: 2em;
        }
        .folded-author {
            font-weight: bold;
            color: var(--primary-high);
        }
        .folded-content {
            color: var(--primary-medium);
        }
        .replies-ellipsis .show-more {
            color: var(--tertiary);
            margin-left: 5px;
        }
        .replies-ellipsis:hover {
            background-color: var(--primary-low);
        }
    `;
    document.head.appendChild(style);

    // 使用防抖函数来避免频繁触发
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 检查页面是否已完全加载
    function isPageFullyLoaded() {
        // 检查 Discourse 应用是否已加载
        if (typeof define !== 'function' || typeof require !== 'function') {
            DEBUG.log('加载检查', 'AMD 模块系统未加载');
            return false;
        }

        // 检查 post-stream 组件是否已加载
        const postStream = document.querySelector('#post-stream');
        if (!postStream) {
            DEBUG.log('加载检查', 'post-stream 元素未找到');
            return false;
        }

        // 检查是否有加载状态
        const loadingPosts = postStream.querySelector('.loading-container, .timeline-loading, .loading-onebox');
        if (loadingPosts) {
            DEBUG.log('加载检查', '帖子正在加载中');
            return false;
        }

        // 检查是否有可见的帖子
        const visiblePosts = postStream.querySelectorAll('article.topic-post:not(.placeholder)');
        if (visiblePosts.length === 0) {
            DEBUG.log('加载检查', '没有可见的帖子');
            return false;
        }

        DEBUG.log('加载检查', `页面加载完成 (可见帖子数: ${visiblePosts.length})`);
        return true;
    }

    // 等待 Discourse 应用加载
    function waitForDiscourse() {
        return new Promise((resolve) => {
            const maxAttempts = 200; // 增加等待时间到 20 秒
            let attempts = 0;

            function check() {
                attempts++;

                // 检查 Discourse 应用是否已加载
                const appLoaded = typeof define === 'function' && typeof require === 'function';
                const postStreamLoaded = document.querySelector('#post-stream article.topic-post');
                const loadingIndicator = document.querySelector('#post-stream .loading-container');

                // 检查 TopicController 是否已初始化
                const topicControllerLoaded = window.require && (() => {
                    try {
                        const container = window.require('discourse/app').default.__container__;
                        const controller = container.lookup('controller:topic');
                        return controller && controller.model && controller.model.postStream;
                    } catch (e) {
                        return false;
                    }
                })();

                if (appLoaded && postStreamLoaded && !loadingIndicator && topicControllerLoaded) {
                    // 额外等待一小段时间,确保内容完全加载
                    setTimeout(() => {
                        DEBUG.log('等待', 'Discourse 应用已加载,帖子已就绪');
                        resolve();
                    }, 1000);
                    return;
                }

                if (attempts >= maxAttempts) {
                    DEBUG.log('等待', '等待超时,将在路由变化时重试');
                    resolve();
                    return;
                }

                setTimeout(check, 100);
            }

            // 如果页面已经加载完成,立即开始检查
            if (document.readyState === 'complete') {
                check();
            } else {
                // 否则等待页面加载完成
                window.addEventListener('load', check);
            }
        });
    }

    // 监听路由变化
    function setupRouteObserver() {
        let lastUrl = location.href;
        let isProcessing = false;

        // 创建一个 MutationObserver 来监视 URL 变化
        const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                if (isProcessing) return;

                isProcessing = true;
                DEBUG.log('路由', '检测到页面 URL 变化');

                // 等待新页面加载完成
                setTimeout(() => {
                    if (window.requestIdleCallback) {
                        requestIdleCallback(() => {
                            DEBUG.log('执行', '页面变化后开始折叠');
                            foldMeaninglessReplies();
                            isProcessing = false;
                        });
                    } else {
                        setTimeout(() => {
                            DEBUG.log('执行', '页面变化后开始折叠');
                            foldMeaninglessReplies();
                            isProcessing = false;
                        }, 1000);
                    }
                }, 1000);
            }
        });

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

        // 监听 popstate 事件(浏览器前进/后退)
        window.addEventListener('popstate', () => {
            if (isProcessing) return;

            isProcessing = true;
            DEBUG.log('路由', '检测到 popstate 事件');
            waitForDiscourse().then(() => {
                if (window.requestIdleCallback) {
                    requestIdleCallback(() => {
                        DEBUG.log('执行', 'popstate 后开始折叠');
                        foldMeaninglessReplies();
                        isProcessing = false;
                    });
                } else {
                    setTimeout(() => {
                        DEBUG.log('执行', 'popstate 后开始折叠');
                        foldMeaninglessReplies();
                        isProcessing = false;
                    }, 1000);
                }
            });
        });
    }

    // 设置定时器
    function setupAutoFold() {
        DEBUG.log('定时', '启动自动折叠定时器');

        // 创建定时器
        const timer = setInterval(() => {
            const postStream = document.querySelector('#post-stream');
            if (!postStream) return;

            const loadingContainer = document.querySelector('#post-stream .loading-container');
            if (loadingContainer) return;

            DEBUG.log('定时', '执行定时折叠检查');
            foldMeaninglessReplies();
        }, 5000);

        // 在页面卸载时清除定时器
        window.addEventListener('unload', () => {
            clearInterval(timer);
        });

        return timer;
    }

    // 初始化函数
    async function initialize() {
        try {
            DEBUG.log('初始化', '脚本开始运行');

            // 等待 Discourse 应用加载
            // await waitForDiscourse();

            // 设置路由观察器
            setupRouteObserver();

            // 设置自动折叠定时器
            const timer = setupAutoFold();

            // 使用 requestIdleCallback 在浏览器空闲时执行折叠操作
            if (window.requestIdleCallback) {
                requestIdleCallback(() => {
                    DEBUG.log('执行', '开始初始折叠');
                    foldMeaninglessReplies();

                    // 设置 MutationObserver
                    setupObserver();
                });
            } else {
                // 如果不支持 requestIdleCallback,则延迟执行
                setTimeout(() => {
                    DEBUG.log('执行', '开始初始折叠');
                    foldMeaninglessReplies();

                    // 设置 MutationObserver
                    setupObserver();
                }, 1000);
            }

        } catch (error) {
            DEBUG.log('错误', '初始化失败', error);
            console.error('折叠脚本初始化失败:', error);
            setTimeout(initialize, 5000);
        }
    }

    // 设置 MutationObserver
    function setupObserver() {
        const postStream = document.querySelector('#post-stream');
        if (!postStream) return;

        DEBUG.log('监听', '开始监听帖子流变化');

        const observer = new MutationObserver(debounce((mutations) => {
            const hasNewPosts = mutations.some(mutation => {
                return Array.from(mutation.addedNodes).some(node =>
                    node.nodeType === 1 && (
                        node.classList?.contains('topic-post') ||
                        node.querySelector?.('.topic-post')
                    )
                );
            });

            const loadingContainer = document.querySelector('#post-stream .loading-container');
            if (hasNewPosts && !loadingContainer) {
                // 等待一小段时间确保新帖子完全加载
                setTimeout(() => {
                    if (!document.querySelector('#post-stream .loading-container')) {
                        DEBUG.log('观察器', '发现新帖子,开始处理');
                        foldMeaninglessReplies();
                    }
                }, 500);
            }
        }, 200));

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

        // 监听滚动事件
        window.addEventListener('scroll', debounce(() => {
            const loadingContainer = document.querySelector('#post-stream .loading-container');
            if (loadingContainer) return;

            const posts = postStream.querySelectorAll('article.topic-post:not(.placeholder)');
            const lastPost = posts[posts.length - 1];
            if (!lastPost) return;

            const rect = lastPost.getBoundingClientRect();
            if (rect.bottom <= window.innerHeight * 2) {
                // 等待一小段时间确保新帖子加载完成
                setTimeout(() => {
                    if (!document.querySelector('#post-stream .loading-container')) {
                        DEBUG.log('滚动', '接近底部,检查新帖子');
                        foldMeaninglessReplies();
                    }
                }, 500);
            }
        }, 200), { passive: true });
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1000));
    } else {
        setTimeout(initialize, 1000);
    }
})();