Auto Read (Linux.do Only)

自动刷阅读回复,仅支持Linux.do社区

  1. // ==UserScript==
  2. // @name Auto Read (Linux.do Only)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.1.0
  5. // @description 自动刷阅读回复,仅支持Linux.do社区
  6. // @author XinSong(https://blog.warhut.cn)自
  7. // @match https://linux.do/*
  8. // @grant unsafeWindow
  9. // @license MIT
  10. // @icon https://www.google.com/s2/favicons?domain=linux.do
  11. // @require https://cdn.tailwindcss.com
  12. // ==/UserScript==
  13.  
  14. (() => {
  15. 'use strict';
  16. // 挂载全局对象(避免作用域污染)
  17. const { document, window } = unsafeWindow;
  18.  
  19. // 配置中心(常量集中管理)
  20. const CONFIG = {
  21. BASE_URL: 'https://linux.do', // 基础URL
  22. LIKE_LIMIT: 20, // 每日点赞上限
  23. MAX_RETRIES: 3, // 错误页面最大重试次数
  24. SCROLL_OPTIONS: { // 滚动配置
  25. speed: 50, // 滚动速度(像素/次)
  26. interval: 100, // 滚动间隔(毫秒)
  27. },
  28. LIKE_INTERVAL: { // 点赞间隔配置
  29. min: 2000, // 最小间隔(毫秒)
  30. max: 5000 // 最大间隔(毫秒)
  31. },
  32. UPDATE_INTERVAL: 500 // 状态更新间隔(毫秒)
  33. };
  34.  
  35. /**
  36. * 状态管理类
  37. * 负责本地存储管理和状态初始化
  38. */
  39. class StateManager {
  40. constructor() {
  41. this.initState(); // 初始化默认状态
  42. this.loadFromStorage(); // 从本地存储加载状态
  43. }
  44.  
  45. // 初始化默认状态
  46. initState() {
  47. this.isReading = false; // 是否正在阅读
  48. this.isLiking = false; // 是否启用自动点赞
  49. this.errorRetries = 0; // 错误页面重试次数
  50. this.unseenHrefs = []; // 未读帖子链接列表
  51. this.currentTask = null; // 当前任务(导航/滚动等)
  52. this.scrollTimer = null; // 滚动定时器
  53. }
  54.  
  55. // 从localStorage加载状态
  56. loadFromStorage() {
  57. // 解析存储的状态对象,默认空对象
  58. const state = JSON.parse(localStorage.getItem('autoReadState')) || {};
  59. // 合并默认状态与存储状态
  60. Object.assign(this, {
  61. isReading: !!state.isReading, // 布尔值转换
  62. isLiking: state.isLiking ?? false, // 安全默认值
  63. errorRetries: state.errorRetries || 0,
  64. unseenHrefs: state.unseenHrefs || []
  65. });
  66. this.resetLikeCounter(); // 重置每日点赞计数
  67. }
  68.  
  69. // 保存状态到localStorage
  70. saveToStorage() {
  71. localStorage.setItem('autoReadState', JSON.stringify(this));
  72. }
  73.  
  74. // 每日点赞计数重置(超过24小时)
  75. resetLikeCounter() {
  76. const lastUpdate = localStorage.getItem('likeTimestamp');
  77. if (lastUpdate && Date.now() - +lastUpdate > 86400000) { // 86400000ms = 24小时
  78. localStorage.setItem('likeCount', 0); // 重置计数
  79. localStorage.setItem('likeTimestamp', Date.now()); // 更新时间戳
  80. }
  81. }
  82. }
  83.  
  84. /**
  85. * 自动阅读核心类
  86. * 负责业务逻辑处理和用户交互
  87. */
  88. class AutoReader {
  89. constructor() {
  90. this.state = new StateManager(); // 初始化状态管理器
  91. this.init(); // 初始化脚本
  92. }
  93.  
  94. // 初始化入口
  95. init() {
  96. window.addEventListener('load', () => {
  97. this.createControlPanel(); // 创建控制面板
  98. this.handleRoute(); // 处理当前路由
  99. setInterval(() => this.updateStatus(), CONFIG.UPDATE_INTERVAL); // 定期更新状态
  100. });
  101. }
  102.  
  103. /**
  104. * 路由处理
  105. * 根据当前页面路径执行不同逻辑
  106. */
  107. handleRoute() {
  108. if (window.location.pathname === '/unseen') { // 未读页面
  109. this.fetchUnseenLinks(); // 获取未读链接
  110. } else if (this.state.isReading) { // 阅读中状态
  111. this.processCurrentPage(); // 处理当前页面内容
  112. }
  113. }
  114.  
  115. /**
  116. * 获取未读帖子链接
  117. */
  118. fetchUnseenLinks() {
  119. // 使用CSS选择器获取所有未读帖子链接
  120. const links = Array.from(document.querySelectorAll('a.title.raw-link.raw-topic-link'))
  121. .map(link => link.getAttribute('href')); // 提取链接
  122.  
  123. if (links.length) { // 存在未读链接
  124. this.state.unseenHrefs = links; // 更新状态
  125. this.state.saveToStorage(); // 保存到本地
  126. this.openNextTopic(); // 打开下一个帖子
  127. } else { // 无未读内容
  128. alert('未发现未读内容');
  129. }
  130. }
  131.  
  132. /**
  133. * 打开下一个帖子
  134. */
  135. openNextTopic() {
  136. const nextUrl = this.state.unseenHrefs.shift(); // 取出队列中第一个链接
  137. if (nextUrl) { // 存在有效链接
  138. this.state.currentTask = 'navigating'; // 设置任务状态为导航
  139. this.state.saveToStorage(); // 保存状态
  140. window.location.href = `${CONFIG.BASE_URL}${nextUrl}`; // 跳转页面
  141. } else { // 链接队列已空
  142. this.navigateToUnseen(); // 回到未读页面重新获取
  143. }
  144. }
  145.  
  146. /**
  147. * 处理当前页面内容(阅读逻辑)
  148. */
  149. processCurrentPage() {
  150. if (this.isErrorPage()) return this.handleError(); // 先检查错误页面
  151.  
  152. // 判断是不是帖子详情页,如果不是,打开第一个未读链接
  153. if (!document.querySelector('article[data-post-id]')) {
  154. this.openNextTopic();
  155. return;
  156. }
  157. // 判断是否存在返回上次阅读的按钮
  158. const backButton = document.querySelector('[title="返回上一个未读帖子"]');
  159. if (backButton) {
  160. backButton.click(); // 点击按钮返回
  161. }
  162.  
  163. // 获取当前页面所有帖子
  164. this.state.posts = Array.from(document.querySelectorAll('article[data-post-id]'));
  165. this.state.currentTask = 'scrolling'; // 设置任务状态为滚动
  166. this.startSmoothScroll(); // 启动平滑滚动
  167. if (this.state.isLiking) this.runAutoLike(); // 启用点赞则执行点赞逻辑
  168. }
  169.  
  170. /**
  171. * 启动平滑滚动
  172. */
  173. startSmoothScroll() {
  174. if (this.state.scrollTimer) return; // 避免重复启动
  175.  
  176. // 记录上一次滚动时间
  177. let lastScrollTime = 0;
  178. // 滚动速度(像素/帧)
  179. const scrollSpeed = CONFIG.SCROLL_OPTIONS.speed;
  180.  
  181. // 使用requestAnimationFrame实现平滑滚动
  182. const scrollStep = () => {
  183. let timestamp = performance.now(); // 获取当前时间戳
  184.  
  185. // 控制滚动频率,防止过快
  186. if (timestamp - lastScrollTime < CONFIG.SCROLL_OPTIONS.interval) {
  187. this.state.scrollTimer = requestAnimationFrame(scrollStep);
  188. return;
  189. }
  190. lastScrollTime = timestamp; // 更新上一次滚动时间
  191.  
  192. window.scrollBy(0, scrollSpeed); // 执行滚动
  193.  
  194. // 判断是否阅读完毕
  195. const divReplies = document.querySelector('div.timeline-replies'); // 查找底部元素
  196. if (divReplies) {
  197. const parts = divReplies.textContent.trim().replace(/[^0-9/]/g, '').split('/');
  198. // 判断是否相等(如:1/1),表示已到达底部
  199. if (parts.length >= 2 && parts[0] === parts[1]) {
  200. this.stopScrolling(); // 停止滚动
  201. this.openNextTopic(); // 打开下一个帖子
  202. return;
  203. }
  204. }
  205.  
  206. this.markReadPosts(); // 标记已读帖子
  207. this.state.scrollTimer = requestAnimationFrame(scrollStep); // 继续下一帧
  208. };
  209.  
  210. // 开始滚动动画
  211. this.state.scrollTimer = requestAnimationFrame(scrollStep);
  212. }
  213.  
  214. /**
  215. * 停止滚动
  216. */
  217. stopScrolling() {
  218. if (this.state.scrollTimer) {
  219. cancelAnimationFrame(this.state.scrollTimer); // 取消动画帧
  220. this.state.scrollTimer = null; // 重置定时器引用
  221. }
  222. this.state.currentTask = null; // 清除当前任务
  223. }
  224.  
  225. /**
  226. * 标记可见帖子为已读
  227. */
  228. markReadPosts() {
  229. document.querySelectorAll('article[data-post-id]').forEach(post => {
  230. const rect = post.getBoundingClientRect(); // 获取元素位置信息
  231. // 元素完全在视口内时并且是已读状态,标记为已读,
  232. if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
  233. post.classList.add('read-state'); // 添加已读类
  234. }
  235. });
  236. }
  237.  
  238. /**
  239. * 自动点赞逻辑(递归调用实现随机间隔)
  240. */
  241. runAutoLike() {
  242. const likeCount = parseInt(localStorage.getItem('likeCount')) || 0; // 当前点赞数
  243. if (likeCount >= CONFIG.LIKE_LIMIT) return; // 达到上限则停止
  244.  
  245. // 查找未点赞的按钮(优先使用明确的选择器)
  246. const likeButton = document.querySelector('.discourse-reactions-reaction-button:not(.liked)');
  247. if (likeButton) {
  248. likeButton.click(); // 模拟点击
  249. // 更新点赞计数和时间戳
  250. localStorage.setItem('likeCount', likeCount + 1);
  251. localStorage.setItem('likeTimestamp', Date.now());
  252. // 生成随机间隔(递归调用实现链式延迟)
  253. const randomDelay = Math.random() * (CONFIG.LIKE_INTERVAL.max - CONFIG.LIKE_INTERVAL.min) + CONFIG.LIKE_INTERVAL.min;
  254. setTimeout(() => this.runAutoLike(), randomDelay);
  255. }
  256. }
  257.  
  258. /**
  259. * 检测是否为错误页面
  260. * @returns {boolean} 是否为404页面
  261. */
  262. isErrorPage() {
  263. return document.title.includes('找不到页面');
  264. }
  265.  
  266. /**
  267. * 错误页面处理
  268. */
  269. handleError() {
  270. this.state.errorRetries++; // 重试次数加一
  271.  
  272. if (this.state.errorRetries > CONFIG.MAX_RETRIES) { // 超过最大重试次数
  273. this.resetState(); // 重置所有状态
  274. return;
  275. }
  276. this.openNextTopic(); // 尝试打开下一个帖子
  277. }
  278.  
  279. /**
  280. * 重置所有状态(用于错误处理或用户重置)
  281. */
  282. resetState() {
  283. this.state.initState(); // 恢复初始状态
  284. this.state.saveToStorage(); // 保存到本地
  285. }
  286.  
  287. /**
  288. * 创建控制面板
  289. */
  290. createControlPanel() {
  291. const controls = document.createElement('div'); // 容器元素
  292. controls.className = 'fixed bottom-4 left-4 z-50 bg-white flex flex-col gap-2'; // 样式
  293.  
  294. // 创建阅读控制按钮
  295. this.createControlButton(controls, 'openRead', '开始阅读', '停止阅读', () => {
  296. this.state.isReading = !this.state.isReading; // 切换阅读状态
  297. this.state.saveToStorage(); // 保存状态
  298. this.state.isReading ? this.processCurrentPage() : this.stopScrolling();// 根据状态执行相应操作
  299. this.updateStatus();// 更新状态
  300. document.getElementById('openRead').textContent = this.state.isReading ? '停止阅读' : '开始阅读';// 更新按钮文本
  301. });
  302.  
  303. // 创建点赞控制按钮
  304. this.createControlButton(controls, 'openUP', '启用点赞', '禁用点赞', () => {
  305. this.state.isLiking = !this.state.isLiking; // 切换点赞状态
  306. this.state.saveToStorage(); // 保存状态
  307. this.updateStatus(); // 更新状态
  308. document.getElementById('openUP').textContent = this.state.isLiking ? '禁用点赞' : '启用点赞';// 更新按钮文本
  309. });
  310.  
  311. // 创建重置列表按钮
  312. this.createControlButton(controls, 'resetList', '重置列表', '重置列表', () => {
  313. if (confirm('确定要重置未读列表吗?')) { // 确认提示
  314. this.resetState(); // 重置状态
  315. alert('未读列表已重置');
  316. }
  317. });
  318.  
  319. // 创建状态显示面板
  320. const status = document.createElement('div'); // 状态面板
  321. status.id = 'auto-read-status'; // 唯一ID
  322. // 在按钮的上面显示,并且在左侧顶上
  323. status.className = 'fixed top-20 left-5 z-9999 bg-white shadow-lg rounded-lg p-2 flex flex-col gap-1';
  324. controls.appendChild(status); // 添加到控制面板
  325. this.updateStatus(); // 初始化状态显示
  326.  
  327. document.body.appendChild(controls); // 添加到页面
  328. }
  329.  
  330. /**
  331. * 创建通用控制按钮
  332. * @param {HTMLElement} parent - 父容器
  333. * @param {string} id - 唯一ID
  334. * @param {string} startText - 初始文本
  335. * @param {string} stopText - 激活后文本
  336. * @param {Function} onClick - 点击事件处理函数
  337. */
  338. createControlButton(parent, id, startText, stopText, onClick) {
  339. const button = document.createElement('button'); // 创建按钮元素
  340. // 基础样式
  341. button.id = id;
  342. button.className = 'px-4 py-2 rounded-lg shadow-lg hover:scale-105 transition-all duration-300 bg-white text-black font-bold';
  343. // 初始文本(根据当前状态判断)
  344. button.textContent = this.state.isReading && startText === '开始阅读' ? stopText : startText;
  345. button.addEventListener('click', onClick); // 绑定点击事件
  346. parent.appendChild(button); // 添加到父容器
  347. }
  348.  
  349. /**
  350. * 更新状态显示面板
  351. */
  352. updateStatus() {
  353. const status = document.getElementById('auto-read-status');
  354. if (!status) return; // 面板不存在时返回
  355.  
  356. const likeCount = parseInt(localStorage.getItem('likeCount')) || 0; // 获取点赞计数
  357. // 使用模板字符串更新面板内容
  358. status.innerHTML = `
  359. <div class="font-bold text-sm">
  360. 阅读状态:${this.state.isReading ? '<span class="text-green-600">运行中</span>' : '<span class="text-red-600">已停止</span>'}<br />
  361. 点赞状态:${this.state.isLiking ? '<span class="text-green-600">启用</span>' : '<span class="text-red-600">禁用</span>'}<br />
  362. 今日点赞:${likeCount}/${CONFIG.LIKE_LIMIT}<br />
  363. 剩余帖子:${this.state.unseenHrefs.length}<br />
  364. 错误重试:<span class="text-red-600">${this.state.errorRetries}/${CONFIG.MAX_RETRIES}</span>
  365. </div>
  366. `;
  367. }
  368.  
  369. /**
  370. * 导航到未读页面
  371. */
  372. navigateToUnseen() {
  373. window.location.href = `${CONFIG.BASE_URL}/unseen`; // 跳转URL
  374. }
  375. }
  376.  
  377. // 初始化脚本入口
  378. new AutoReader();
  379. })();

QingJ © 2025

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