NGA Cache History

将帖子内容缓存 IndexedDB 里,以便在帖子被审核/删除时仍能查看

  1. // ==UserScript==
  2. // @name NGA Cache History
  3. // @name:zh-CN NGA 帖子缓存插件
  4. // @namespace https://gf.qytechs.cn/users/263018
  5. // @version 1.2.4
  6. // @author snyssss
  7. // @description 将帖子内容缓存 IndexedDB 里,以便在帖子被审核/删除时仍能查看
  8. // @license MIT
  9.  
  10. // @match *://bbs.nga.cn/*
  11. // @match *://ngabbs.com/*
  12. // @match *://nga.178.com/*
  13.  
  14. // @grant GM_addStyle
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_registerMenuCommand
  18. // @grant unsafeWindow
  19.  
  20. // @noframes
  21. // ==/UserScript==
  22. (async ({ commonui: ui, _LOADERREAD: loader }) => {
  23. // 检查是否支持 IndexedDB
  24. if (window.indexedDB === undefined) {
  25. return;
  26. }
  27.  
  28. // 常量
  29. const VERSION = 1;
  30. const DB_NAME = "NGA_CACHE";
  31. const TABLE_NAME = "reads";
  32. const SHOW_DIFFRENCE_KEY = "SHOW_DIFFRENCE";
  33. const EXPIRE_DURATION_KEY = "EXPIRE_DURATION";
  34. const REFETCH_NOTIFICATION_INTERVAL_KEY = "REFETCH_NOTIFICATION_INTERVAL";
  35.  
  36. // 显示差异
  37. const SHOW_DIFFRENCE = GM_getValue(SHOW_DIFFRENCE_KEY, false);
  38.  
  39. // 缓存时长
  40. const EXPIRE_DURATION = GM_getValue(EXPIRE_DURATION_KEY, 7);
  41.  
  42. // 获取提示信息间隔
  43. const REFETCH_NOTIFICATION_INTERVAL = GM_getValue(
  44. REFETCH_NOTIFICATION_INTERVAL_KEY,
  45. 10
  46. );
  47.  
  48. // 判断帖子是否正常
  49. const isSuccess = () => {
  50. return ui;
  51. };
  52.  
  53. // 格式化 URL
  54. const formatUrl = (url) => {
  55. // 分割 URL
  56. const urlSplit = url.split("?");
  57.  
  58. // 获取页面参数
  59. const params = new URLSearchParams(urlSplit[1]);
  60.  
  61. // 如果是第一页,移除页码参数
  62. if (params.get("page") === "1") {
  63. params.delete("page");
  64. }
  65.  
  66. // 移除 _ff 参数
  67. params.delete("_ff");
  68.  
  69. // 返回格式化后的结果
  70. return `${urlSplit[0]}?${params.toString()}`;
  71. };
  72.  
  73. // 获取首页 URL
  74. const getHeadUrl = (url) => {
  75. // 格式化 URL
  76. url = formatUrl(url);
  77.  
  78. // 分割 URL
  79. const urlSplit = url.split("?");
  80.  
  81. // 获取页面参数
  82. const params = new URLSearchParams(urlSplit[1]);
  83.  
  84. // 获取 TID
  85. const tid = params.get("tid");
  86.  
  87. // 返回首页 URL
  88. return `${urlSplit[0]}?tid=${tid}`;
  89. };
  90.  
  91. // 获取数据库实例
  92. const db = await new Promise((resolve) => {
  93. // 打开 IndexedDB 数据库
  94. const request = window.indexedDB.open(DB_NAME, VERSION);
  95.  
  96. // 如果数据库不存在则创建
  97. request.onupgradeneeded = (event) => {
  98. // 创建表
  99. const store = event.target.result.createObjectStore(TABLE_NAME, {
  100. keyPath: "url",
  101. });
  102.  
  103. // 创建索引,用于清除过期数据
  104. store.createIndex("timestamp", "timestamp");
  105. };
  106.  
  107. // 成功后返回实例
  108. request.onsuccess = (event) => {
  109. resolve(event.target.result);
  110. };
  111. });
  112.  
  113. // 获取数据
  114. const get = (url, onsuccess, onerror = () => {}) => {
  115. // 格式化 URL
  116. url = formatUrl(url);
  117.  
  118. // 只缓存帖子内容
  119. if (url.indexOf("/read.php") < 0) {
  120. return;
  121. }
  122.  
  123. // 创建事务
  124. const transaction = db.transaction([TABLE_NAME], "readonly");
  125.  
  126. // 获取对象仓库
  127. const store = transaction.objectStore(TABLE_NAME);
  128.  
  129. // 获取数据
  130. const request = store.get(url);
  131.  
  132. // 成功后处理数据
  133. request.onsuccess = (event) => {
  134. // 获取页面对象
  135. const data = event.target.result;
  136.  
  137. // 不存在则抛出异常
  138. if (data === undefined) {
  139. onerror();
  140. return;
  141. }
  142.  
  143. // 处理数据
  144. onsuccess(data);
  145. };
  146.  
  147. // 失败后抛出异常
  148. request.onerror = () => {
  149. onerror();
  150. };
  151. };
  152.  
  153. // 删除超时数据
  154. const expire = (offset) => {
  155. // 创建事务
  156. const transaction = db.transaction([TABLE_NAME], "readwrite");
  157.  
  158. // 获取对象仓库
  159. const store = transaction.objectStore(TABLE_NAME);
  160.  
  161. // 获取索引
  162. const index = store.index("timestamp");
  163.  
  164. // 查找超时数据
  165. const request = index.openCursor(
  166. IDBKeyRange.upperBound(Date.now() - offset)
  167. );
  168.  
  169. // 成功后删除数据
  170. request.onsuccess = (event) => {
  171. const cursor = event.target.result;
  172.  
  173. if (cursor) {
  174. store.delete(cursor.primaryKey);
  175.  
  176. cursor.continue();
  177. }
  178. };
  179. };
  180.  
  181. // 删除数据
  182. const remove = (url, onsuccess = () => {}, onerror = () => {}) => {
  183. // 格式化 URL
  184. url = formatUrl(url);
  185.  
  186. // 创建事务
  187. const transaction = db.transaction([TABLE_NAME], "readwrite");
  188.  
  189. // 获取对象仓库
  190. const store = transaction.objectStore(TABLE_NAME);
  191.  
  192. // 删除数据
  193. const request = store.delete(url);
  194.  
  195. // 成功后回调
  196. request.onsuccess = () => {
  197. onsuccess();
  198. };
  199.  
  200. // 失败后回调
  201. request.onerror = () => {
  202. onerror();
  203. };
  204. };
  205.  
  206. // 写入数据
  207. const put = (url, data, onsuccess = () => {}, onerror = () => {}) => {
  208. // 格式化 URL
  209. url = formatUrl(url);
  210.  
  211. // 创建事务
  212. const transaction = db.transaction([TABLE_NAME], "readwrite");
  213.  
  214. // 获取对象仓库
  215. const store = transaction.objectStore(TABLE_NAME);
  216.  
  217. // 写入数据
  218. const request = store.put({
  219. url,
  220. timestamp: Date.now(),
  221. ...data,
  222. });
  223.  
  224. // 成功后回调
  225. request.onsuccess = () => {
  226. onsuccess();
  227. };
  228.  
  229. // 失败后回调
  230. request.onerror = () => {
  231. onerror();
  232. };
  233. };
  234.  
  235. // 缓存数据
  236. const save = (url) => {
  237. // 格式化 URL
  238. url = formatUrl(url);
  239.  
  240. // 只缓存帖子内容
  241. if (url.indexOf("/read.php") < 0) {
  242. return;
  243. }
  244.  
  245. // 重新请求原始数据用于缓存
  246. fetch(url)
  247. .then((res) => res.blob())
  248. .then((res) => {
  249. // 读取内容
  250. const reader = new FileReader();
  251.  
  252. reader.onload = async () => {
  253. // 读取内容
  254. const content = reader.result;
  255.  
  256. // 解析标题
  257. const parser = new DOMParser();
  258. const html = parser.parseFromString(content, "text/html");
  259. const title = (() => {
  260. const str = html.querySelector("title").textContent;
  261. const index = str.lastIndexOf(" ");
  262.  
  263. if (index > 0) {
  264. return str.substring(0, index);
  265. }
  266.  
  267. return str;
  268. })();
  269.  
  270. // 没有楼层,说明卡审核
  271. if (content.indexOf("commonui.postArg.proc(") < 0) {
  272. return;
  273. }
  274.  
  275. // 找到 ID 是 postcontainer 开头的元素
  276. const containers = html.querySelectorAll("[id^=postcontainer]");
  277.  
  278. if (containers.length === 0) {
  279. return;
  280. }
  281.  
  282. // 有锚点,但是找不到楼层,也是卡审核
  283. const anchor = url.match(/(#pid\d+Anchor)$/);
  284.  
  285. if (anchor && html.querySelector(anchor[1]) === null) {
  286. return;
  287. }
  288.  
  289. // 如果未开启浏览记录,直接写入缓存
  290. if (SHOW_DIFFRENCE === false) {
  291. put(url, {
  292. title,
  293. content,
  294. });
  295. }
  296. // 否则判断是否是正常的翻页,如果是则需要更新最大楼层数
  297. else {
  298. // 分割 URL
  299. const urlSplit = url.split("?");
  300.  
  301. // 获取页面参数
  302. const params = new URLSearchParams(urlSplit[1]);
  303.  
  304. // 移除 TID 参数
  305. params.delete("tid");
  306.  
  307. // 移除页码参数
  308. params.delete("page");
  309.  
  310. // 如果仍有参数,只缓存当前页,无需更新最大楼层数
  311. if (params.size > 0) {
  312. put(url, {
  313. title,
  314. content,
  315. });
  316. }
  317. // 否则需要更新最大楼层数
  318. else {
  319. // 获取首页 URL
  320. const headUrl = getHeadUrl(url);
  321.  
  322. // 当前页不是首页,写入缓存
  323. if (headUrl !== url) {
  324. put(url, {
  325. title,
  326. content,
  327. });
  328. }
  329.  
  330. // 获取当前页面的最大楼层数
  331. const count = parseInt(
  332. containers[containers.length - 1]
  333. .getAttribute("id")
  334. .replace("postcontainer", ""),
  335. 10
  336. );
  337.  
  338. // 获取首页缓存
  339. get(
  340. headUrl,
  341. (data) => {
  342. // 获取缓存楼层数
  343. const cache = data.rows || 0;
  344.  
  345. // 计算最大楼层数
  346. const max = Math.max(count, cache);
  347.  
  348. // 当前页是首页,直接更新缓存
  349. if (headUrl === url) {
  350. put(url, {
  351. title,
  352. content,
  353. rows: max,
  354. });
  355.  
  356. loadAction();
  357. return;
  358. }
  359.  
  360. // 如果与缓存的最大楼层数相同,无需更新
  361. if (max === cache) {
  362. return;
  363. }
  364.  
  365. // 更新缓存
  366. put(headUrl, {
  367. ...data,
  368. rows: max,
  369. });
  370. },
  371. () => {
  372. // 当前页是首页,直接更新缓存
  373. if (headUrl === url) {
  374. put(url, {
  375. title,
  376. content,
  377. rows: count,
  378. });
  379.  
  380. loadAction();
  381. }
  382. }
  383. );
  384. }
  385. }
  386. };
  387.  
  388. reader.readAsText(res, "GBK");
  389. });
  390. };
  391.  
  392. // 读取数据
  393. const load = (url, document) => {
  394. // 格式化 URL
  395. url = formatUrl(url);
  396.  
  397. return get(url, (data) => {
  398. // 加载缓存内容
  399. const html = document.open("text/html", "replace");
  400.  
  401. html.write(data.content);
  402. html.close();
  403.  
  404. // 缓存时间格式
  405. const formatedDate = (() => {
  406. const date = new Date(data.timestamp);
  407. const year = date.getFullYear();
  408. const month = ("0" + (date.getMonth() + 1)).slice(-2);
  409. const day = ("0" + date.getDate()).slice(-2);
  410. const hours = ("0" + date.getHours()).slice(-2);
  411. const minutes = ("0" + date.getMinutes()).slice(-2);
  412.  
  413. return `${year}-${month}-${day} ${hours}:${minutes}`;
  414. })();
  415.  
  416. // 写入缓存时间
  417. (() => {
  418. const execute = () => {
  419. const container = document.querySelector('td[id^="postcontainer"]');
  420.  
  421. if (container) {
  422. const elements = container.querySelectorAll(":scope > .clear");
  423.  
  424. const anchor = elements[elements.length - 1];
  425.  
  426. if (anchor) {
  427. anchor.insertAdjacentHTML(
  428. "afterend",
  429. `<h4 class="silver subtitle">缓存</h4><span class="block_txt block_txt_c3">${formatedDate}</span>`
  430. );
  431. return;
  432. }
  433. }
  434.  
  435. setTimeout(execute, 160);
  436. };
  437.  
  438. execute();
  439. })();
  440. });
  441. };
  442.  
  443. // STYLE
  444. GM_addStyle(`
  445. .s-table-wrapper {
  446. height: calc((2em + 10px) * 11 + 3px);
  447. overflow-y: auto;
  448. }
  449. .s-table {
  450. margin: 0;
  451. }
  452. .s-table th,
  453. .s-table td {
  454. position: relative;
  455. white-space: nowrap;
  456. }
  457. .s-table th {
  458. position: sticky;
  459. top: 2px;
  460. z-index: 1;
  461. }
  462. .s-text-ellipsis > * {
  463. flex: 1;
  464. width: 1px;
  465. overflow: hidden;
  466. text-overflow: ellipsis;
  467. }
  468. `);
  469.  
  470. // UI
  471. const loadUI = () => {
  472. if (!ui) {
  473. return;
  474. }
  475.  
  476. const content = (() => {
  477. const c = document.createElement("div");
  478.  
  479. c.innerHTML = `
  480. <div class="s-table-wrapper" style="width: 1000px; max-width: 95vw;">
  481. <table class="s-table forumbox">
  482. <thead>
  483. <tr class="block_txt_c0">
  484. <th class="c1" width="1">时间</th>
  485. <th class="c2">内容</th>
  486. <th class="c3" width="1">操作</th>
  487. </tr>
  488. </thead>
  489. <tbody></tbody>
  490. </table>
  491. </div>
  492. <div style="display: flex; margin-top: 10px;">
  493. <input type="text" style="flex: 1;" placeholder="目前支持通过帖子链接或标题进行筛选,查询旧数据可能需要一些时间" />
  494. <button>筛选</button>
  495. </div>
  496. `;
  497.  
  498. return c;
  499. })();
  500.  
  501. let position = null;
  502. let hasNext = true;
  503. let isFetching = false;
  504. let keyword = "";
  505.  
  506. const list = content.querySelector("TBODY");
  507.  
  508. const wrapper = content.querySelector(".s-table-wrapper");
  509.  
  510. const keywordInput = content.querySelector("INPUT");
  511.  
  512. const filterButton = content.querySelector("BUTTON");
  513.  
  514. const fetchData = () => {
  515. isFetching = true;
  516.  
  517. // 声明查询数量
  518. let limit = 10;
  519.  
  520. // 创建事务
  521. const transaction = db.transaction([TABLE_NAME], "readonly");
  522.  
  523. // 获取对象仓库
  524. const store = transaction.objectStore(TABLE_NAME);
  525.  
  526. // 获取索引
  527. const index = store.index("timestamp");
  528.  
  529. // 查找数据
  530. const request = index.openCursor(
  531. position ? IDBKeyRange.upperBound(position) : null,
  532. "prev"
  533. );
  534.  
  535. // 加载列表
  536. request.onsuccess = (event) => {
  537. const cursor = event.target.result;
  538.  
  539. if (cursor) {
  540. const { url, title, timestamp } = cursor.value;
  541.  
  542. position = timestamp;
  543.  
  544. if (list.querySelector(`[data-url="${url}"]`)) {
  545. cursor.continue();
  546. return;
  547. }
  548.  
  549. if (keyword) {
  550. if (url.indexOf(keyword) < 0 && title.indexOf(keyword) < 0) {
  551. cursor.continue();
  552. return;
  553. }
  554. }
  555.  
  556. const item = document.createElement("TR");
  557.  
  558. item.className = `row${(list.querySelectorAll("TR").length % 2) + 1}`;
  559.  
  560. item.setAttribute("data-url", url);
  561.  
  562. item.innerHTML = `
  563. <td class="c1">
  564. <span class="nobr">${ui.time2dis(timestamp / 1000)}</span>
  565. </td>
  566. <td class="c2">
  567. <div class="s-text-ellipsis">
  568. <span>
  569. <a href="${url}" title="${title}" class="b nobr">${title}</a>
  570. </span>
  571. </div>
  572. </td>
  573. <td class="c3">
  574. <button>查看缓存版本</button>
  575. <button>删除</button>
  576. </td>
  577. `;
  578.  
  579. const buttons = item.querySelectorAll("button");
  580.  
  581. // 查看缓存版本
  582. buttons[0].onclick = () => {
  583. const iWindow = ui.createCommmonWindow();
  584. const iframe = document.createElement("IFRAME");
  585.  
  586. iframe.style.width = "80vw";
  587. iframe.style.height = "80vh";
  588. iframe.style.border = "none";
  589.  
  590. const iframeLoad = () => {
  591. iframe.removeEventListener("load", iframeLoad);
  592.  
  593. load(url, iframe.contentDocument);
  594. };
  595.  
  596. iframe.addEventListener("load", iframeLoad);
  597.  
  598. iWindow._.addTitle(title);
  599. iWindow._.addContent(iframe);
  600. iWindow._.show();
  601. };
  602.  
  603. // 删除缓存
  604. buttons[1].onclick = () => {
  605. remove(url, () => {
  606. list.removeChild(item);
  607.  
  608. if (list.childElementCount < 10) {
  609. fetchData();
  610. }
  611. });
  612. };
  613.  
  614. list.appendChild(item);
  615.  
  616. if (limit > 1) {
  617. cursor.continue();
  618. } else {
  619. isFetching = false;
  620. }
  621. } else {
  622. hasNext = false;
  623. }
  624.  
  625. limit -= 1;
  626. };
  627. };
  628.  
  629. const refetch = (value = ``) => {
  630. list.innerHTML = ``;
  631.  
  632. position = null;
  633. hasNext = true;
  634. isFetching = false;
  635. keyword = value;
  636.  
  637. keywordInput.value = value;
  638.  
  639. fetchData();
  640. };
  641.  
  642. wrapper.onscroll = () => {
  643. if (isFetching || !hasNext) {
  644. return;
  645. }
  646.  
  647. if (
  648. wrapper.scrollHeight - wrapper.scrollTop <=
  649. wrapper.clientHeight * 1.1
  650. ) {
  651. fetchData();
  652. }
  653. };
  654.  
  655. filterButton.onclick = () => {
  656. refetch(keywordInput.value);
  657. };
  658.  
  659. // 增加菜单项
  660. (() => {
  661. const title = "浏览记录";
  662.  
  663. let window;
  664.  
  665. ui.mainMenu.addItemOnTheFly(title, null, () => {
  666. if (window === undefined) {
  667. window = ui.createCommmonWindow();
  668. }
  669.  
  670. refetch();
  671.  
  672. window._.addTitle(title);
  673. window._.addContent(content);
  674. window._.show();
  675. });
  676. })();
  677. };
  678.  
  679. // 加载操作按钮
  680. // 目前只有主楼的删除缓存
  681. const loadAction = () => {
  682. if (ui && ui.postArg) {
  683. const { data } = ui.postArg;
  684.  
  685. if (data && data["0"] && data["0"]["pid"] === 0) {
  686. const item = data["0"];
  687. const pInfoC = item["pInfoC"];
  688. const anchor = pInfoC.querySelector(`[title="操作菜单"]`);
  689.  
  690. const action = pInfoC.querySelector(`[title="缓存"]`);
  691.  
  692. if (anchor && action === null) {
  693. const element = document.createElement("A");
  694.  
  695. element.href = "javascript:void(0)";
  696. element.className = `postinfob postfavb postoptb small_colored_text_btn block_txt_c0 stxt`;
  697. element.title = "缓存";
  698.  
  699. element.append(...__TXT.svg("turned_in", "", 8));
  700.  
  701. element.onclick = () => {
  702. const url = window.location.href;
  703.  
  704. // 判断是否已有缓存
  705. // 目前默认缓存所有页面,所以一定会有缓存
  706. const cached = element.classList.contains("postoptb");
  707.  
  708. if (cached) {
  709. remove(url, () => {
  710. element.classList.remove("postoptb");
  711. });
  712. } else {
  713. save(url);
  714.  
  715. element.classList.add("postoptb");
  716. }
  717. };
  718.  
  719. anchor.parentElement.insertBefore(element, anchor);
  720. }
  721. }
  722. }
  723. };
  724.  
  725. // 加载消息
  726. const loadMessage = () => {
  727. if (!ui) {
  728. return;
  729. }
  730.  
  731. // 获取消息并写入缓存
  732. const execute = () => {
  733. fetch("/nuke.php?lite=js&__lib=noti&__act=get_all")
  734. .then((res) => res.blob())
  735. .then((blob) => {
  736. const reader = new FileReader();
  737.  
  738. reader.onload = () => {
  739. const text = reader.result;
  740. const result = JSON.parse(
  741. text
  742. .replace("window.script_muti_get_var_store=", "")
  743. .replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": ')
  744. );
  745.  
  746. if (result.data) {
  747. const data = result.data[0];
  748.  
  749. const list = ["0", "1", "2"].reduce(
  750. (res, key) => ({
  751. ...res,
  752. [key]: data[key],
  753. }),
  754. {}
  755. );
  756.  
  757. // 有未读消息,说明抢先获取了,需要弹出提醒
  758. if (data.unread) {
  759. for (let type in list) {
  760. const group = list[type];
  761.  
  762. if (!group) {
  763. continue;
  764. }
  765.  
  766. for (let i = 0; i < group.length; i += 1) {
  767. const item = group[i];
  768.  
  769. if (!item) {
  770. continue;
  771. }
  772.  
  773. if (i < group.length - 5) {
  774. continue;
  775. }
  776.  
  777. ui.notification._add(type, item);
  778. }
  779.  
  780. if (group.length > 5) {
  781. ui.notification._more.style.display = "";
  782. }
  783. }
  784.  
  785. ui.notification.openBox();
  786. }
  787.  
  788. // 处理缓存
  789. // 只处理 0,也就是 _BIT_REPLY 的情况
  790. if (list["0"]) {
  791. const group = list["0"];
  792.  
  793. for (let i = 0; i < group.length; i += 1) {
  794. const item = group[i];
  795.  
  796. if (!item) {
  797. continue;
  798. }
  799.  
  800. // 消息的时间
  801. const time = item[9] * 1000;
  802.  
  803. // 消息的内容,参考 js_notification.js 的 TPL
  804. let str = TPL[0][item[0]];
  805.  
  806. if (typeof str == "function") {
  807. str = str(item);
  808. }
  809.  
  810. str = str
  811. .replace(/\{(_[A-Z0-9_]+)\}/g, function ($0, $1) {
  812. return TPLSUB[$1] ? TPLSUB[$1] : $0;
  813. })
  814. .replace(/\{(_[A-Z0-9_]+)\}/g, function ($0, $1) {
  815. return item[KEY[$1]] ? item[KEY[$1]] : $0;
  816. });
  817.  
  818. // 获取里面出现的所有页面链接
  819. const urls = [
  820. ...str.matchAll(/href="(\/read.php[^"]*)"/gi),
  821. ].map((match) => `${window.location.origin}${match[1]}`);
  822.  
  823. for (let index in urls) {
  824. // 链接地址
  825. const url = urls[index];
  826.  
  827. // 创建事务
  828. const transaction = db.transaction(
  829. [TABLE_NAME],
  830. "readonly"
  831. );
  832.  
  833. // 获取对象仓库
  834. const store = transaction.objectStore(TABLE_NAME);
  835.  
  836. // 获取数据
  837. const request = store.get(url);
  838.  
  839. // 成功后处理数据
  840. request.onsuccess = (event) => {
  841. // 获取页面对象
  842. const data = event.target.result;
  843.  
  844. // 存在,且缓存的时间晚于消息时间则跳过
  845. if (data && data.timestamp > time) {
  846. return;
  847. }
  848.  
  849. // 写入缓存
  850. save(url);
  851. };
  852. }
  853. }
  854. }
  855. }
  856. };
  857.  
  858. reader.readAsText(blob, "GBK");
  859. });
  860. };
  861.  
  862. // NGA 的消息机制是在页面加载的时候由服务端写在页面里再请求消息
  863. // 这会导致页面不刷新的时候,收到的提醒不能及时获知,等刷新时帖子可能已经没了
  864. // 所以需要定时获取最新消息,保证不刷论坛的情况下也会缓存提醒
  865. // 泥潭审核机制导致有消息提示但是找不到帖子的情况待解决
  866. const excuteInterval = () => {
  867. if (REFETCH_NOTIFICATION_INTERVAL > 0) {
  868. execute();
  869. setInterval(execute, REFETCH_NOTIFICATION_INTERVAL * 60 * 1000);
  870. }
  871. };
  872.  
  873. // 启动定时器
  874. if (ui.notification) {
  875. excuteInterval();
  876. } else {
  877. ui.loadNotiScript(excuteInterval);
  878. }
  879. };
  880.  
  881. // 绑定事件
  882. const hook = () => {
  883. // 钩子
  884. const hookFunction = (object, functionName, callback) => {
  885. ((originalFunction) => {
  886. object[functionName] = function () {
  887. const returnValue = originalFunction.apply(this, arguments);
  888.  
  889. callback.apply(this, [returnValue, originalFunction, arguments]);
  890.  
  891. return returnValue;
  892. };
  893. })(object[functionName]);
  894. };
  895.  
  896. // 页面跳转
  897. if (loader) {
  898. hookFunction(loader, "go", (returnValue, originalFunction, arguments) => {
  899. if (arguments[1]) {
  900. const { url } = arguments[1];
  901.  
  902. save(url);
  903. }
  904. });
  905. }
  906.  
  907. // 快速翻页
  908. if (ui) {
  909. hookFunction(
  910. ui,
  911. "loadReadHidden",
  912. (returnValue, originalFunction, arguments) => {
  913. if (arguments && __PAGE) {
  914. const p = (() => {
  915. if (arguments[1] & 2) {
  916. return __PAGE[2] + 1;
  917. }
  918.  
  919. if (arguments[1] & 4) {
  920. return __PAGE[2] - 1;
  921. }
  922.  
  923. return arguments[0];
  924. })();
  925.  
  926. if (p < 1 || (__PAGE[1] > 0 && p > __PAGE[1])) {
  927. return;
  928. }
  929.  
  930. const urlParams = new URLSearchParams(window.location.search);
  931.  
  932. urlParams.set("page", p);
  933.  
  934. const url = `${window.location.origin}${
  935. window.location.pathname
  936. }?${urlParams.toString()}`;
  937.  
  938. save(url);
  939. }
  940. }
  941. );
  942. }
  943.  
  944. // 显示浏览记录或恢复帖子列表里异常的帖子
  945. if (ui && ui.topicArg) {
  946. const execute = () => {
  947. ui.topicArg.data.forEach((item) => {
  948. const tid = item[8];
  949. const postDate = item[12];
  950.  
  951. const url = `${window.location.origin}/read.php?tid=${tid}`;
  952.  
  953. get(url, (data) => {
  954. if (postDate > 0) {
  955. if (SHOW_DIFFRENCE) {
  956. const replies = parseInt(item[0].innerHTML, 10);
  957. const rows = data.rows === undefined ? replies : data.rows;
  958.  
  959. const diffrence = replies - rows;
  960.  
  961. if (diffrence > 0) {
  962. const page = Math.ceil(rows / 20);
  963.  
  964. if (page > 1) {
  965. item[0].setAttribute("href", `${url}&page=${page}`);
  966. }
  967.  
  968. item[0].innerHTML = `${replies}<small>(+${diffrence})</small>`;
  969. }
  970.  
  971. item[1].style.opacity = "0.5";
  972. }
  973. return;
  974. }
  975.  
  976. item[1].innerHTML = data.title;
  977. item[2].innerHTML = "缓存";
  978. item[3].innerHTML = ui.time2dis(data.timestamp / 1000);
  979. });
  980. });
  981. };
  982.  
  983. hookFunction(ui.topicArg, "loadAll", execute);
  984. execute();
  985. }
  986. };
  987.  
  988. // 加载菜单项
  989. (() => {
  990. GM_registerMenuCommand(
  991. `浏览记录:${SHOW_DIFFRENCE ? "显示" : "关闭"}`,
  992. () => {
  993. GM_setValue(SHOW_DIFFRENCE_KEY, !SHOW_DIFFRENCE);
  994. location.reload();
  995. }
  996. );
  997.  
  998. GM_registerMenuCommand(`缓存天数:${EXPIRE_DURATION} 天`, () => {
  999. const input = prompt("请输入缓存天数(最大1000):", EXPIRE_DURATION);
  1000.  
  1001. if (input) {
  1002. const value = parseInt(input, 10);
  1003.  
  1004. if (value > 0 && value <= 1000) {
  1005. GM_setValue(EXPIRE_DURATION_KEY, value);
  1006.  
  1007. location.reload();
  1008. }
  1009. }
  1010. });
  1011.  
  1012. GM_registerMenuCommand(
  1013. `消息刷新间隔:${REFETCH_NOTIFICATION_INTERVAL} 分钟`,
  1014. () => {
  1015. const input = prompt(
  1016. "请输入消息刷新间隔(单位:分钟,设置为 0 的时候不启用):",
  1017. REFETCH_NOTIFICATION_INTERVAL
  1018. );
  1019.  
  1020. if (input) {
  1021. const value = parseInt(input, 10);
  1022.  
  1023. if (value <= 1440) {
  1024. GM_setValue(REFETCH_NOTIFICATION_INTERVAL_KEY, value);
  1025.  
  1026. location.reload();
  1027. }
  1028. }
  1029. }
  1030. );
  1031. })();
  1032.  
  1033. // 执行脚本
  1034. (() => {
  1035. // 绑定事件
  1036. hook();
  1037.  
  1038. // 删除超时数据
  1039. expire(EXPIRE_DURATION * 24 * 60 * 60 * 1000);
  1040.  
  1041. // 加载UI
  1042. loadUI();
  1043.  
  1044. // 加载消息
  1045. loadMessage();
  1046.  
  1047. // 当前链接地址
  1048. const url = window.location.href;
  1049.  
  1050. // 帖子正常的情况下缓存数据,否则尝试从缓存中读取
  1051. if (isSuccess()) {
  1052. save(url);
  1053. } else {
  1054. load(url, document);
  1055. }
  1056. })();
  1057. })(unsafeWindow);

QingJ © 2025

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