Linux Do 量子速读

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

  1. // ==UserScript==
  2. // @name Linux Do 量子速读
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description 帮您在Linux Do论坛中折叠无意义回复,告别水贴,光速获取信息!
  6. // @author 量子咸鱼K
  7. // @match *://linux.do/t/topic/*
  8. // @grant GM_log
  9. // @run-at document-end
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. // 立即执行的日志,确认脚本加载
  14. console.log('[折叠器] 脚本已加载');
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // 添加日志函数
  20. const DEBUG = {
  21. enabled: true,
  22. log: function(type, message, data = null) {
  23. if (!this.enabled) return;
  24. const timestamp = new Date().toISOString().split('T')[1];
  25. console.log(`[折叠器][${timestamp}][${type}] ${message}`, data ? data : '');
  26. }
  27. };
  28.  
  29. // 立即执行的测试日志
  30. DEBUG.log('测试', '日志系统初始化成功');
  31.  
  32. // 配置项
  33. const CONFIG = {
  34. // 判定为无意义回复的最大字符数
  35. MAX_CHARS: 30,
  36. // 连续显示的最大回复数
  37. MAX_VISIBLE_REPLIES: 8,
  38. // 用于判定无意义回复的关键词和正则表达式
  39. MEANINGLESS_PATTERNS: [
  40. // 基础表情和重复字符
  41. /^[。.…~~]+$/, // 省略号
  42. /^.*[哈嘿呵h]{2,}$/i, // 笑声
  43. /^.*[6666]{2,}$/, // 666
  44. /^.*[??!!.。]{2,}$/, // 连续的标点符号
  45. /^.*[::][++]1[::]$/, // :+1:
  46. /^.*(\s*:[\w-]+:\s*){1,}$/, // 纯表情符号
  47.  
  48. // 单字重复
  49. /^.*(.)\1{1,}$/, // 任何字符重复
  50.  
  51. // 感谢类 感谢@hanhai贡献补充规则
  52. /^.*[谢蟹感]谢?(你|您|分享|大佬|楼主|老铁|老哥|佬友?|大神|博主)?(,|,|.|!|!|~|~|。)*.*$/i,
  53. /^.*感恩|感动|感激[!!~~。.]*$/,
  54. /^.*(thank|thanks|thx|tks)[!!~~。.]*$/i,
  55.  
  56. // 支持类 感谢@hanhai贡献补充规则
  57. /.*期待.*/i,
  58. /^.*(支持|顶|赞|好评|mark占?位?|收藏|马克|签到|打卡|学习|关注|收藏了|路过|前来|学习了)[!!~~。.]*$/i,
  59. /^.*(\+1|1\+|加1|[➕+]1)[!!~~。.]*$/,
  60. /^.*先赞后看[!!~~。.]*$/,
  61. /^.*已阅[!!~~。.]*$/,
  62. /^.*非常好用[!!~~。.]*$/,
  63. /^.*好用[,,]?爱用[!!~~。.]*$/,
  64. /^.*爱用[,,]?喜欢[!!~~。.]*$/,
  65. /^.*火钳威武[!!~~。.]*$/,
  66.  
  67. // 称赞类
  68. /^.*(好|棒|强|厉害|可以|不错|牛|帅|赞|妙|秒|绝|狠|太强|很强|太棒|很棒|牛逼|nb|可以的)[!!~~。.]*$/i,
  69. /^.*(nice|good|perfect|awesome|ok+)[!!~~。.]*$/i,
  70. /^.*[牛nb]{1,}[bB呀啊哇plus]{0,5}$/, // 牛b,nbbb,牛逼plus等
  71. /^.*牛啊?皇[!!~~。.]*$/,
  72.  
  73. // 楼层相关
  74. /^.*[第前后大小]?[1-9一二三四五六七八九十百千]{1,}[楼层名]?[!!~~。.]*$/,
  75. /^.*(前排|沙发|板凳|地板)[!!~~。.]*$/,
  76. /^.*[大小]?后排[!!~~。.]*$/,
  77. /^.*排队[!!~~。.]*$/,
  78. /^.*[前后][排队][!!~~。.]*$/,
  79.  
  80. // 佬相关
  81. /^.*(佬|大佬|巨佬|巨巨|大神)[!!~~。.]*$/,
  82. /^.*佬(的)?分享[!!~~。.]*$/,
  83. /^.*始皇(大佬|陛下|老师|[vV][1-9])?[!!~~。.]*$/,
  84. /^.*吾皇[万岁]{2,}$/,
  85. /^.*伟大[~~]*[,,]?无需多[盐言][!!~~。.]*$/,
  86.  
  87. // 其他常见短语
  88. /^.*(顶上去|顶上来|顶一下|帮顶|支持一下|学习了|学到了|受益了|get|学习打卡)[!!~~。.]*$/i,
  89. /^.*(看看|路过|潜水|冒泡|打卡|签到|留念|留名)[!!~~。.]*$/,
  90. /^.*[1-9一二三四五六七八九十]\s*[份分]到手[!!~~。.]*$/,
  91. /^.*别说话[!!~~。.]*$/,
  92. /^.*前排[!!~~。.]*爽[~~]*$/,
  93. /^.*前排[!!~~。.]*始皇[牛nb逼]{1,}[!!~~。.]*(破音)$/,
  94.  
  95. // 表情符号组合
  96. /^.*(:[++]1:\s*){1,}$/, // 连续的 :+1: 表情
  97. /^.*[::][^\s]{1,10}[::](\s*[::][^\s]{1,10}[::])*$/, // 任意表情符号组合
  98.  
  99. // Custom
  100. "来了","太强","哈哈哈","红红火火","牛啊","好好好","重生了","来啦","cy","插眼","mark","Mark","tql","始皇"
  101. ]
  102. };
  103.  
  104. // 判断是否为无意义回复
  105. function isMeaninglessReply(content) {
  106. const cleanContent = content.replace(/\s+/g, '');
  107. if (cleanContent.length <= CONFIG.MAX_CHARS) {
  108. const matchedPattern = CONFIG.MEANINGLESS_PATTERNS.find(pattern => {
  109. if (pattern instanceof RegExp) {
  110. return pattern.test(cleanContent);
  111. } else {
  112. return cleanContent.toLowerCase().includes(pattern.toLowerCase());
  113. }
  114. });
  115.  
  116. if (matchedPattern) {
  117. DEBUG.log('检测', `发现无意义回复: "${content}" (匹配模式: ${matchedPattern})`);
  118. return true;
  119. }
  120. }
  121. return false;
  122. }
  123.  
  124. // 创建折叠后的回复元素
  125. function createFoldedReply(post) {
  126. try {
  127. const userInfo = post.querySelector('.topic-meta-data');
  128. if (!userInfo) {
  129. DEBUG.log('错误', '未找到用户信息区域');
  130. return null;
  131. }
  132.  
  133. const username = userInfo.querySelector('.username');
  134. const postNumber = userInfo.querySelector('.post-number, .linuxfloor');
  135. const cookedContent = post.querySelector('.cooked');
  136.  
  137.  
  138. let author;
  139. if (!username || !cookedContent) {
  140. author = userInfo.querySelector('.full-name').childNodes[0].getAttribute('data-user-card');
  141. //console.log(username,cookedContent,userInfo,author);
  142. }else{
  143. author = username.textContent;
  144. }
  145. const content = cookedContent.textContent.trim();
  146. const number = postNumber ? postNumber.textContent : '';
  147.  
  148. DEBUG.log('创建', `创建折叠元素: #${number} ${author}`);
  149.  
  150. const foldedDiv = document.createElement('div');
  151. foldedDiv.className = 'folded-reply';
  152. foldedDiv.innerHTML = `
  153. ${number ? `<span class="folded-post-number">${number}</span>` : ''}
  154. <span class="folded-author">${author}</span>:
  155. <span class="folded-content">${content}</span>
  156. `;
  157. foldedDiv.style.cssText = `
  158. padding: 5px 15px;
  159. margin: 5px 0;
  160. background-color: var(--primary-very-low);
  161. border-radius: 4px;
  162. font-size: 0.9em;
  163. cursor: pointer;
  164. display: flex;
  165. align-items: center;
  166. gap: 8px;
  167. `;
  168.  
  169. foldedDiv.addEventListener('click', () => {
  170. DEBUG.log('点击', `展开回复: #${number}`);
  171. post.style.display = '';
  172. foldedDiv.style.display = 'none';
  173. });
  174.  
  175. return foldedDiv;
  176. } catch (error) {
  177. DEBUG.log('错误', '创建折叠元素失败', error);
  178. return null;
  179. }
  180. }
  181.  
  182. // 处理连续的无意义回复
  183. function handleConsecutiveMeaninglessReplies(replies) {
  184. let currentIndex = 0;
  185. let consecutiveGroups = [];
  186. let currentGroup = [];
  187.  
  188. // 首先找出所有连续的回复组
  189. for (let i = 0; i < replies.length; i++) {
  190. if (currentGroup.length === 0) {
  191. currentGroup.push(replies[i]);
  192. } else {
  193. const lastPost = currentGroup[currentGroup.length - 1].post;
  194. const currentPost = replies[i].post;
  195.  
  196. // 检查是否连续(通过比较帖子编号)
  197. const lastNumber = parseInt(lastPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, ''));
  198. const currentNumber = parseInt(currentPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, ''));
  199.  
  200. if (lastNumber && currentNumber && currentNumber === lastNumber + 1) {
  201. currentGroup.push(replies[i]);
  202. } else {
  203. if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) {
  204. consecutiveGroups.push([...currentGroup]);
  205. }
  206. currentGroup = [replies[i]];
  207. }
  208. }
  209. }
  210.  
  211. // 处理最后一组
  212. if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) {
  213. consecutiveGroups.push(currentGroup);
  214. }
  215.  
  216. // 处理每一组连续回复
  217. consecutiveGroups.forEach(group => {
  218. DEBUG.log('处理', `发现连续回复组: 数量=${group.length}`);
  219.  
  220. // 显示前 MAX_VISIBLE_REPLIES 个回复
  221. for (let i = 0; i < CONFIG.MAX_VISIBLE_REPLIES; i++) {
  222. if (group[i]) {
  223. group[i].foldedReply.style.display = '';
  224. }
  225. }
  226.  
  227. // 隐藏剩余的回复
  228. for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) {
  229. group[i].foldedReply.style.display = 'none';
  230. }
  231.  
  232. // 创建省略号元素
  233. const ellipsis = document.createElement('div');
  234. ellipsis.className = 'replies-ellipsis';
  235. ellipsis.innerHTML = `
  236. <span>还有 ${group.length - CONFIG.MAX_VISIBLE_REPLIES} 条类似回复</span>
  237. <span class="show-more">点击展开</span>
  238. `;
  239. ellipsis.style.cssText = `
  240. text-align: center;
  241. padding: 8px;
  242. color: var(--primary-medium);
  243. cursor: pointer;
  244. margin: 5px 0;
  245. background-color: var(--primary-very-low);
  246. border-radius: 4px;
  247. font-size: 0.9em;
  248. `;
  249.  
  250. // 插入省略号到最后一个可见回复之后
  251. const lastVisibleReply = group[CONFIG.MAX_VISIBLE_REPLIES - 1].foldedReply;
  252. if (lastVisibleReply) {
  253. lastVisibleReply.parentNode.insertBefore(ellipsis, lastVisibleReply.nextSibling);
  254. DEBUG.log('插入', '插入省略号元素');
  255. }
  256.  
  257. // 点击省略号时展开所有回复
  258. ellipsis.addEventListener('click', () => {
  259. DEBUG.log('展开', '展开连续回复');
  260. for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) {
  261. group[i].foldedReply.style.display = '';
  262. }
  263. ellipsis.style.display = 'none';
  264. });
  265. });
  266. }
  267.  
  268. // 主函数
  269. function foldMeaninglessReplies() {
  270. DEBUG.log('执行', '开始处理帖子');
  271. // 移除已存在的折叠元素
  272. document.querySelectorAll('.folded-reply, .replies-ellipsis').forEach(el => el.remove());
  273.  
  274. const posts = Array.from(document.querySelectorAll('.post-stream article.boxed.onscreen-post')).slice(1);
  275. DEBUG.log('统计', `找到 ${posts.length} 个回复帖子`);
  276. const meaninglessReplies = [];
  277.  
  278. posts.forEach(post => {
  279. try {
  280. const content = post.querySelector('.cooked')?.textContent.trim();
  281. if (!content) {
  282. DEBUG.log('跳过', '帖子内容为空');
  283. return;
  284. }
  285.  
  286. if (isMeaninglessReply(content)) {
  287. const foldedReply = createFoldedReply(post);
  288. if (foldedReply) {
  289. post.parentNode.insertBefore(foldedReply, post);
  290. post.style.display = 'none';
  291. meaninglessReplies.push({post, foldedReply});
  292. }
  293. }
  294. } catch (error) {
  295. DEBUG.log('错误', '处理帖子时发生错误', error);
  296. }
  297. });
  298.  
  299. DEBUG.log('统计', `本次共折叠 ${meaninglessReplies.length} 个回复`);
  300.  
  301. if (meaninglessReplies.length > 0) {
  302. handleConsecutiveMeaninglessReplies(meaninglessReplies);
  303. }
  304. }
  305.  
  306. // 添加样式
  307. const style = document.createElement('style');
  308. style.textContent = `
  309. .folded-reply {
  310. transition: background-color 0.2s;
  311. }
  312. .folded-reply:hover {
  313. background-color: var(--primary-low);
  314. }
  315. .folded-post-number {
  316. color: var(--primary-medium);
  317. font-size: 0.8em;
  318. min-width: 2em;
  319. }
  320. .folded-author {
  321. font-weight: bold;
  322. color: var(--primary-high);
  323. }
  324. .folded-content {
  325. color: var(--primary-medium);
  326. }
  327. .replies-ellipsis .show-more {
  328. color: var(--tertiary);
  329. margin-left: 5px;
  330. }
  331. .replies-ellipsis:hover {
  332. background-color: var(--primary-low);
  333. }
  334. `;
  335. document.head.appendChild(style);
  336.  
  337. // 使用防抖函数来避免频繁触发
  338. function debounce(func, wait) {
  339. let timeout;
  340. return function executedFunction(...args) {
  341. const later = () => {
  342. clearTimeout(timeout);
  343. func(...args);
  344. };
  345. clearTimeout(timeout);
  346. timeout = setTimeout(later, wait);
  347. };
  348. }
  349.  
  350. // 检查页面是否已完全加载
  351. function isPageFullyLoaded() {
  352. // 检查 Discourse 应用是否已加载
  353. if (typeof define !== 'function' || typeof require !== 'function') {
  354. DEBUG.log('加载检查', 'AMD 模块系统未加载');
  355. return false;
  356. }
  357.  
  358. // 检查 post-stream 组件是否已加载
  359. const postStream = document.querySelector('#post-stream');
  360. if (!postStream) {
  361. DEBUG.log('加载检查', 'post-stream 元素未找到');
  362. return false;
  363. }
  364.  
  365. // 检查是否有加载状态
  366. const loadingPosts = postStream.querySelector('.loading-container, .timeline-loading, .loading-onebox');
  367. if (loadingPosts) {
  368. DEBUG.log('加载检查', '帖子正在加载中');
  369. return false;
  370. }
  371.  
  372. // 检查是否有可见的帖子
  373. const visiblePosts = postStream.querySelectorAll('article.topic-post:not(.placeholder)');
  374. if (visiblePosts.length === 0) {
  375. DEBUG.log('加载检查', '没有可见的帖子');
  376. return false;
  377. }
  378.  
  379. DEBUG.log('加载检查', `页面加载完成 (可见帖子数: ${visiblePosts.length})`);
  380. return true;
  381. }
  382.  
  383. // 等待 Discourse 应用加载
  384. function waitForDiscourse() {
  385. return new Promise((resolve) => {
  386. const maxAttempts = 200; // 增加等待时间到 20 秒
  387. let attempts = 0;
  388.  
  389. function check() {
  390. attempts++;
  391.  
  392. // 检查 Discourse 应用是否已加载
  393. const appLoaded = typeof define === 'function' && typeof require === 'function';
  394. const postStreamLoaded = document.querySelector('#post-stream article.topic-post');
  395. const loadingIndicator = document.querySelector('#post-stream .loading-container');
  396.  
  397. // 检查 TopicController 是否已初始化
  398. const topicControllerLoaded = window.require && (() => {
  399. try {
  400. const container = window.require('discourse/app').default.__container__;
  401. const controller = container.lookup('controller:topic');
  402. return controller && controller.model && controller.model.postStream;
  403. } catch (e) {
  404. return false;
  405. }
  406. })();
  407.  
  408. if (appLoaded && postStreamLoaded && !loadingIndicator && topicControllerLoaded) {
  409. // 额外等待一小段时间,确保内容完全加载
  410. setTimeout(() => {
  411. DEBUG.log('等待', 'Discourse 应用已加载,帖子已就绪');
  412. resolve();
  413. }, 1000);
  414. return;
  415. }
  416.  
  417. if (attempts >= maxAttempts) {
  418. DEBUG.log('等待', '等待超时,将在路由变化时重试');
  419. resolve();
  420. return;
  421. }
  422.  
  423. setTimeout(check, 100);
  424. }
  425.  
  426. // 如果页面已经加载完成,立即开始检查
  427. if (document.readyState === 'complete') {
  428. check();
  429. } else {
  430. // 否则等待页面加载完成
  431. window.addEventListener('load', check);
  432. }
  433. });
  434. }
  435.  
  436. // 监听路由变化
  437. function setupRouteObserver() {
  438. let lastUrl = location.href;
  439. let isProcessing = false;
  440.  
  441. // 创建一个 MutationObserver 来监视 URL 变化
  442. const observer = new MutationObserver(() => {
  443. if (location.href !== lastUrl) {
  444. lastUrl = location.href;
  445. if (isProcessing) return;
  446.  
  447. isProcessing = true;
  448. DEBUG.log('路由', '检测到页面 URL 变化');
  449.  
  450. // 等待新页面加载完成
  451. setTimeout(() => {
  452. if (window.requestIdleCallback) {
  453. requestIdleCallback(() => {
  454. DEBUG.log('执行', '页面变化后开始折叠');
  455. foldMeaninglessReplies();
  456. isProcessing = false;
  457. });
  458. } else {
  459. setTimeout(() => {
  460. DEBUG.log('执行', '页面变化后开始折叠');
  461. foldMeaninglessReplies();
  462. isProcessing = false;
  463. }, 1000);
  464. }
  465. }, 1000);
  466. }
  467. });
  468.  
  469. observer.observe(document, {
  470. subtree: true,
  471. childList: true
  472. });
  473.  
  474. // 监听 popstate 事件(浏览器前进/后退)
  475. window.addEventListener('popstate', () => {
  476. if (isProcessing) return;
  477.  
  478. isProcessing = true;
  479. DEBUG.log('路由', '检测到 popstate 事件');
  480. waitForDiscourse().then(() => {
  481. if (window.requestIdleCallback) {
  482. requestIdleCallback(() => {
  483. DEBUG.log('执行', 'popstate 后开始折叠');
  484. foldMeaninglessReplies();
  485. isProcessing = false;
  486. });
  487. } else {
  488. setTimeout(() => {
  489. DEBUG.log('执行', 'popstate 后开始折叠');
  490. foldMeaninglessReplies();
  491. isProcessing = false;
  492. }, 1000);
  493. }
  494. });
  495. });
  496. }
  497.  
  498. // 设置定时器
  499. function setupAutoFold() {
  500. DEBUG.log('定时', '启动自动折叠定时器');
  501.  
  502. // 创建定时器
  503. const timer = setInterval(() => {
  504. const postStream = document.querySelector('#post-stream');
  505. if (!postStream) return;
  506.  
  507. const loadingContainer = document.querySelector('#post-stream .loading-container');
  508. if (loadingContainer) return;
  509.  
  510. DEBUG.log('定时', '执行定时折叠检查');
  511. foldMeaninglessReplies();
  512. }, 5000);
  513.  
  514. // 在页面卸载时清除定时器
  515. window.addEventListener('unload', () => {
  516. clearInterval(timer);
  517. });
  518.  
  519. return timer;
  520. }
  521.  
  522. // 初始化函数
  523. async function initialize() {
  524. try {
  525. DEBUG.log('初始化', '脚本开始运行');
  526.  
  527. // 等待 Discourse 应用加载
  528. // await waitForDiscourse();
  529.  
  530. // 设置路由观察器
  531. setupRouteObserver();
  532.  
  533. // 设置自动折叠定时器
  534. const timer = setupAutoFold();
  535.  
  536. // 使用 requestIdleCallback 在浏览器空闲时执行折叠操作
  537. if (window.requestIdleCallback) {
  538. requestIdleCallback(() => {
  539. DEBUG.log('执行', '开始初始折叠');
  540. foldMeaninglessReplies();
  541.  
  542. // 设置 MutationObserver
  543. setupObserver();
  544. });
  545. } else {
  546. // 如果不支持 requestIdleCallback,则延迟执行
  547. setTimeout(() => {
  548. DEBUG.log('执行', '开始初始折叠');
  549. foldMeaninglessReplies();
  550.  
  551. // 设置 MutationObserver
  552. setupObserver();
  553. }, 1000);
  554. }
  555.  
  556. } catch (error) {
  557. DEBUG.log('错误', '初始化失败', error);
  558. console.error('折叠脚本初始化失败:', error);
  559. setTimeout(initialize, 5000);
  560. }
  561. }
  562.  
  563. // 设置 MutationObserver
  564. function setupObserver() {
  565. const postStream = document.querySelector('#post-stream');
  566. if (!postStream) return;
  567.  
  568. DEBUG.log('监听', '开始监听帖子流变化');
  569.  
  570. const observer = new MutationObserver(debounce((mutations) => {
  571. const hasNewPosts = mutations.some(mutation => {
  572. return Array.from(mutation.addedNodes).some(node =>
  573. node.nodeType === 1 && (
  574. node.classList?.contains('topic-post') ||
  575. node.querySelector?.('.topic-post')
  576. )
  577. );
  578. });
  579.  
  580. const loadingContainer = document.querySelector('#post-stream .loading-container');
  581. if (hasNewPosts && !loadingContainer) {
  582. // 等待一小段时间确保新帖子完全加载
  583. setTimeout(() => {
  584. if (!document.querySelector('#post-stream .loading-container')) {
  585. DEBUG.log('观察器', '发现新帖子,开始处理');
  586. foldMeaninglessReplies();
  587. }
  588. }, 500);
  589. }
  590. }, 200));
  591.  
  592. observer.observe(postStream, {
  593. childList: true,
  594. subtree: true
  595. });
  596.  
  597. // 监听滚动事件
  598. window.addEventListener('scroll', debounce(() => {
  599. const loadingContainer = document.querySelector('#post-stream .loading-container');
  600. if (loadingContainer) return;
  601.  
  602. const posts = postStream.querySelectorAll('article.topic-post:not(.placeholder)');
  603. const lastPost = posts[posts.length - 1];
  604. if (!lastPost) return;
  605.  
  606. const rect = lastPost.getBoundingClientRect();
  607. if (rect.bottom <= window.innerHeight * 2) {
  608. // 等待一小段时间确保新帖子加载完成
  609. setTimeout(() => {
  610. if (!document.querySelector('#post-stream .loading-container')) {
  611. DEBUG.log('滚动', '接近底部,检查新帖子');
  612. foldMeaninglessReplies();
  613. }
  614. }, 500);
  615. }
  616. }, 200), { passive: true });
  617. }
  618.  
  619. // 启动脚本
  620. if (document.readyState === 'loading') {
  621. document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1000));
  622. } else {
  623. setTimeout(initialize, 1000);
  624. }
  625. })();

QingJ © 2025

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