NGA Auto Pagerize

简单的自动翻页

目前为 2022-11-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Auto Pagerize
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 1.4.6
  5. // @author snyssss
  6. // @description 简单的自动翻页
  7.  
  8. // @match *://bbs.nga.cn/*
  9. // @match *://ngabbs.com/*
  10. // @match *://nga.178.com/*
  11.  
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. ((ui, n = {}, api = {}, uid) => {
  19. if (!ui) return;
  20.  
  21. // KEY
  22. const ATTACHMENT_STYLE_ENABLE_KEY = "ATTACHMENT_STYLE_ENABLE";
  23. const PAGE_BUTTON_STYLE_ENABLE_KEY = "PAGE_BUTTON_STYLE_ENABLE_KEY";
  24. const HOTKEYS_ENABLE_KEY = "HOTKEYS_ENABLE_KEY";
  25. const FORUM_NAME_ENABLE_KEY = "FORUM_NAME_ENABLE_KEY";
  26. const POST_LOSS_DETECTION_KEY = "POSTS_LOSS_DETECTION_KEY";
  27. const AUTO_CHECK_IN_ENABLE_KEY = "AUTO_CHECK_IN_ENABLE_KEY";
  28. const AUTO_CHECK_IN_LAST_TIME_KEY = "AUTO_CHECK_IN_LAST_TIME_KEY";
  29.  
  30. // 附件样式
  31. const attachmentStyleEnable =
  32. GM_getValue(ATTACHMENT_STYLE_ENABLE_KEY) || false;
  33.  
  34. // 页码样式
  35. const pageButtonStyleEnable =
  36. GM_getValue(PAGE_BUTTON_STYLE_ENABLE_KEY) || false;
  37.  
  38. // 快捷翻页
  39. const hotkeysEnable = GM_getValue(HOTKEYS_ENABLE_KEY) || false;
  40.  
  41. // 版面名称
  42. const forumNameEnable = GM_getValue(FORUM_NAME_ENABLE_KEY) || false;
  43.  
  44. // 抽楼检测
  45. const postLossDetectionEnable = GM_getValue(POST_LOSS_DETECTION_KEY) || false;
  46.  
  47. // 自动签到
  48. const autoCheckInEnable = GM_getValue(AUTO_CHECK_IN_ENABLE_KEY) || false;
  49.  
  50. // 自动签到时间
  51. const autoCheckInLastTime = GM_getValue(AUTO_CHECK_IN_LAST_TIME_KEY) || 0;
  52.  
  53. // 自动签到 UA
  54. const autoCheckInUserAgent = "Nga_Official/80024(Android12)";
  55.  
  56. // 加载脚本
  57. (() => {
  58. const hookFunction = (object, functionName, callback) => {
  59. ((originalFunction) => {
  60. object[functionName] = function () {
  61. const returnValue = originalFunction.apply(this, arguments);
  62.  
  63. callback.apply(this, [returnValue, originalFunction, arguments]);
  64.  
  65. return returnValue;
  66. };
  67. })(object[functionName]);
  68. };
  69.  
  70. const hooked = {
  71. autoPagerize: false,
  72. uniqueTopic: false,
  73. attachmentStyle: false,
  74. pageButtonStyle: false,
  75. hotkeys: false,
  76. forumName: false,
  77. postLossDetection: false,
  78. postLossDetectionTopic: false,
  79. };
  80.  
  81. const hook = () => {
  82. // 翻页
  83. const loadReadHidden = (() => {
  84. const THREAD_MAX_PAGE = 500;
  85.  
  86. const delay = (interval) =>
  87. new Promise((resolve) => setTimeout(resolve, interval));
  88.  
  89. const retry = async (fn, retriesLeft = 10, interval = 160) => {
  90. try {
  91. return await fn();
  92. } catch (error) {
  93. await delay(interval);
  94.  
  95. if (retriesLeft > 0) {
  96. return await retry(fn, retriesLeft - 1, interval);
  97. }
  98. }
  99. };
  100.  
  101. return (p, opt = 1) => {
  102. if (ui.loadReadHidden) {
  103. retry(() => {
  104. if (ui.loadReadHidden.lock) {
  105. throw new Error();
  106. }
  107.  
  108. if (__PAGE) {
  109. const max = __PAGE[1];
  110. const cur = __PAGE[2];
  111.  
  112. if (location.pathname === "/thread.php") {
  113. if (p > THREAD_MAX_PAGE) {
  114. return;
  115. }
  116.  
  117. if (p === 0 && opt === 2 && cur === THREAD_MAX_PAGE) {
  118. return;
  119. }
  120. }
  121.  
  122. if (p < 1 && opt === 1) {
  123. return;
  124. }
  125.  
  126. if (p > max && max > 0) {
  127. p = max;
  128. }
  129.  
  130. if (p === cur) {
  131. return;
  132. }
  133.  
  134. ui.loadReadHidden(p, opt);
  135. }
  136. });
  137. }
  138. };
  139. })();
  140.  
  141. // 自动翻页
  142. if (hooked.autoPagerize === false) {
  143. if (ui.pageBtn) {
  144. const execute = (() => {
  145. const observer = new IntersectionObserver((entries) => {
  146. if (entries.find((item) => item.isIntersecting)) {
  147. loadReadHidden(0, 2);
  148. }
  149. });
  150.  
  151. return () => {
  152. const anchor = document.querySelector('[title="加载下一页"]');
  153.  
  154. if (anchor) {
  155. observer.observe(anchor);
  156. } else {
  157. observer.disconnect();
  158. }
  159. };
  160. })();
  161.  
  162. hookFunction(ui, "pageBtn", execute);
  163.  
  164. hooked.autoPagerize = true;
  165.  
  166. execute();
  167. }
  168. }
  169.  
  170. // 移除重复内容
  171. if (hooked.uniqueTopic === false) {
  172. if (ui.topicArg) {
  173. const execute = () => {
  174. if (location.search.indexOf("searchpost=1") > 0) {
  175. return;
  176. }
  177.  
  178. ui.topicArg.data = ui.topicArg.data.reduce(
  179. (accumulator, currentValue) => {
  180. if (document.contains(currentValue[0])) {
  181. const index = accumulator.findIndex(
  182. (item) => item[8] === currentValue[8]
  183. );
  184.  
  185. if (index < 0) {
  186. return [...accumulator, currentValue];
  187. }
  188.  
  189. currentValue[0].closest("TBODY").remove();
  190. }
  191.  
  192. return accumulator;
  193. },
  194. []
  195. );
  196. };
  197.  
  198. hookFunction(ui.topicArg, "loadAll", execute);
  199.  
  200. hooked.uniqueTopic = true;
  201.  
  202. execute();
  203. }
  204. }
  205.  
  206. // 附件样式
  207. if (hooked.attachmentStyle === false && attachmentStyleEnable) {
  208. if (ui.topicArg) {
  209. const execute = () => {
  210. const elements =
  211. document.querySelectorAll('[title="主题中有附件"]');
  212.  
  213. elements.forEach((element) => {
  214. element.className = "block_txt white nobr vertmod";
  215. element.style = "background-color: #BD7E6D";
  216. element.innerHTML = "附件";
  217. });
  218. };
  219.  
  220. hookFunction(ui.topicArg, "loadAll", execute);
  221.  
  222. hooked.attachmentStyle = true;
  223.  
  224. execute();
  225. }
  226. }
  227.  
  228. // 页码样式
  229. if (hooked.pageButtonStyle === false && pageButtonStyleEnable) {
  230. const execute = () => {
  231. if (ui.pageBtn) {
  232. const elements = document.querySelectorAll('[name="pageball"] A');
  233.  
  234. elements.forEach((element) => {
  235. const matches = element.innerHTML.match(/\d+/);
  236.  
  237. if (matches) {
  238. element.innerHTML = `&nbsp;${matches[0]}&nbsp;`;
  239. }
  240. });
  241. }
  242. };
  243.  
  244. hookFunction(ui, "pageBtn", execute);
  245.  
  246. hooked.pageButtonStyle = true;
  247.  
  248. execute();
  249. }
  250.  
  251. // 快捷翻页
  252. if (hooked.hotkeys === false && hotkeysEnable) {
  253. const execute = () => {
  254. document.addEventListener("keydown", ({ key, ctrlKey }) => {
  255. if (__PAGE) {
  256. const max = __PAGE[1];
  257. const cur = __PAGE[2];
  258.  
  259. const activeElement = document.activeElement;
  260.  
  261. if (activeElement === null || activeElement.tagName !== "BODY") {
  262. return;
  263. }
  264.  
  265. if (key === "ArrowLeft" && ctrlKey) {
  266. loadReadHidden(1);
  267. return;
  268. }
  269.  
  270. if (key === "ArrowLeft") {
  271. loadReadHidden(cur - 1);
  272. return;
  273. }
  274.  
  275. if (key === "ArrowRight" && ctrlKey) {
  276. loadReadHidden(max);
  277. return;
  278. }
  279.  
  280. if (key === "ArrowRight") {
  281. loadReadHidden(cur + 1);
  282. return;
  283. }
  284. }
  285. });
  286. };
  287.  
  288. hooked.hotkeys = true;
  289.  
  290. execute();
  291. }
  292.  
  293. // 版面名称
  294. if (hooked.forumName === false && forumNameEnable) {
  295. if (ui.topicArg) {
  296. if (!n.doRequest || !api.indexForumList) {
  297. return;
  298. }
  299.  
  300. class Queue {
  301. execute(task) {
  302. task(this.data).finally(() => {
  303. if (this.waitingQueue.length) {
  304. const next = this.waitingQueue.shift();
  305.  
  306. this.execute(next);
  307. } else {
  308. this.isRunning = false;
  309. }
  310. });
  311. }
  312.  
  313. enqueue(task) {
  314. if (this.initialized === false) {
  315. this.initialized = true;
  316. this.init();
  317. }
  318.  
  319. if (this.isRunning) {
  320. this.waitingQueue.push(task);
  321. } else {
  322. this.isRunning = true;
  323.  
  324. this.execute(task);
  325. }
  326. }
  327.  
  328. init() {
  329. this.enqueue(async () => {
  330. this.data = await new Promise((resolve) => {
  331. try {
  332. n.doRequest({
  333. u: api.indexForumList(),
  334. f: function (res) {
  335. if (res.data) {
  336. resolve(res.data[0]);
  337. } else {
  338. resolve({});
  339. }
  340. },
  341. });
  342. } catch (e) {
  343. resolve({});
  344. }
  345. });
  346. });
  347. }
  348.  
  349. constructor() {
  350. this.waitingQueue = [];
  351. this.isRunning = false;
  352.  
  353. this.initialized = false;
  354. }
  355. }
  356.  
  357. const deepSearch = (content = {}, fid = 0) => {
  358. const children = Object.values(content);
  359.  
  360. for (let i = 0; i < children.length; i += 1) {
  361. const item = children[i];
  362.  
  363. if (item.fid === fid) {
  364. return item;
  365. }
  366.  
  367. if (item.content) {
  368. const result = deepSearch(item.content || [], fid);
  369.  
  370. if (result !== null) {
  371. return result;
  372. }
  373. }
  374. }
  375.  
  376. return null;
  377. };
  378.  
  379. const queue = new Queue();
  380.  
  381. const execute = () => {
  382. if (location.search.indexOf("authorid") < 0) {
  383. return;
  384. }
  385.  
  386. ui.topicArg.data.forEach((item) => {
  387. const parentNode = item[1].closest(".c2");
  388.  
  389. if (parentNode.querySelector(".titleadd2") === null) {
  390. const fid = item[7];
  391.  
  392. queue.enqueue(async (data) => {
  393. const result = deepSearch(data.all, parseInt(fid, 10));
  394.  
  395. if (result) {
  396. const anchor = parentNode.querySelector(".topic_content");
  397.  
  398. const title = document.createElement("SPAN");
  399.  
  400. title.className = "titleadd2";
  401. title.innerHTML = `<a href="/thread.php?fid=${fid}" class="silver">[${result.name}]</a>`;
  402.  
  403. if (anchor) {
  404. anchor.before(title);
  405. } else {
  406. parentNode.append(title);
  407. }
  408. }
  409. });
  410. }
  411. });
  412. };
  413.  
  414. hookFunction(ui.topicArg, "loadAll", execute);
  415.  
  416. hooked.forumName = true;
  417.  
  418. execute();
  419. }
  420. }
  421.  
  422. // 抽楼检测
  423. if (postLossDetectionEnable) {
  424. const cache = {};
  425.  
  426. const fetchData = async (key, tid, pid) => {
  427. if (cache[key] === undefined) {
  428. cache[key] = await new Promise((resolve) => {
  429. fetch(`/post.php?lite=js&tid=${tid}&pid=${pid}`)
  430. .then((res) => res.blob())
  431. .then((blob) => {
  432. const reader = new FileReader();
  433.  
  434. reader.onload = () => {
  435. const text = reader.result;
  436. const result = JSON.parse(
  437. text.replace("window.script_muti_get_var_store=", "")
  438. );
  439.  
  440. const { error } = result;
  441.  
  442. if (error) {
  443. resolve(error[0]);
  444. } else {
  445. resolve("");
  446. }
  447. };
  448.  
  449. reader.readAsText(blob, "GBK");
  450. })
  451. .catch(() => {
  452. resolve("");
  453. });
  454. });
  455. }
  456.  
  457. return cache[key];
  458. };
  459.  
  460. if (hooked.postLossDetection === false) {
  461. if (ui.postArg && uid) {
  462. const execute = () => {
  463. Object.values(ui.postArg.data)
  464. .filter((item) => +item.pAid === uid)
  465. .forEach(async ({ tid, pid, pInfoC }) => {
  466. const key = `${tid}#${pid}`;
  467.  
  468. const error = await fetchData(key, tid, pid);
  469.  
  470. if (error) {
  471. if (pInfoC) {
  472. if (pInfoC.querySelector(`[id="${key}"]`)) {
  473. return;
  474. }
  475.  
  476. const node = document.createElement("SPAN");
  477.  
  478. node.id = key;
  479. node.className =
  480. "small_colored_text_btn block_txt_c0 stxt";
  481. node.style = "margin-left: 0.4em; line-height: inherit;";
  482. node.innerHTML = error;
  483.  
  484. pInfoC.prepend(node);
  485. }
  486. }
  487. });
  488. };
  489.  
  490. hookFunction(ui.postArg, "proc", execute);
  491.  
  492. hooked.postLossDetection = true;
  493.  
  494. execute();
  495. }
  496. }
  497.  
  498. if (hooked.postLossDetectionTopic === false) {
  499. if (ui.topicArg && uid) {
  500. const execute = () => {
  501. if (location.search.indexOf(`authorid=${uid}`) < 0) {
  502. return;
  503. }
  504.  
  505. Object.values(ui.topicArg.data).forEach(async (item) => {
  506. const tid = item[8];
  507. const pid = item[9] || 0;
  508.  
  509. const postDate = item[12];
  510.  
  511. if (pid && postDate) {
  512. const key = `${tid}#${pid}`;
  513.  
  514. const error = await fetchData(key, tid, pid);
  515.  
  516. if (error) {
  517. const parentNode = item[1].closest(".c2");
  518.  
  519. if (parentNode.querySelector(`[id="${key}"]`)) {
  520. return;
  521. }
  522.  
  523. const anchor = parentNode.querySelector(".topic_content");
  524.  
  525. const node = document.createElement("SPAN");
  526.  
  527. node.id = key;
  528. node.className = "small_colored_text_btn block_txt_c0";
  529. node.style = "float:right; line-height: inherit;";
  530. node.innerHTML = error;
  531.  
  532. if (anchor) {
  533. anchor.after(node);
  534. } else {
  535. parentNode.append(node);
  536. }
  537. }
  538. }
  539. });
  540. };
  541.  
  542. hookFunction(ui.topicArg, "loadAll", execute);
  543.  
  544. hooked.postLossDetectionTopic = true;
  545.  
  546. execute();
  547. }
  548. }
  549. }
  550. };
  551.  
  552. hookFunction(ui, "eval", () => {
  553. if (Object.values(hooked).findIndex((item) => item === false) < 0) {
  554. return;
  555. }
  556.  
  557. hook();
  558. });
  559.  
  560. hook();
  561. })();
  562.  
  563. // 加载菜单项
  564. (() => {
  565. if (attachmentStyleEnable) {
  566. GM_registerMenuCommand("附件样式:启用", () => {
  567. GM_setValue(ATTACHMENT_STYLE_ENABLE_KEY, false);
  568. location.reload();
  569. });
  570. } else {
  571. GM_registerMenuCommand("附件样式:禁用", () => {
  572. GM_setValue(ATTACHMENT_STYLE_ENABLE_KEY, true);
  573. location.reload();
  574. });
  575. }
  576.  
  577. if (pageButtonStyleEnable) {
  578. GM_registerMenuCommand("页码样式:启用", () => {
  579. GM_setValue(PAGE_BUTTON_STYLE_ENABLE_KEY, false);
  580. location.reload();
  581. });
  582. } else {
  583. GM_registerMenuCommand("页码样式:禁用", () => {
  584. GM_setValue(PAGE_BUTTON_STYLE_ENABLE_KEY, true);
  585. location.reload();
  586. });
  587. }
  588.  
  589. if (hotkeysEnable) {
  590. GM_registerMenuCommand("快捷翻页:启用", () => {
  591. GM_setValue(HOTKEYS_ENABLE_KEY, false);
  592. location.reload();
  593. });
  594. } else {
  595. GM_registerMenuCommand("快捷翻页:禁用", () => {
  596. GM_setValue(HOTKEYS_ENABLE_KEY, true);
  597. location.reload();
  598. });
  599. }
  600.  
  601. if (forumNameEnable) {
  602. GM_registerMenuCommand("版面名称:启用", () => {
  603. GM_setValue(FORUM_NAME_ENABLE_KEY, false);
  604. location.reload();
  605. });
  606. } else {
  607. GM_registerMenuCommand("版面名称:禁用", () => {
  608. GM_setValue(FORUM_NAME_ENABLE_KEY, true);
  609. location.reload();
  610. });
  611. }
  612.  
  613. if (postLossDetectionEnable) {
  614. GM_registerMenuCommand("抽楼检测:启用", () => {
  615. GM_setValue(POST_LOSS_DETECTION_KEY, false);
  616. location.reload();
  617. });
  618. } else {
  619. GM_registerMenuCommand("抽楼检测:禁用", () => {
  620. GM_setValue(POST_LOSS_DETECTION_KEY, true);
  621. location.reload();
  622. });
  623. }
  624.  
  625. if (autoCheckInEnable) {
  626. GM_registerMenuCommand("自动签到:启用", () => {
  627. GM_setValue(AUTO_CHECK_IN_ENABLE_KEY, false);
  628. GM_setValue(AUTO_CHECK_IN_LAST_TIME_KEY, 0);
  629. location.reload();
  630. });
  631. } else {
  632. GM_registerMenuCommand("自动签到:禁用", () => {
  633. GM_setValue(AUTO_CHECK_IN_ENABLE_KEY, true);
  634. location.reload();
  635. });
  636. }
  637. })();
  638.  
  639. // 自动签到
  640. if (autoCheckInEnable && uid) {
  641. const today = new Date();
  642.  
  643. const lastTime = new Date(autoCheckInLastTime);
  644.  
  645. const isToday =
  646. lastTime.getDate() === today.getDate() &&
  647. lastTime.getMonth() === today.getMonth() &&
  648. lastTime.getFullYear() === today.getFullYear();
  649.  
  650. if (isToday === false) {
  651. fetch(`/nuke.php?__lib=check_in&__act=check_in&lite=js`, {
  652. method: "POST",
  653. headers: {
  654. "X-User-Agent": autoCheckInUserAgent,
  655. },
  656. })
  657. .then((res) => res.blob())
  658. .then((blob) => {
  659. const reader = new FileReader();
  660.  
  661. reader.onload = () => {
  662. const text = reader.result;
  663. const result = JSON.parse(
  664. text.replace("window.script_muti_get_var_store=", "")
  665. );
  666.  
  667. const { data, error } = result;
  668.  
  669. if (data || error) {
  670. alert((data || error)[0]);
  671. }
  672.  
  673. GM_setValue(AUTO_CHECK_IN_LAST_TIME_KEY, today.getTime());
  674. };
  675.  
  676. reader.readAsText(blob, "GBK");
  677. });
  678. }
  679. }
  680. })(commonui, __NUKE, __API, __CURRENT_UID);