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