Linux Do 量子速读

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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