Soul++

提升你的魂+使用体验

  1. // ==UserScript==
  2. // @name Soul++
  3. // @namespace SoulPlusPlus
  4. // @version 1.0.2
  5. // @description 提升你的魂+使用体验
  6. // @run-at document-start
  7. // @author 镜花水中捞月
  8. // @homepage https://github.com/FetchTheMoon
  9. // @icon64 https://cdn.jsdelivr.net/gh/FetchTheMoon/UserScript/LOGO.png
  10. // @supportURL https://github.com/FetchTheMoon/UserScript/issues
  11. // ----------------COPY START---------------------
  12. // @match https://*.spring-plus.net/*
  13. // @match https://*.summer-plus.net/*
  14. // @match https://*.soul-plus.net/*
  15. // @match https://*.south-plus.net/*
  16. // @match https://*.north-plus.net/*
  17. // @match https://*.snow-plus.net/*
  18. // @match https://*.level-plus.net/*
  19. // @match https://*.white-plus.net/*
  20. // @match https://*.imoutolove.me/*
  21. // @match https://*.south-plus.org/*
  22. // @match https://*.east-plus.net/*
  23. // --------------------------------------------
  24. // @match https://spring-plus.net/*
  25. // @match https://summer-plus.net/*
  26. // @match https://soul-plus.net/*
  27. // @match https://south-plus.net/*
  28. // @match https://north-plus.net/*
  29. // @match https://snow-plus.net/*
  30. // @match https://level-plus.net/*
  31. // @match https://white-plus.net/*
  32. // @match https://imoutolove.me/*
  33. // @match https://south-plus.org/*
  34. // @match https://east-plus.net/*
  35. // --------------------------------------------
  36. // @grant GM_getValue
  37. // @grant GM_setValue
  38. // @grant GM_listValues
  39. // @grant GM_addValueChangeListener
  40. // @grant GM_removeValueChangeListener
  41. // @grant GM_notification
  42. // @grant GM_deleteValue
  43. // @grant GM_addStyle
  44. // @grant GM_getResourceText
  45. // @grant unsafeWindow
  46. // --------------------------------------------
  47. // @require https://cdn.jsdelivr.net/npm/toastify-js@1.11.2/src/toastify.min.js
  48. // @resource TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js@1.11.2/src/toastify.css
  49. // ----------------COPY END---------------------
  50. // @license GPL-3.0 License
  51. // ==/UserScript==
  52.  
  53. 'use strict';
  54.  
  55. const PageType = Object.freeze({
  56. THREADS_PAGE: Symbol("普通主题列表"),
  57. PIC_WALL_PAGE: Symbol("图墙区主题列表"),
  58. POSTS_PAGE: Symbol("帖子列表"),
  59. SEARCH_RESULT: Symbol("搜索结果")
  60. });
  61.  
  62. const ToastType = Object.freeze({
  63. INFO: Symbol("信息"),
  64. SUCCESS: Symbol("成功"),
  65. DANGER: Symbol("危险,失败"),
  66. WARNING: Symbol("警告")
  67. });
  68.  
  69. const FETCH_CONFIG = {
  70. credentials: 'include',
  71. mode: "no-cors"
  72. };
  73.  
  74. function getElementByXpath(from, xpath) {
  75. return from.evaluate(xpath, from, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  76. }
  77.  
  78. function waitForImageToLoad(imageElement) {
  79. return new Promise(resolve => {
  80. imageElement.onload = resolve
  81. })
  82. }
  83.  
  84. function toast(info, toastType, time = 3000, close = true) {
  85. let t;
  86. switch (toastType) {
  87. case ToastType.INFO:
  88. t = "linear-gradient(109deg, #3da1e0, #004dc1)";
  89. break;
  90. case ToastType.SUCCESS:
  91. t = "linear-gradient(213deg, #5daa16, #05bb1b)";
  92. break;
  93. case ToastType.DANGER:
  94. t = "linear-gradient(18deg, #cb3131, #ac1415)";
  95. break;
  96. case ToastType.WARNING:
  97. t = "linear-gradient(180deg, #e98202, #fe5e00)";
  98. break;
  99. default:
  100. t = "linear-gradient(109deg, #3da1e0, #004dc1)";
  101. }
  102. Toastify({
  103. text: info,
  104. duration: time,
  105. close: close,
  106. gravity: "bottom", // `top` or `bottom`
  107. position: "right", // `left`, `center` or `right`
  108. stopOnFocus: true, // Prevents dismissing of toast on hover
  109. style: {
  110. background: t,
  111. },
  112. onClick: function () {
  113. } // Callback after click
  114. }).showToast();
  115.  
  116. }
  117.  
  118. function getTimeStamp() {
  119. return new Date().getTime();
  120. }
  121.  
  122. function sleep(ms) {
  123. return new Promise(resolve => setTimeout(resolve, ms))
  124. }
  125.  
  126.  
  127. function getRandomInt(min, max) {
  128. min = Math.ceil(min);
  129. max = Math.floor(max);
  130. return Math.floor(Math.random() * (max - min + 1)) + min;
  131. }
  132.  
  133. async function fetchRetry(url, options, n = 1) {
  134. try {
  135. return await fetch(url, options)
  136. } catch (err) {
  137. if (n <= 1) throw err;
  138. return await fetchRetry(url, options, n - 1);
  139. }
  140. }
  141.  
  142. async function getPage(url, dummy = false, retry = 3, toastPop = false) {
  143. return await fetchRetry(url, FETCH_CONFIG, retry)
  144. .then(resp => resp.text())
  145. .then(html => {
  146. if (dummy) {
  147. let dummy = document.createElement("html");
  148. dummy.insertAdjacentHTML('afterbegin', html);
  149. return dummy
  150. } else {
  151. return html
  152. }
  153. }).catch(e => {
  154. if (toastPop) {
  155. toast(`访问 ${url} 失败\n${e}`, ToastType.WARNING);
  156. } else {
  157. console.error(`访问 ${url} 失败\n${e}`);
  158. }
  159. }
  160. );
  161. }
  162.  
  163. class GMK {
  164.  
  165. static addStyle(css) {
  166. return GM_addStyle(css);
  167. }
  168.  
  169. static setValue(key, value) {
  170. return GM_setValue(key, value)
  171. }
  172.  
  173. static getValue(key) {
  174. return GM_getValue(key)
  175. }
  176.  
  177. static getResourceText(key) {
  178. return GM_getResourceText(key);
  179. }
  180.  
  181. static listValues() {
  182. return GM_listValues();
  183. }
  184.  
  185. static deleteValue(_name) {
  186. return GM_deleteValue(_name);
  187. }
  188.  
  189. // listener_id = GM_addValueChangeListener(name, function(name, old_value, new_value, remote) {})
  190. static addValueChangeListener(_name, callback) {
  191. return GM_addValueChangeListener(_name, callback);
  192. }
  193.  
  194. static removeValueChangeListener(listener_id) {
  195. return GM_removeValueChangeListener(listener_id);
  196. }
  197. }
  198.  
  199. class MppManager {
  200. static TASK_KEY = "Soul++:MppThreadsStatus";
  201.  
  202. constructor() {
  203. }
  204.  
  205. static isThreadExist(_tid) {
  206. return this.getMarkList().hasOwnProperty(_tid);
  207. }
  208.  
  209. static getMarkList() {
  210. return GMK.getValue(this.TASK_KEY) || {}
  211. }
  212.  
  213. static addMarkThread(_tid) {
  214. GMK.setValue(this.TASK_KEY, { ...this.getMarkList(), ...{ [_tid]: {} } })
  215. }
  216.  
  217. static deleteMarkedThread(_tid) {
  218. let markList = this.getMarkList();
  219. delete markList[_tid];
  220. GMK.setValue(this.TASK_KEY, markList)
  221. }
  222.  
  223. static isMarked(_tid) {
  224. return this.getMarkList().hasOwnProperty(_tid)
  225. }
  226.  
  227. static getAllThreadStatus() {
  228. return this.getMarkList();
  229. }
  230.  
  231. static setThreadStatus(_tid, threadStatus) {
  232. GMK.setValue(this.TASK_KEY, {
  233. ...this.getMarkList(),
  234. [_tid]: threadStatus
  235. });
  236. }
  237.  
  238. static getLastFetchTime(_tid) {
  239. let res = Object.entries(GMK.getValue(this.TASK_KEY)).filter(e => e[0] === _tid);
  240. return res[0][1]["lastFetchTime"];
  241. };
  242.  
  243. static isAllChecked() {
  244. let res = Object.entries(GMK.getValue(this.TASK_KEY)).filter(e => !e[1]["allPagesChecked"]);
  245. return res.length === 0;
  246. };
  247. }
  248.  
  249. //##############################################################
  250. // 功能
  251. //##############################################################
  252.  
  253. function buyRefresh_free(target = document) {
  254. let buyButtons = target.querySelectorAll(".quote.jumbotron>.btn.btn-danger")
  255. buyButtons.forEach(button => {
  256. // 获取GET购买地址
  257. const urlRegex = /location\.href='(.+)'/
  258. const buyUrl = button.getAttribute("onclick");
  259. if (buyUrl === null) return;
  260. let m = buyUrl.match(urlRegex);
  261. if (m === null) return;
  262. let url = m[1];
  263. // 避免点击按钮的时候跳转,删掉这个属性
  264. button.setAttribute("onclick", "null");
  265. // 拿到帖子ID
  266. let postContainer = button.closest(".tpc_content .f14")
  267. let post_id = postContainer.getAttribute("id");
  268. // 添加点击事件,用fetch发送请求,然后读取页面再直接修改当前页面
  269. let customPurchase = (e => {
  270. e.stopPropagation();
  271. let btn = e.target;
  272. btn.setAttribute("value", "正在购买……请稍等………");
  273. try {
  274. fetch(url, FETCH_CONFIG)
  275. .then(resp => resp.text())
  276. .then(text => {
  277. if (!text.includes("操作完成")) {
  278. toast("购买失败!", ToastType.DANGER);
  279. return;
  280. }
  281. let threadID = postContainer.getAttribute("tid");
  282. let pg = postContainer.getAttribute("page");
  283. let resultURL = `./read.php?tid=${threadID}&page=${pg}`;
  284. fetch(resultURL, FETCH_CONFIG).then(resp => resp.text())
  285. .then(html => {
  286. let dummy = document.createElement("html");
  287. dummy.innerHTML = html;
  288. if (GMK.getValue("hidePostImage")) {
  289. hidePostImage(dummy);
  290. }
  291. let purchased = dummy.querySelector("#" + post_id);
  292. let notPurchased = document.querySelector("#" + post_id);
  293. notPurchased.parentNode.replaceChild(purchased, notPurchased);
  294.  
  295. });
  296.  
  297. btn.style.display = "none";
  298.  
  299. });
  300.  
  301. } catch (error) {
  302. toast(`发送请求出错,购买失败!\n${error}`, ToastType.DANGER);
  303. console.log('Request Failed', error);
  304. }
  305. })
  306.  
  307. button.addEventListener("click", customPurchase);
  308. });
  309. }
  310.  
  311. function hideImg(img) {
  312. // 避免折叠论坛表情
  313. const emojiPathReg = /images\/post\/smile\//;
  314. if (img.getAttribute("src").match(emojiPathReg)) {
  315. return
  316. }
  317. // 避免折叠论坛自带的文件图标
  318. const fileIconPathReg = /images\/colorImagination\/file\//;
  319. if (img.getAttribute("src").match(fileIconPathReg)) {
  320. return
  321. }
  322.  
  323. // 避免重复处理
  324. let p = img.parentNode;
  325. if (p.getAttribute("class") === "spp-img-mask") return;
  326.  
  327. // 如果开启了按需加载
  328. if (GMK.getValue("loadImageOnDemand")) {
  329. img.dataset.src = img.getAttribute("src");
  330. img.setAttribute("src", "")
  331. }
  332.  
  333. // 如果图片的父元素是A标签,去掉它
  334. if (img.parentNode.tagName === "A") img.parentNode.replaceWith(img);
  335. // 创建包裹元素
  336. let wrapper = document.createElement('div');
  337. wrapper.setAttribute("class", "spp-img-mask");
  338. wrapper.style.display = "grid";
  339. wrapper.style.gridTemplateRows = "auto auto";
  340. wrapper.style.justifyItems = "center";
  341.  
  342. // 将父元素下的图片元素替换成包裹元素
  343. img.parentNode.replaceChild(wrapper, img);
  344.  
  345. // 将图片元素当成子元素放入包裹元素
  346. wrapper.appendChild(img);
  347.  
  348. img.style.width = "100%";
  349.  
  350. // 添加类名
  351. img.setAttribute("class", "spp-thread-imgs spp-hide");
  352.  
  353. // 包裹元素样式
  354. wrapper.style.borderStyle = "dashed";
  355. wrapper.style.width = "auto";
  356. wrapper.style.height = "20";
  357. wrapper.style.textAlign = "center";
  358. wrapper.style.verticalAlign = "center";
  359. wrapper.style.cursor = "pointer";
  360.  
  361. // 创建遮罩小人儿表情
  362. let icon_hide = document.createElement("img");
  363. icon_hide.setAttribute("src", "images/post/smile/smallface/face106.gif");
  364.  
  365. let icon_show = document.createElement("img");
  366. icon_show.setAttribute("src", "images/post/smile/smallface/face109.gif");
  367.  
  368.  
  369. // 创建遮罩文本
  370. let tip = document.createElement("span");
  371. let tip_text = document.createElement("span");
  372. tip_text.innerText = "看看是啥";
  373.  
  374. // 凑一堆儿来
  375. tip.appendChild(icon_hide);
  376. tip.appendChild(icon_show);
  377. tip.appendChild(tip_text);
  378.  
  379. // 添加类名
  380. icon_hide.setAttribute("class", "spp-img-mask-icon-hide");
  381. icon_show.setAttribute("class", "spp-img-mask-icon-show spp-hide");
  382. tip.setAttribute("class", "ssp-img-mask-text");
  383.  
  384. // 插入元素
  385. wrapper.insertBefore(tip, img);
  386. // 防止点击图片打开新窗口
  387. document.querySelector(".spp-thread-imgs").addEventListener("click", e => e.preventDefault());
  388. // 事件监听
  389. wrapper.addEventListener("click", (e) => {
  390. e.stopPropagation();
  391. // console.log(e.target);
  392. // console.log(e.currentTarget);
  393. let img = e.currentTarget.querySelector(".spp-thread-imgs");
  394. img.classList.toggle("spp-hide");
  395. // 按需加载
  396. if (GMK.getValue("loadImageOnDemand") && !img.classList.contains("spp-hide")) {
  397. let loading = document.createElement("div");
  398. loading.innerHTML = `<div class="spp-loading-animation">
  399. <div class="dot1"></div>
  400. <div class="dot2"></div>
  401. <div class="dot3"></div>
  402. </div>`;
  403. loading = loading.firstChild;
  404. img.parentNode.append(loading);
  405. img.setAttribute("src", img.dataset.src);
  406. waitForImageToLoad(img).then(() => {
  407. loading.parentNode.removeChild(loading);
  408. });
  409. }
  410. e.currentTarget.querySelector(".spp-img-mask-icon-hide").classList.toggle("spp-hide");
  411. e.currentTarget.querySelector(".spp-img-mask-icon-show").classList.toggle("spp-hide");
  412. });
  413.  
  414. }
  415.  
  416. function hideAvatar(avatar) {
  417. let src = avatar.getAttribute("src");
  418. if (src === "images/face/none.gif") return;
  419.  
  420. // 如果开启了按需加载
  421. if (GMK.getValue("loadImageOnDemand")) {
  422. avatar.dataset.src = avatar.getAttribute("src");
  423. avatar.setAttribute("src", "")
  424. }
  425.  
  426. // 创建包裹元素
  427. let wrapper = document.createElement('div');
  428. wrapper.setAttribute("class", "spp-avatar-mask");
  429. wrapper.style.minWidth = "162px";
  430. wrapper.style.minHeight = "162px";
  431. wrapper.style.display = "grid";
  432. wrapper.style.justifyItems = "center";
  433. wrapper.style.alignItems = "center";
  434.  
  435. // 创建一个假头像
  436. let fakeAvatarElement = document.createElement("img");
  437. fakeAvatarElement.setAttribute("src", "images/face/none.gif");
  438. fakeAvatarElement.style.borderStyle = "dashed";
  439. fakeAvatarElement.style.borderRadius = "3";
  440. fakeAvatarElement.style.borderWidth = "3px";
  441. fakeAvatarElement.style.borderColor = "Orange";
  442.  
  443. // 替换包裹元素
  444. avatar.parentNode.replaceChild(wrapper, avatar);
  445.  
  446. // 将假头像和真头像插到包裹元素中
  447. wrapper.appendChild(avatar);
  448. wrapper.appendChild(fakeAvatarElement);
  449.  
  450. // 隐藏真头像
  451. avatar.classList.add("spp-hide");
  452.  
  453. // 设置类名
  454. avatar.classList.add("spp-avatar-real");
  455. fakeAvatarElement.classList.add("spp-avatar-fake");
  456.  
  457. // 事件监听
  458. wrapper.addEventListener("mouseenter", (e) => {
  459. e.stopPropagation();
  460. e.currentTarget.querySelector(".spp-avatar-fake").classList.add("spp-hide");
  461. e.currentTarget.querySelector(".spp-avatar-real").classList.remove("spp-hide");
  462. // 按需加载
  463. if (GMK.getValue("loadImageOnDemand") && !avatar.classList.contains("spp-hide")) {
  464. let loading = document.createElement("div");
  465. loading.innerHTML = `<div class="spp-loading-animation">
  466. <div class="dot1"></div>
  467. <div class="dot2"></div>
  468. <div class="dot3"></div>
  469. </div>`;
  470. loading = loading.firstChild;
  471. e.currentTarget.append(loading);
  472. avatar.setAttribute("src", avatar.dataset.src);
  473. waitForImageToLoad(avatar).then(() => {
  474. loading.parentNode.removeChild(loading);
  475. });
  476. }
  477. });
  478.  
  479. wrapper.addEventListener("mouseleave", (e) => {
  480. e.stopPropagation();
  481. e.currentTarget.querySelector(".spp-avatar-fake").classList.remove("spp-hide");
  482. e.currentTarget.querySelector(".spp-avatar-real").classList.add("spp-hide");
  483. e.currentTarget.querySelectorAll(".spp-loading-animation").forEach(ele => ele.parentNode.removeChild(ele));
  484. });
  485.  
  486.  
  487. }
  488.  
  489. function hidePostImage(target = document) {
  490. let thread_user_post_images = target.querySelectorAll(".t5.t2 .r_one img");
  491.  
  492. thread_user_post_images.forEach(hideImg);
  493. }
  494.  
  495. function hideUserAvatar(target = document) {
  496. let user_avatars = target.querySelectorAll(".user-pic img");
  497. user_avatars.forEach(hideAvatar);
  498. }
  499.  
  500. function dynamicLoadingNextPage(pageType) {
  501.  
  502. class NextPageLoader {
  503.  
  504. constructor() {
  505. this.isFetching = false;
  506. this.nextPageDummy = null;
  507. }
  508.  
  509. GetURLDummy(url) {
  510. this.nextPageDummy = document.createElement("html");
  511. this.isFetching = true;
  512. return fetch(url, FETCH_CONFIG)
  513. .then(response => response.text())
  514.  
  515. }
  516.  
  517. AppendNextPageItems(itemSelector, divider) {
  518.  
  519. let postsFragment = document.createDocumentFragment();
  520. this.nextPageDummy.querySelectorAll(itemSelector).forEach(ele => postsFragment.appendChild(ele));
  521. // 追加下一页的所有子项追加到分割线下面
  522. divider.parentNode.appendChild(postsFragment);
  523. }
  524.  
  525. UpdatePageList() {
  526. // 主动更新帖子列表上下方的当前页码数
  527. let pagesOld = document.querySelectorAll(".pages");
  528. let pagesNew = this.nextPageDummy.querySelectorAll(".pages");
  529. for (let i = 0; i < pagesOld.length; i++) {
  530. pagesOld[i].parentNode.replaceChild(pagesNew[i], pagesOld[i]);
  531. }
  532. }
  533.  
  534. }
  535.  
  536.  
  537. function getNextPageUrl() {
  538. let pageSeq = document.querySelector(".pages b");
  539. if (!pageSeq) return null;
  540. let pageNum = pageSeq.parentNode;
  541. let url = pageNum.nextSibling.firstChild.getAttribute("href");
  542. if (pageNum.nextSibling.nextSibling.classList.contains("pagesone")) return null;
  543. if (document.URL.includes(url)) return null;
  544. return url;
  545. }
  546.  
  547.  
  548. function makeDivider(itemsSelector, dividerMaker) {
  549. let divider = dividerMaker();
  550. let allItem = document.querySelectorAll(itemsSelector);
  551. let lastItem = allItem[allItem.length - 1];
  552. lastItem.parentNode.appendChild(divider);
  553. return divider;
  554. }
  555.  
  556. let nextPageLoader;
  557. let nextPageURL;
  558. nextPageLoader = nextPageLoader || new NextPageLoader()
  559. // 处理搜索结果页面
  560. if (pageType === PageType.SEARCH_RESULT) {
  561. document.addEventListener('wheel', (e) => {
  562. e.stopPropagation();
  563. const itemListSelector = ".tr3.tac";
  564. if (e.deltaY < 0 || nextPageLoader.isFetching) return;
  565. if (!nextPageLoader.nextPageDummy) {
  566. nextPageURL = getNextPageUrl();
  567. if (!nextPageURL) return;
  568. let divider = makeDivider(itemListSelector, () => {
  569. let divider = document.createElement("tr");
  570. let dividerContent = document.createElement("td");
  571. divider.setAttribute("class", "tr2 spp-next-page-loader-divider")
  572. divider.appendChild(dividerContent);
  573. dividerContent.colSpan = 7;
  574. dividerContent.style.textAlign = "center";
  575. dividerContent.style.fontWeight = "bold";
  576. dividerContent.innerText = "...";
  577. return divider;
  578. });
  579. divider.firstChild.innerText = "正在获取下一页的帖子......";
  580. let p = nextPageLoader.GetURLDummy(nextPageURL);
  581. p
  582. .then(html => {
  583. nextPageLoader.nextPageDummy.innerHTML = html
  584. if (GMK.getValue("blockAdforumSearchResult")) blockAdforumSearchResult(nextPageLoader.nextPageDummy);
  585. })
  586. .catch(err => {
  587. console.error(err);
  588. divider.firstChild.innerText = "获取下一页的帖子出错,请手动刷新";
  589. })
  590. .finally(() => {
  591. nextPageLoader.isFetching = false;
  592. divider.firstChild.innerText = "滚动条到底后继续向下滚动将会加载下一页的帖子";
  593. });
  594.  
  595. }
  596. // 否则判断一下是否到底了,到底了就追加下一页的内容
  597. else if (Math.abs(document.documentElement.scrollHeight - (window.pageYOffset + window.innerHeight)) < 20) {
  598. let divider = getElementByXpath(document, "//tr[@class='tr2 spp-next-page-loader-divider'][last()]");
  599. nextPageLoader.AppendNextPageItems(itemListSelector, divider);
  600. nextPageLoader.UpdatePageList();
  601.  
  602. divider.firstChild.innerText = `以下是第${nextPageURL.match(/page-(\d+)/)[1]}页`;
  603. window.history.pushState({}, 0, nextPageURL); // 将地址栏也改变了
  604. nextPageLoader.nextPageDummy = null;
  605. }
  606.  
  607. })
  608. }
  609. // 处理主题列表页面
  610. if (pageType === PageType.THREADS_PAGE) {
  611. document.addEventListener('wheel', (e) => {
  612. e.stopPropagation();
  613. const itemListSelector = ".tr3.t_one";
  614. if (e.deltaY < 0 || nextPageLoader.isFetching) return;
  615. if (!nextPageLoader.nextPageDummy) {
  616. nextPageURL = getNextPageUrl();
  617. if (!nextPageURL) return;
  618. let divider = makeDivider(itemListSelector, () => {
  619. let divider = document.createElement("tr");
  620. let dividerContent = document.createElement("td");
  621. divider.setAttribute("class", "tr2 spp-next-page-loader-divider")
  622. divider.appendChild(dividerContent);
  623. dividerContent.colSpan = 5;
  624. dividerContent.style.textAlign = "center";
  625. dividerContent.style.fontWeight = "bold";
  626. dividerContent.innerText = "...";
  627. return divider;
  628. });
  629. divider.firstChild.innerText = "正在获取下一页的帖子......";
  630. let p = nextPageLoader.GetURLDummy(nextPageURL);
  631. p
  632. .then(html => {
  633. nextPageLoader.nextPageDummy.innerHTML = html;
  634. threadAddAnchorAttribute(nextPageLoader.nextPageDummy, page + 1, fid);
  635. if (GMK.getValue("highlightViewedThread")) highlightViewedThread(nextPageLoader.nextPageDummy);
  636. })
  637. .catch(err => {
  638. console.error(err);
  639. divider.firstChild.innerText = "获取下一页的帖子出错,请手动刷新";
  640. })
  641. .finally(() => {
  642. nextPageLoader.isFetching = false;
  643. divider.firstChild.innerText = "滚动条到底后继续向下滚动将会加载下一页的帖子";
  644. });
  645.  
  646. }
  647. // 否则判断一下是否到底了,到底了就追加下一页的内容
  648. else if (Math.abs(document.documentElement.scrollHeight - (window.pageYOffset + window.innerHeight)) < 20) {
  649. let divider = getElementByXpath(document, "//tr[@class='tr2 spp-next-page-loader-divider'][last()]");
  650. nextPageLoader.AppendNextPageItems(itemListSelector, divider);
  651. nextPageLoader.UpdatePageList();
  652. divider.firstChild.innerText = `以下是第${page + 1}页`;
  653. window.history.pushState({}, 0, nextPageURL); // 将地址栏也改变了
  654. page += 1;
  655. nextPageLoader.nextPageDummy = null;
  656. }
  657.  
  658. })
  659. }
  660. // 处理楼层列表页面
  661. if (pageType === PageType.POSTS_PAGE) {
  662. document.addEventListener('wheel', (e) => {
  663. e.stopPropagation();
  664. const itemListSelector = ".t5.t2";
  665. if (e.deltaY < 0 || nextPageLoader.isFetching) return;
  666. if (!nextPageLoader.nextPageDummy) {
  667. nextPageURL = getNextPageUrl();
  668. if (!nextPageURL) return;
  669. let divider = makeDivider(itemListSelector, () => {
  670. let divider = document.createElement("div");
  671. let dividerContent = document.createElement("span");
  672. divider.setAttribute("class", "t5 t2 spp-next-page-loader-divider")
  673. divider.appendChild(dividerContent);
  674. divider.style.textAlign = "center";
  675. divider.style.fontWeight = "bold";
  676. divider.style.fontSize = "14px";
  677. divider.innerText = "...";
  678. return divider;
  679. });
  680. divider.innerText = "加载中..";
  681. nextPageLoader.GetURLDummy(nextPageURL)
  682. .then(html => {
  683. nextPageLoader.nextPageDummy.innerHTML = html
  684. postAddAnchorAttribute(nextPageLoader.nextPageDummy, page + 1, tid);
  685. if (GMK.getValue("buyRefresh_free")) buyRefresh_free(nextPageLoader.nextPageDummy);
  686. if (GMK.getValue("hidePostImage")) hidePostImage(nextPageLoader.nextPageDummy);
  687. if (GMK.getValue("hideUserAvatar")) hideUserAvatar(nextPageLoader.nextPageDummy);
  688. if (GMK.getValue("hoistingResourcePost")) hoistingResourcePost(nextPageLoader.nextPageDummy);
  689. })
  690. .catch(err => {
  691. console.error(err);
  692. divider.innerText = "获取下一页的帖子出错,请手动刷新";
  693. })
  694. .finally(() => {
  695. nextPageLoader.isFetching = false;
  696. divider.innerText = "滚动条到底后继续向下滚动将会加载下一页的帖子";
  697. });
  698.  
  699. }
  700. // 否则判断一下是否到底了,到底了就追加下一页的内容
  701. else if (Math.abs(document.documentElement.scrollHeight - (window.pageYOffset + window.innerHeight)) < 20) {
  702. let divider = getElementByXpath(document, "//div[@class='t5 t2 spp-next-page-loader-divider'][last()]");
  703. nextPageLoader.AppendNextPageItems(itemListSelector, divider);
  704. nextPageLoader.UpdatePageList();
  705. divider.innerText = `以下是第${page + 1}页`;
  706. // window.history.pushState({}, 0, nextPageURL); // 将地址栏也改变了
  707. page += 1;
  708. nextPageLoader.nextPageDummy = null;
  709. }
  710.  
  711. })
  712. }
  713. // 处理图墙区主题列表页面
  714. if (pageType === PageType.PIC_WALL_PAGE) {
  715. document.addEventListener('wheel', (e) => {
  716. e.stopPropagation();
  717. const itemListSelector = ".dcsns-li.dcsns-rss.dcsns-feed-0";
  718. if (e.deltaY < 0 || nextPageLoader.isFetching) return;
  719. if (!nextPageLoader.nextPageDummy) {
  720. nextPageURL = getNextPageUrl();
  721. if (!nextPageURL) return;
  722. let divider = makeDivider(itemListSelector, () => {
  723. let divider = document.createElement("tr");
  724. let dividerContent = document.createElement("td");
  725. divider.setAttribute("class", "tr2 spp-next-page-loader-divider")
  726. divider.appendChild(dividerContent);
  727. dividerContent.colSpan = 5;
  728. dividerContent.style.textAlign = "center";
  729. dividerContent.style.fontWeight = "bold";
  730. dividerContent.innerText = "...";
  731. return divider;
  732. });
  733. divider.firstChild.innerText = "正在获取下一页的帖子......";
  734. let p = nextPageLoader.GetURLDummy(nextPageURL);
  735. p
  736. .then(html => {
  737. nextPageLoader.nextPageDummy.innerHTML = html;
  738. nextPageLoader.nextPageDummy.querySelectorAll(".dcsns-li.dcsns-rss.dcsns-feed-0 .lazy").forEach(ele => {
  739. ele.setAttribute("loading", "lazy");
  740. ele.setAttribute("class", "");
  741. ele.setAttribute("src", ele.getAttribute("data-original"));
  742. ele.setAttribute("data-original", "");
  743. ele.style.display = "inline";
  744. });
  745.  
  746. })
  747. .catch(err => {
  748. console.error(err);
  749. divider.firstChild.innerText = "获取下一页的帖子出错,请手动刷新";
  750. })
  751. .finally(() => {
  752. nextPageLoader.isFetching = false;
  753. divider.firstChild.innerText = "滚动条到底后继续向下滚动将会加载下一页的帖子";
  754. });
  755.  
  756. }
  757. // 否则判断一下是否到底了,到底了就追加下一页的内容
  758. else if (Math.abs(document.documentElement.scrollHeight - (window.pageYOffset + window.innerHeight)) < 20) {
  759. let divider = getElementByXpath(document, "//tr[@class='tr2 spp-next-page-loader-divider'][last()]");
  760. nextPageLoader.AppendNextPageItems(itemListSelector, divider);
  761. nextPageLoader.UpdatePageList();
  762. divider.firstChild.innerText = `以下是第${page + 1}页`;
  763. window.history.pushState({}, 0, nextPageURL); // 将地址栏也改变了
  764. page += 1;
  765. nextPageLoader.nextPageDummy = null;
  766. }
  767.  
  768. })
  769. }
  770. }
  771.  
  772. async function automaticTaskCollection() {
  773.  
  774. function setUIDsValue(uid, value) {
  775. let tmp = GMK.getValue("LastAutomaticTaskCollectionDate") || {};
  776. tmp[uid] = value;
  777. GMK.setValue("LastAutomaticTaskCollectionDate", tmp);
  778. }
  779.  
  780. if (document.querySelector("#login_0")) {
  781. console.log(`尚未登录,不接任务`);
  782. return
  783. }
  784.  
  785. let uid = document.querySelector("#menu_profile .ul2").innerHTML.match(/u\.php\?action-show-uid-(\d+)\.html/)[1];
  786. let uname = document.querySelector("#user-login a").innerText;
  787.  
  788. console.log(GMK.getValue("LastAutomaticTaskCollectionDate"));
  789. let lastTime = GMK.getValue("LastAutomaticTaskCollectionDate") ?
  790. (parseInt(GMK.getValue("LastAutomaticTaskCollectionDate")[uid]) || 0) : 0;
  791. console.log(`${uname}[${uid}] 上次:${new Date(lastTime).toLocaleDateString()} ${new Date(lastTime).toLocaleTimeString()}`);
  792.  
  793.  
  794. if (new Date().getTime() - lastTime < (3600 * 1000)) {
  795. console.log("再等等……");
  796. return;
  797. }
  798.  
  799.  
  800. async function forumTask(pageURL, selector, jobType) {
  801. let dummy = await fetch(
  802. pageURL,
  803. FETCH_CONFIG)
  804. .then(response => response.text())
  805. .then(html => {
  806. let dummy = document.createElement("html");
  807. dummy.innerHTML = html;
  808. return dummy
  809. })
  810. .catch(err => console.error(err));
  811.  
  812. async function t(task) {
  813. let job = task.getAttribute("onclick");
  814. let r = job.match(/startjob\('(\d+)'\);/);
  815. let jobID = r[1];
  816. let taskURL = `/plugin.php?H_name=tasks&action=ajax&actions=${jobType}&cid=${jobID}&nowtime=${new Date().getTime()}&verify=${verifyhash}`;
  817.  
  818.  
  819. await fetch(taskURL, FETCH_CONFIG)
  820. .then(response => response.text())
  821. .then(html => {
  822. console.log(html);
  823. if (html.includes("success\t")) toast(html.match(/!\[CDATA\[success\t(.+)]]>/)[1], ToastType.SUCCESS);
  824. }
  825. )
  826. .catch(err => console.error(err));
  827. }
  828.  
  829. await dummy.querySelectorAll(selector).forEach(t);
  830.  
  831. console.log(`${pageURL} done, ${new Date().getTime()}`)
  832. }
  833.  
  834. for (let i = 0; i < 2; i++) {
  835. forumTask(
  836. "/plugin.php?H_name-tasks.html",
  837. "a[title=按这申请此任务]",
  838. "job"
  839. ).catch(err => console.error(err));
  840. await sleep(3000);
  841. forumTask(
  842. "/plugin.php?H_name-tasks-actions-newtasks.html.html",
  843. "a[title=领取此奖励]",
  844. "job2"
  845. ).catch(err => console.error(err));
  846. }
  847. console.log(`${uname}[${uid}], 本次领取时间:${new Date().getTime()}`);
  848. setUIDsValue(uid, new Date().getTime());
  849.  
  850. }
  851.  
  852. function blockAdforumSearchResult(target = document) {
  853. target.querySelectorAll(".tr3.tac").forEach(ele => {
  854. let forum = ele.childNodes[2];
  855. if (forum.firstChild.getAttribute("href").match(/fid-17[1-4]/)) {
  856. ele.style.display = "none";
  857. }
  858. });
  859. }
  860.  
  861. function createFloatDraggableButton(text, GMKey, style) {
  862. let btn = document.createElement("button");
  863. let main = document.getElementById("main");
  864. main.appendChild(btn);
  865.  
  866. btn.innerText = text;
  867. btn.setAttribute("id", "spp-float-draggable-button");
  868. btn.setAttribute("draggable", "true");
  869. btn.style.display = "block";
  870. btn.style.position = "fixed";
  871. btn.style.background = "#efefef";
  872. btn.style.zIndex = "99";
  873. btn.style.width = "30px";
  874. btn.style.padding = "10";
  875. btn.style.borderRadius = "1px";
  876.  
  877. if (style) {
  878. for (const k in style) {
  879. btn.style[k] = style[k];
  880. }
  881. }
  882.  
  883. let GM_style;
  884. if (GMKey) GM_style = GMK.getValue(GMKey);
  885. if (GM_style) {
  886. for (const k in GM_style) {
  887. btn.style[k] = GM_style[k];
  888. }
  889. }
  890.  
  891. return btn;
  892.  
  893. }
  894.  
  895. function mark() {
  896. if (document.location.href.includes("/read.php")) {
  897.  
  898. const GREY = "linear-gradient(to top, rgb(184 184 184), rgb(188 188 188))";
  899. const BLACK = "linear-gradient(to top, #313131,#000000)";
  900. const GMKey = "Style_markPlusPlus";
  901.  
  902. let markButton = createFloatDraggableButton(
  903. MppManager.isMarked(tid) ? "MARKED" : "MARK",
  904. GMKey,
  905. {
  906. left: "calc(50vw + 470px)",
  907. top: "234px",
  908. background: MppManager.isMarked(tid) ? GREY : BLACK,
  909. color: "white",
  910. fontWeight: "bold",
  911. outline: "none",
  912. border: "none",
  913. borderRadius: "3px",
  914. width: "30px",
  915. opacity: MppManager.isMarked(tid) ? "0.4" : "0.8",
  916. cursor: "pointer",
  917. },
  918. );
  919.  
  920. let dragStart = {};
  921. let dragEnd = {};
  922. // 防止拖到视口以外了
  923. AddIntersectionObserver(([entry]) => {
  924. if (!entry.isIntersecting) {
  925. // console.log('LEAVE');
  926. markButton.style.left = dragStart['saved']['left'];
  927. markButton.style.top = dragStart['saved']['top'];
  928. }
  929. }, markButton)
  930.  
  931. markButton.addEventListener("click", async evt => {
  932. evt.stopPropagation();
  933.  
  934. if (MppManager.isMarked(tid)) {
  935. MppManager.deleteMarkedThread(tid);
  936. evt.target.style.background = BLACK;
  937. evt.target.innerText = "MARK";
  938. evt.target.style.opacity = "0.8";
  939. } else {
  940. MppManager.addMarkThread(tid);
  941. evt.target.style.background = GREY;
  942. evt.target.innerText = "MARKED";
  943. evt.target.style.opacity = "0.4";
  944. // 第一次mark就先把基本内容给收录了
  945. let threadStatus = {};
  946.  
  947. threadStatus['page'] = 1;
  948. threadStatus['lastFetchTime'] = 0;
  949. threadStatus['maxPage'] = totalpage;
  950. threadStatus['title'] = document.querySelector('.crumbs-item.current strong>a').textContent;
  951. threadStatus["markTime"] = new Date().toLocaleDateString();
  952. MppManager.setThreadStatus(tid, threadStatus);
  953. console.log(MppManager.getMarkList());
  954. }
  955.  
  956.  
  957. });
  958.  
  959. markButton.addEventListener("contextmenu", openStatus);
  960.  
  961. markButton.addEventListener("dragstart", (e) => {
  962. e.stopPropagation();
  963. dragStart = {
  964. clientX: e.clientX,
  965. clientY: e.clientY,
  966. saved: {
  967. left: e.target.style.left,
  968. top: e.target.style.top,
  969. }
  970. }
  971. });
  972.  
  973. markButton.addEventListener("dragend", (e) => {
  974. e.stopPropagation();
  975. // 获得丅的交叉点坐标
  976. let startX = window.innerWidth / 2;
  977. let startY = 0;
  978. dragEnd = {
  979. clientX: e.clientX,
  980. clientY: e.clientY,
  981. }
  982. let newLeft = parseFloat(e.target.style.left.match(/(-?\d+)px/)[1]) + (dragEnd.clientX - dragStart.clientX)
  983. let newTop = parseFloat(e.target.style.top.match(/(-?\d+)px/)[1]) + (dragEnd.clientY - dragStart.clientY);
  984.  
  985.  
  986. e.target.style.left = `calc(50vw + ${newLeft}px)`;
  987. e.target.style.top = `${newTop}px`;
  988.  
  989. let tmp = {
  990. ...GMK.getValue(GMKey),
  991. ...{
  992. left: `calc(50vw + ${newLeft}px)`,
  993. top: `${newTop}px`,
  994. }
  995. };
  996. GMK.setValue(GMKey, tmp);
  997. });
  998. }
  999. let menuButton = document.createElement("li");
  1000. let a = document.createElement("a");
  1001. a.innerText = "我的MARK";
  1002. a.style.cursor = "pointer";
  1003. a.classList.add("mpp-status");
  1004. menuButton.appendChild(a);
  1005. document.querySelector("#main").insertAdjacentHTML("afterbegin", `
  1006. <style>
  1007. .mpp{
  1008. position: fixed;
  1009. left: 50%;
  1010. top: 50%;
  1011. transform: translate(-50%, -50%);
  1012. background: white;
  1013. z-index: 2000000;
  1014. }
  1015. .mpp-mask{
  1016. position: fixed;
  1017. left: 0;
  1018. top: 0;
  1019. width: 100%;
  1020. height: 100%;
  1021. background: black;
  1022. opacity: 0.5;
  1023. user-select: none;
  1024. z-index: 1000000;
  1025. }
  1026. .mpp-container{
  1027. background: #eeeeee;
  1028. display:grid;
  1029. grid-template-areas:
  1030. "title"
  1031. "main";
  1032. grid-template-rows: 28px auto;
  1033. grid-gap: 10px;
  1034. min-height: 80vh;
  1035. max-height: 80vh;
  1036. width: 900px;
  1037. overflow-y: hidden;
  1038. }
  1039. .mpp-title{
  1040. grid-area: title;
  1041. background: #111111;
  1042. border-left: black;
  1043. border-right: black;
  1044. text-align: center;
  1045. font-weight: bold;
  1046. height: 100%;
  1047. color: white;
  1048. padding-top: 5px;
  1049. margin: 0;
  1050. }
  1051.  
  1052. .mpp-main{
  1053. grid-area: main;
  1054. height: 75vh;
  1055. overflow-y: scroll;
  1056. overflow-x: hidden;
  1057.  
  1058. }
  1059.  
  1060.  
  1061. .spp-hide{
  1062. display: none;
  1063. }
  1064.  
  1065. .mpp-accordion{
  1066. width: 100%;
  1067. border: none;
  1068. outline: none;
  1069. background-color: whitesmoke;
  1070. text-align: left;
  1071. padding: 10px 10px;
  1072. font-size: 12px;
  1073. /*font-weight: bold;*/
  1074. color: #444;
  1075. cursor: pointer;
  1076. transition: background-color 0.2s linear;
  1077. display: inline-grid;
  1078. grid-template-columns:1fr 4fr 1fr 1fr 1fr 1fr 1fr;
  1079. align-items: center;
  1080. justify-items: center;
  1081. margin-bottom: 4px;
  1082. box-shadow: 1px 2px 2px #AAAAAA;
  1083. }
  1084. .mpp-header{
  1085. width: 100%;
  1086. display: inline-grid;
  1087. grid-template-columns:1fr 4fr 1fr 1fr 1fr 1fr 1fr;
  1088. padding: 10px 10px;
  1089. font-size: 12px;
  1090. align-items: center;
  1091. justify-items: left;
  1092. }
  1093.  
  1094. /*.mpp-accordion-plus{ */
  1095. /* font-size: 14px;*/
  1096. /* float: right;*/
  1097. /*}*/
  1098.  
  1099. button.mpp-accordion:before{
  1100. content: '无';
  1101. font-size: 10px;
  1102. color: gray;
  1103. }
  1104. button.mpp-accordion.have-content:before{
  1105. content: '+';
  1106. font-size: 14px;
  1107. font-weight: bold;
  1108. color: black;
  1109. }
  1110. button.mpp-accordion.have-content.mpp-accordion-is-open:before{
  1111. content: '-';
  1112. font-size: 14px;
  1113. font-weight: bold;
  1114. color: black;
  1115. }
  1116. button.mpp-accordion:hover, button.mpp-accordion.mpp-accordion-is-open{
  1117. background-color: #ddd;
  1118. }
  1119.  
  1120. .mpp-accordion-content{
  1121. background: #eeeeee;
  1122. border-left: 1px solid whitesmoke;
  1123. border-right: 1px solid whitesmoke;
  1124. padding: 0 20px;
  1125. margin-bottom: 1px;
  1126. max-height: 0;
  1127. overflow: hidden;
  1128. font-size: 10px;
  1129. }
  1130.  
  1131. .mpp-accordion-content.mpp-accordion-is-open{
  1132. max-height: fit-content;
  1133. }
  1134.  
  1135. .mpp-sticky{
  1136. position: sticky;
  1137. top: 0;
  1138. }
  1139.  
  1140. .mpp-accordion-op{
  1141. display: flex;
  1142.  
  1143. justify-content: end;
  1144. align-content: center;
  1145. }
  1146. .mpp-accordion-op a{
  1147. padding: 5px;
  1148. margin-left: 20px;
  1149. font-size: 12px;
  1150. cursor: pointer;
  1151. }
  1152. a.mpp-delete{
  1153. text-align: right;
  1154. color: brown;
  1155. }
  1156. a.mpp-sell{
  1157. color: blueviolet;
  1158. font-weight: bold;
  1159. }
  1160. a.mpp-hyperlink{
  1161. color: blue;
  1162. }
  1163. a.mpp-hash{
  1164. color: forestgreen;
  1165. }
  1166. .mpp-content-cell.mpp-content-title,
  1167. .mpp-content-cell.mpp-content-last-fetch-time{
  1168. justify-self: left;
  1169. }
  1170. .mpp-content-cell.mpp-content-last-fetch-time{
  1171. padding-left: 1em;
  1172. }
  1173. a.mpp-status{
  1174. /*font-weight: bold;*/
  1175. color: dodgerblue;
  1176. cursor:pointer;
  1177. }
  1178. span.mpp-content-result{
  1179. justify-self: center;
  1180. }
  1181.  
  1182. </style>
  1183. <div class="mpp-mask spp-hide"></div>
  1184. <div class="mpp spp-hide">
  1185. <div class="mpp-container">
  1186. <p class="mpp-title">我的Mark(保持此窗口开启才会运行)</p>
  1187. <div class="mpp-main">
  1188. <div class="mpp-accordion-op mpp-sticky">
  1189. <a class="mpp-accordion-expand-all">全部展开</a>
  1190. <a class="mpp-accordion-collapse-all">全部折叠</a>
  1191. </div>
  1192. <div class="mpp-header" >
  1193. <span class="mpp-header-cell mpp-content-result"></span>
  1194. <span class="mpp-header-cell mpp-content-title" >帖子标题</span>
  1195. <span class="mpp-header-cell">页数</span>
  1196. <span class="mpp-header-cell mpp-content-last-fetch-time">检查时间</span>
  1197. <span class="mpp-header-cell">悬赏状态</span>
  1198. <span class="mpp-header-cell">MARK时间</span>
  1199. <a class="mpp-delete mpp-content-cell" data-tid="1274464"></a>
  1200. </div>
  1201. <div class="mpp-content-container">
  1202. </div>
  1203. </div>
  1204. </div>
  1205. </div>
  1206. `);
  1207. document.querySelector("#guide").prepend(menuButton);
  1208. // document.querySelector('.fl>.gray2>.fl:first-child').insertAdjacentText("beforeend",
  1209. // `, `);
  1210. // document.querySelector('.fl>.gray2>.fl:first-child').insertAdjacentHTML("beforeend",
  1211. // `<a class="mpp-status">我的MARK</a>`);
  1212. document.querySelector(".mpp-accordion-expand-all").addEventListener("click", evt => {
  1213.  
  1214. document.querySelectorAll(".mpp-accordion").forEach(ele => {
  1215. if (!ele.nextElementSibling.querySelectorAll("p>a").length) return;
  1216. if (!ele.classList.contains(" mpp-accordion-is-open")) ele.classList.add("mpp-accordion-is-open");
  1217. });
  1218. document.querySelectorAll(".mpp-accordion-content").forEach(ele => {
  1219. if (!ele.querySelectorAll("p>a").length) return;
  1220. if (!ele.classList.contains("mpp-accordion-is-open")) ele.classList.add("mpp-accordion-is-open");
  1221. ele.style.maxHeight = ele.scrollHeight + 'px';
  1222. });
  1223. });
  1224. document.querySelector(".mpp-accordion-collapse-all").addEventListener("click", evt => {
  1225. document.querySelectorAll(".mpp-accordion").forEach(ele => {
  1226. if (ele.classList.contains("mpp-accordion-is-open")) ele.classList.remove("mpp-accordion-is-open")
  1227. });
  1228. document.querySelectorAll(".mpp-accordion-content").forEach(ele => {
  1229. if (ele.classList.contains("mpp-accordion-is-open")) ele.classList.remove("mpp-accordion-is-open")
  1230. ele.style.maxHeight = null;
  1231. });
  1232. });
  1233.  
  1234. // 用于tab之间广播通讯,只允许一个tab运行mark++
  1235. const bc = new BroadcastChannel("Soul++:MppTaskStart");
  1236.  
  1237.  
  1238. let refreshID;
  1239.  
  1240.  
  1241. // 自己不会接到
  1242. bc.onmessage = async msg => {
  1243. console.log('BroadcastChannel:', msg.data);
  1244. if (msg.data.includes("mppTaskStart")) {
  1245. closeMenu(null);
  1246. // toast("由于你在别的标签打开了“我的MARK”,此标签的“我的MARK”被关闭了",ToastType.WARNING, 99999 * 1000);
  1247. }
  1248. };
  1249.  
  1250.  
  1251. function insertDataHTML() {
  1252. let container = document.querySelector(".mpp-content-container");
  1253. let threadsStatus = MppManager.getAllThreadStatus()
  1254. let insertHTML = ``;
  1255. for (const [_tid, status] of Object.entries(threadsStatus)) {
  1256. let posts = "";
  1257. if (status['sell']) status["sell"].forEach(ele => posts += `<p><a class="mpp-sell" href="${ele}" target="_blank">[出售]${ele}</a></p>`);
  1258. if (status['hyperlink']) status["hyperlink"].forEach(ele => posts += `<p><a class="mpp-hyperlink" href="${ele}" target="_blank">[超链]${ele}</a></p>`);
  1259. if (status["magnetOrMiaochuan"]) status["magnetOrMiaochuan"].forEach(ele => posts += `<p><a class="mpp-hash" href="${ele}" target="_blank">[磁力或秒传]${ele}</a></p>`);
  1260. let button = container.querySelector(`button.mpp-accordion[data-tid="${_tid}"`);
  1261. if (button) button.classList.remove("have-content");
  1262. let content = container.querySelector(`div.mpp-accordion-content[data-tid="${_tid}"`);
  1263. // console.log(button ? button.classList.toString() : "mpp-accordion");
  1264. // console.log(content ? content.classList.toString() : "mpp-accordion-content");
  1265. // <span class="mpp-content-cell mpp-accordion-plus">${posts === "" ? "" : button.classList.contains("") ? "-" : "+"}</span>
  1266. insertHTML += `
  1267. <button type="button" class="${button ? button.classList.toString() : "mpp-accordion"} ${posts ? "have-content" : ""}" data-tid="${_tid}">
  1268. <a
  1269. class="mpp-content-cell mpp-content-title"
  1270. href="/read.php?tid=${_tid}"
  1271. target="_blank"
  1272. >${status["title"].slice(0, 20)}${status["title"].length > 20 ? "..." : ""}</a>
  1273. <span class="mpp-content-cell">${status["page"] || 0} / ${status["maxPage"]}</span>
  1274. <span class="mpp-content-cell mpp-content-last-fetch-time">${status["lastFetchTime"] ? `${Math.round((getTimeStamp() - parseInt(status["lastFetchTime"])) / 1000 / 60)} 分钟之前` : '尚未检查'}</span>
  1275. <span class="mpp-content-cell">${status["offerState"]}</span>
  1276. <span class="mpp-content-cell">${status["markTime"]}</span>
  1277. <a class="mpp-delete mpp-content-cell" data-tid="${_tid}">删除</a>
  1278. </button>
  1279.  
  1280. <div class="${content ? content.classList.toString() : "mpp-accordion-content"}" data-tid="${_tid}">
  1281. ` + posts + `
  1282. </div>
  1283. `;
  1284.  
  1285. }
  1286. container.innerHTML = insertHTML;
  1287. document.querySelectorAll("button.mpp-accordion").forEach(ele => {
  1288. ele.addEventListener("click", evt => {
  1289. evt.stopPropagation();
  1290. if (!evt.currentTarget.nextElementSibling.querySelectorAll("p>a").length) return;
  1291. let btn = evt.currentTarget;
  1292. let content = btn.nextElementSibling;
  1293. btn.classList.toggle("mpp-accordion-is-open");
  1294. content.classList.toggle("mpp-accordion-is-open");
  1295. content.style.maxHeight = content.classList.contains("mpp-accordion-is-open") ? content.scrollHeight + 'px' : null;
  1296. // evt.currentTarget.querySelector(".mpp-accordion-plus").textContent = content.classList.contains("mpp-accordion-is-open") ? "-" : "+";
  1297. });
  1298.  
  1299. });
  1300. document.querySelectorAll(".mpp-delete.mpp-content-cell").forEach(ele => {
  1301. ele.addEventListener("click", evt => {
  1302. evt.stopPropagation();
  1303. evt.preventDefault();
  1304. if (!confirm("删除后无法撤销,确定删除?")) return;
  1305. const parentButton = evt.currentTarget.closest("button")
  1306. const content = parentButton.nextElementSibling;
  1307. parentButton.remove();
  1308. content.remove();
  1309. MppManager.deleteMarkedThread(evt.currentTarget.dataset.tid);
  1310.  
  1311. })
  1312. });
  1313. }
  1314.  
  1315. function openStatus(evt) {
  1316. evt.stopPropagation();
  1317. evt.preventDefault();
  1318. // 显示数据
  1319. insertDataHTML();
  1320.  
  1321. // 显示菜单
  1322. let sppMenu = document.querySelector(".mpp");
  1323. sppMenu.classList.remove("spp-hide");
  1324. // 显示遮罩
  1325. let sppMenuMask = document.querySelector(".mpp-mask");
  1326. sppMenuMask.classList.remove("spp-hide");
  1327. // 防止滚动到菜单后面的页面
  1328. document.body.style.overflow = "hidden";
  1329.  
  1330. bc.postMessage('mppTaskStart');
  1331. setTimeout(mppTask, 5000);
  1332. sessionStorage.setItem("Soul++:MppTaskID", 'start');
  1333. refreshID = setInterval(insertDataHTML, 1000);
  1334. }
  1335.  
  1336. function closeMenu(evt) {
  1337. if (evt) evt.stopPropagation();
  1338. document.body.style.overflow = null;
  1339. let sppMenu = document.querySelector(".mpp");
  1340. sppMenu.classList.add("spp-hide");
  1341. let sppMenuMask = document.querySelector(".mpp-mask");
  1342. sppMenuMask.classList.add("spp-hide");
  1343. // clearInterval(parseInt(sessionStorage.getItem("Soul++:MppTaskID")));
  1344. sessionStorage.setItem("Soul++:MppTaskID", 'stop');
  1345. clearInterval(refreshID);
  1346. }
  1347.  
  1348. document.querySelector("a.mpp-status").addEventListener("click", openStatus)
  1349.  
  1350. document.querySelector(".mpp-mask").addEventListener("click", closeMenu);
  1351.  
  1352.  
  1353. }
  1354.  
  1355. function backToTop() {
  1356.  
  1357. const GMKey = "Style_backToTop";
  1358. let backToTopButton = createFloatDraggableButton(
  1359. "回到顶部",
  1360. GMKey,
  1361. {
  1362. left: "calc(50vw + 470px)",
  1363. bottom: "40px",
  1364. background: "linear-gradient(to top, #eeeeee,#ffffff)",
  1365. color: "black",
  1366. fontWeight: "bold",
  1367. outline: "none",
  1368. border: "none",
  1369. borderRadius: "3px",
  1370. width: "30px",
  1371. opacity: "80%",
  1372. cursor: "pointer",
  1373. }
  1374. );
  1375.  
  1376. let dragStart = {};
  1377. let dragEnd = {};
  1378.  
  1379. // 防止拖到视口以外了
  1380. AddIntersectionObserver(([entry]) => {
  1381. if (!entry.isIntersecting) {
  1382. console.log('LEAVE');
  1383. backToTopButton.style.left = dragStart.saved.left;
  1384. backToTopButton.style.bottom = dragStart.saved.bottom;
  1385. }
  1386. }, backToTopButton)
  1387.  
  1388. backToTopButton.addEventListener("click", (e) => {
  1389. e.stopPropagation();
  1390. window.scrollTo({ top: 0, behavior: "smooth" });
  1391. });
  1392.  
  1393. backToTopButton.addEventListener("dragstart", (e) => {
  1394. e.stopPropagation();
  1395.  
  1396. dragStart = {
  1397. clientX: e.clientX,
  1398. clientY: e.clientY,
  1399. saved: {
  1400. left: e.target.style.left,
  1401. bottom: e.target.style.bottom,
  1402. }
  1403. }
  1404. });
  1405.  
  1406. backToTopButton.addEventListener("dragend", (e) => {
  1407. e.stopPropagation();
  1408. // 获得丅的交叉点坐标
  1409. dragEnd = {
  1410. clientX: e.clientX,
  1411. clientY: e.clientY,
  1412. }
  1413. let newLeft = parseFloat(e.target.style.left.match(/(-?\d+)px/)[1]) + (dragEnd.clientX - dragStart.clientX);
  1414. // bottom从下往上算的,所以要减
  1415. let newBottom = parseFloat(e.target.style.bottom.match(/(-?\d+)px/)[1]) - (dragEnd.clientY - dragStart.clientY);
  1416.  
  1417.  
  1418. e.target.style.left = `calc(50vw + ${newLeft}px)`;
  1419. e.target.style.bottom = `${newBottom}px`;
  1420.  
  1421. let tmp = {
  1422. ...GMK.getValue(GMKey),
  1423. ...{
  1424. left: `calc(50vw + ${newLeft}px)`,
  1425. bottom: `${newBottom}px`,
  1426. }
  1427. };
  1428. GMK.setValue(GMKey, tmp);
  1429.  
  1430.  
  1431. });
  1432.  
  1433. }
  1434.  
  1435. function postAddAnchorAttribute(target, pg, threadID) {
  1436. target.querySelectorAll(".tpc_content .f14").forEach(ele => {
  1437. ele.setAttribute("page", pg);
  1438. ele.setAttribute("tid", threadID);
  1439. let pid = ele.previousElementSibling.getAttribute("name");
  1440. ele.setAttribute("pid", pid);
  1441.  
  1442. });
  1443. }
  1444.  
  1445. function threadAddAnchorAttribute(target, pg, forumID) {
  1446. target.querySelectorAll(".tr3.t_one").forEach(ele => {
  1447. ele.setAttribute("page", pg);
  1448. ele.setAttribute("fid", forumID);
  1449. let tid_m = ele.querySelector("a").getAttribute("href").match(/tid-(\d+)/);
  1450. if (!tid_m) return;
  1451. let tid = tid_m[1];
  1452. ele.setAttribute("tid", tid);
  1453.  
  1454. });
  1455. }
  1456.  
  1457. function highlightViewedThread() {
  1458.  
  1459. function removeCurrent() {
  1460. let prev = document.querySelector(".spp-last-viewed-thread");
  1461. if (prev) prev.classList.remove("spp-last-viewed-thread");
  1462. }
  1463.  
  1464. function setLastViewed() {
  1465. let tmp = GMK.getValue("Soul++:lastViewedThread") || {};
  1466. tmp[fid] = tid;
  1467. GMK.setValue("Soul++:lastViewedThread", tmp);
  1468. }
  1469.  
  1470. function setViewed() {
  1471. let tmp = GMK.getValue("Soul++:viewedThreads") || {};
  1472. tmp[fid] = tmp[fid] || [];
  1473. if (!tmp[fid].includes(tid)) tmp[fid].push(tid);
  1474. GMK.setValue("Soul++:viewedThreads", tmp);
  1475. }
  1476.  
  1477. // 帖子阅读页面处理
  1478. if ((document.location.href.includes("/read.php"))) {
  1479. // 直接打开页面的话不会触发visibilitychange事件
  1480. if (!document.hidden) setViewed();
  1481. // 当visibilitychange触发时,hidden代表用户关闭或者离开了当前页面
  1482. document.addEventListener("visibilitychange", () => {
  1483. if (document.hidden) {
  1484. setLastViewed();
  1485. } else {
  1486. setViewed();
  1487. }
  1488. });
  1489. }
  1490. // 帖子列表页面处理
  1491. else if ((document.location.href.includes("/thread.php") || document.location.href.includes("/thread_new.php"))) {
  1492. // 在帖子列表页面会主动滚动到最后浏览的帖子的位置
  1493. document.addEventListener('readystatechange', (event) => {
  1494. if (document.readyState === "complete") {
  1495. history.scrollRestoration = "manual";
  1496. let ele = document.querySelector(".spp-last-viewed-thread");
  1497. if (!ele) return;
  1498. ele.scrollIntoView({ behavior: "auto", block: "center" });
  1499. }
  1500. });
  1501.  
  1502. // 主动更新帖子列表页
  1503. // GM_addValueChangeListener(name, function(name, old_value, new_value, remote) {})
  1504. GMK.addValueChangeListener("Soul++:viewedThreads", (_name, oldVal, newVal, remote) => {
  1505. console.log(`本版块已阅帖:${newVal[fid]}`);
  1506. document.querySelectorAll(".tr3.t_one").forEach(ele => {
  1507. if (newVal[fid].includes(ele.getAttribute("tid"))) {
  1508. ele.querySelector("h3 a").classList.add("spp-viewed-thread");
  1509. }
  1510. });
  1511. })
  1512. GMK.addValueChangeListener("Soul++:lastViewedThread", (_name, oldVal, newVal, remote) => {
  1513. // 新记录中当前fid下正在阅读的tid和DOM树中一致的话则返回
  1514. if (document.querySelector(".spp-last-viewed-thread").getAttribute("tid") === newVal[fid]) return;
  1515. console.log(`正在本版块阅读新帖:${newVal[fid]}`);
  1516. removeCurrent();
  1517. document.querySelectorAll(".tr3.t_one").forEach(ele => {
  1518. if (ele.getAttribute("tid") === newVal[fid]) {
  1519. ele.classList.add("spp-last-viewed-thread");
  1520. ele.scrollIntoView({ behavior: "auto", block: "center" });
  1521. }
  1522. });
  1523. })
  1524.  
  1525. // 将已经阅读过的帖子改成灰色
  1526. let readedThreads = GMK.getValue("Soul++:viewedThreads") || {};
  1527. let thisForumReadedThreads = readedThreads[fid] || [];
  1528. // console.log(`当前版块已读帖子:${thisForumReadedThreads}`);
  1529. document.querySelectorAll("h3 a").forEach(ele => {
  1530. let container = ele.closest(".tr3.t_one");
  1531. if (!container) return;
  1532.  
  1533. if (thisForumReadedThreads.includes(container.getAttribute("tid"))) {
  1534. console.log(`${container.getAttribute("tid")} 已读`);
  1535. ele.classList.add("spp-viewed-thread");
  1536. }
  1537. if (ele.getAttribute("id") === `a_ajax_${GMK.getValue("Soul++:lastViewedThread")[fid]}`) {
  1538. container.classList.add("spp-last-viewed-thread");
  1539. }
  1540. ele.addEventListener("click", e => {
  1541. e.stopPropagation();
  1542. removeCurrent();
  1543. e.target.closest(".tr3.t_one").classList.add("spp-last-viewed-thread");
  1544. });
  1545. });
  1546. }
  1547. }
  1548.  
  1549. function createSettingMenu() {
  1550. let menuBox = document.createElement("div");
  1551. document.querySelector("#main").prepend(menuBox);
  1552.  
  1553. let menuButton = document.createElement("li");
  1554. let a = document.createElement("a");
  1555. a.innerText = "Soul++";
  1556. a.style.cursor = "pointer";
  1557. menuButton.appendChild(a);
  1558. a.addEventListener("click", e => {
  1559. e.stopPropagation();
  1560. // 读取数据更显选项显示
  1561. document.querySelectorAll(".spp-accordion-content").forEach(ele => {
  1562. ele.querySelectorAll(".spp-menu-checkbox").forEach(ele => {
  1563. let checkbox = ele.querySelector("input");
  1564. if (checkbox) {
  1565. let key = checkbox.dataset.funckey;
  1566. checkbox.checked = GMK.getValue(key);
  1567. }
  1568. })
  1569. });
  1570.  
  1571. // 显示菜单
  1572. let sppMenu = document.querySelector(".spp-menu");
  1573. sppMenu.classList.remove("spp-hide");
  1574. // 显示遮罩
  1575. let sppMenuMask = document.querySelector(".spp-menu-mask");
  1576. sppMenuMask.classList.remove("spp-hide");
  1577. // 防止滚动到菜单后面的页面
  1578. document.body.style.overflow = "hidden";
  1579.  
  1580. });
  1581. document.querySelector("#guide").prepend(menuButton);
  1582.  
  1583. menuBox.outerHTML = `
  1584. <div class="spp-menu-mask spp-hide"></div>
  1585. <div class="spp-menu spp-hide">
  1586. <div class="spp-menu-container">
  1587. <p class="spp-menu-title">Soul++ 设置</p>
  1588. <div class="spp-menu-main">
  1589. <div class="spp-menu-accordion-op spp-sticky">
  1590. <a class="spp-menu-accordion-support-me" style="grid-column-start: 1">支持作者</a>
  1591. <a class="spp-menu-accordion-expand-all" style="grid-column-start: 4">全部展开</a>
  1592. <a class="spp-menu-accordion-collapse-all" style="grid-column-start: 5">全部折叠</a>
  1593. </div>
  1594. <button type="button" class="spp-accordion spp-accordion-is-open">🔄 免刷新</button>
  1595. <div class="spp-accordion-content spp-accordion-is-open">
  1596. <div class="spp-menu-checkbox"><label><input data-funcKey="buyRefresh_free" type="checkbox" id="buy-refresh-free">购买免刷新</label></div>
  1597. </div>
  1598.  
  1599.  
  1600. <button type="button" class="spp-accordion spp-accordion-is-open">♾️ 无缝加载</button>
  1601. <div class="spp-accordion-content spp-accordion-is-open">
  1602. <div class="spp-menu-checkbox"><label><input data-funcKey="dynamicLoadingThreads" type="checkbox" id="dynamic-load-posts">无缝加载板块帖子列表</label></div>
  1603. <div class="spp-menu-checkbox"><label><input data-funcKey="dynamicLoadingPosts" type="checkbox" id="dynamic-load-threads">无缝加载贴内楼层列表</label></div>
  1604. <div class="spp-menu-checkbox"><label><input data-funcKey="dynamicLoadingSearchResult" type="checkbox" id="dynamic-load-search-result">无缝加载搜索页结果</label></div>
  1605. <div class="spp-menu-checkbox"><label><input data-funcKey="dynamicLoadingPicWall" type="checkbox" id="dynamic-load-pic-wall">无缝加载图墙模式帖子</label></div>
  1606. </div>
  1607.  
  1608. <button type="button" class="spp-accordion spp-accordion-is-open">🛑 屏蔽</button>
  1609. <div class="spp-accordion-content spp-accordion-is-open">
  1610. <div class="spp-menu-checkbox"><label><input data-funcKey="blockAdforumSearchResult" type="checkbox" id="block-adforum-search-result">屏蔽网赚区搜索结果</label></div>
  1611. </div>
  1612.  
  1613. <button type="button" class="spp-accordion spp-accordion-is-open">🔞 SFW安全模式</button>
  1614. <div class="spp-accordion-content spp-accordion-is-open">
  1615. <div class="spp-menu-checkbox"><label><input data-funcKey="hidePostImage" type="checkbox" id="hide-post-image">折叠贴内图片(点击虚线框 展开/折叠 图片)</label></div>
  1616. <div class="spp-menu-checkbox spp-menu-sub-item"><label><input data-funcKey="loadImageOnDemand" type="checkbox" id="load-image-on-demand">按需加载头像、图片(展开后才开始加载)</label></div>
  1617. <div class="spp-menu-checkbox"><label><input data-funcKey="hideForumRules" type="checkbox" id="hide-chaguan-poster">折叠板块公告(其实板块公告右边有个小箭头,我只是帮你们点了一下)</label></div>
  1618. <div class="spp-menu-checkbox"><label><input data-funcKey="hideUserAvatar" type="checkbox" id="hide-user-avatar">替换用户头像为默认(鼠标滑入查看)</label></div>
  1619. </div>
  1620. <button type="button" class="spp-accordion spp-accordion-is-open">🔖 mark++</button>
  1621. <div class="spp-accordion-content spp-accordion-is-open">
  1622. <div class="spp-menu-checkbox"><label><input data-funcKey="markPlusPlus" type="checkbox" id="mark-plus-plus">开启MARK++</label></div>
  1623. <div class="spp-menu-checkbox"><label class="spp-menu-description">- 打开后查看帖子页面右边会出现MARK按钮(可拖到任意位置)</label></div>
  1624. <div class="spp-menu-checkbox"><label class="spp-menu-description">- 点击MARK之后,当前帖子会加入到“我的MARK”列表里</label></div>
  1625. <div class="spp-menu-checkbox"><label class="spp-menu-description">- 在导航栏可以找到“我的MARK”入口,右键点击MARK按钮也可以打开“我的MARK”</label></div>
  1626. <div class="spp-menu-checkbox"><label class="spp-menu-description" style="color: brown">- 保持打开“我的MARK”窗口,脚本会以5秒/帖的频率检查MARK列表</label></div>
  1627. <div class="spp-menu-checkbox"><label class="spp-menu-description" style="color: brown">- 同一时间只允许一个浏览器标签打开“我的MARK”</label></div>
  1628. </div>
  1629. <button type="button" class="spp-accordion spp-accordion-is-open">💠 其它</button>
  1630. <div class="spp-accordion-content spp-accordion-is-open">
  1631. <div class="spp-menu-checkbox"><label><input data-funcKey="automaticTaskCollection" type="checkbox" id="automatic-task-collection">自动领取和完成论坛任务</label></div>
  1632. <div class="spp-menu-checkbox"><label><input data-funcKey="hoistingResourcePost" type="checkbox" id="hoisting-resource-post">将当前页包含[购买/秒传/磁力链/超链]的楼层提升到前面</label></div>
  1633. <div class="spp-menu-checkbox"><label><input data-funcKey="replaceAllDomainToTheSame" type="checkbox" id="replace-all-plus-to-the-same">统一替换所有plus链接为当前正在使用的域名</label></div>
  1634. <div class="spp-menu-checkbox"><label><input data-funcKey="highlightViewedThread" type="checkbox" id="highlight-viewed-threads">标记已阅读过的帖子</label></div>
  1635. <div class="spp-menu-checkbox"><label><input data-funcKey="linkToReplyAndQuote" type="checkbox" id="link-to-reply-and-quote">给[回复第X楼/引用第X楼]增加跳转到该楼层的链接</label></div>
  1636. </div>
  1637. <button type="button" class="spp-accordion spp-danger">❗</button>
  1638. <div class="spp-accordion-content spp-danger-content">
  1639. <button class="spp-btn-danger" data-funcKey="resetAll" id="spp-reset-all">清空所有设置</button>
  1640. </div>
  1641.  
  1642. </div>
  1643. <div class="spp-menu-op-zone">
  1644. <button id="spp-menu-close"><img alt="" src="images/post/smile/smallface/face099.jpg"/> 我好了</button>
  1645. </div>
  1646. </div>
  1647. </div>
  1648. `;
  1649.  
  1650. (function () {
  1651. function closeMenu(evt, saveAndRefresh) {
  1652. evt.stopPropagation();
  1653. document.body.style.overflow = null;
  1654. let sppMenu = document.querySelector(".spp-menu");
  1655. sppMenu.classList.add("spp-hide");
  1656. let sppMenuMask = document.querySelector(".spp-menu-mask");
  1657. sppMenuMask.classList.add("spp-hide");
  1658. if (saveAndRefresh) {
  1659. changes.forEach(e => GMK.setValue(e[0], e[1]))
  1660. document.location.reload();
  1661. }
  1662. changes = [];
  1663.  
  1664. }
  1665.  
  1666. window.addEventListener("keydown", evt => {
  1667. if (evt.key === "Escape") closeMenu(evt, false);
  1668. });
  1669. let changes = [];
  1670. document.querySelectorAll(".spp-menu-checkbox input").forEach(ele => {
  1671. ele.addEventListener("change", evt => {
  1672. changes.push([evt.currentTarget.dataset.funckey, evt.currentTarget.checked]);
  1673. });
  1674. });
  1675.  
  1676. document.querySelector("#spp-menu-close").addEventListener("click", evt => closeMenu(evt, true));
  1677. document.querySelector(".spp-menu-mask").addEventListener("click", evt => closeMenu(evt, false));
  1678. document.querySelector("#spp-reset-all").addEventListener("click", evt => {
  1679. evt.stopPropagation();
  1680.  
  1681. if (confirm(
  1682. `
  1683. 【警告】
  1684. 这将会清空你在所有的数据和设置,包括:
  1685. - 已读的帖子
  1686. - 可拖放按钮的位置
  1687. - 已经MARK过的帖子
  1688. 你确定要这样做?`
  1689. )) {
  1690. GMK.listValues().forEach(vName => {
  1691. console.log(vName);
  1692. GMK.deleteValue(vName);
  1693. });
  1694. console.log(GMK.listValues());
  1695. document.location.reload();
  1696. }
  1697. });
  1698. document.querySelector(".spp-menu-accordion-support-me").addEventListener("click", evt => {
  1699. evt.stopPropagation();
  1700. let toastTip = Toastify({
  1701. text: "点我或者点击图片即可关闭",
  1702. duration: 15000,
  1703. close: true,
  1704. gravity: "top", // `top` or `bottom`
  1705. position: "center", // `left`, `center` or `right`
  1706. stopOnFocus: true, // Prevents dismissing of toast on hover
  1707. style: {},
  1708. onClick: function () {
  1709. toastTip.hideToast();
  1710. toastRedEnvelop.hideToast();
  1711. }
  1712. })
  1713. toastTip.showToast();
  1714. let img = document.createElement("img");
  1715. img.style.background = "none";
  1716. img.style.boxShadow = "none";
  1717. img.style.borderRadius = "10px";
  1718. img.style.width = "300px";
  1719. img.style.height = "435px";
  1720. img.src = 'https://cdn.jsdelivr.net/gh/FetchTheMoon/UserScript/images/RedEnvelope.jpg';
  1721. let toastRedEnvelop = Toastify({
  1722. node: img,
  1723. duration: 9999999,
  1724. close: false,
  1725. gravity: "top", // `top` or `bottom`
  1726. position: "center", // `left`, `center` or `right`
  1727. stopOnFocus: true, // Prevents dismissing of toast on hover
  1728. style: {
  1729. background: "none",
  1730. boxShadow: "none",
  1731. },
  1732. onClick: function () {
  1733. toastTip.hideToast();
  1734. toastRedEnvelop.hideToast();
  1735. }
  1736. });
  1737. toastRedEnvelop.showToast();
  1738. });
  1739. document.querySelector(".spp-menu-accordion-expand-all").addEventListener("click", evt => {
  1740.  
  1741. document.querySelectorAll(".spp-accordion").forEach(ele => {
  1742. if (ele.classList.contains("spp-danger")) return;
  1743. if (!ele.classList.contains(" spp-accordion-is-open")) ele.classList.add("spp-accordion-is-open");
  1744. });
  1745. document.querySelectorAll(".spp-accordion-content").forEach(ele => {
  1746. if (ele.classList.contains("spp-danger-content")) return;
  1747. if (!ele.classList.contains("spp-accordion-is-open")) ele.classList.add("spp-accordion-is-open");
  1748. ele.style.maxHeight = ele.scrollHeight + 'px';
  1749. });
  1750. });
  1751. document.querySelector(".spp-menu-accordion-collapse-all").addEventListener("click", evt => {
  1752. document.querySelectorAll(".spp-accordion").forEach(ele => {
  1753. if (ele.classList.contains("spp-accordion-is-open")) ele.classList.remove("spp-accordion-is-open")
  1754. });
  1755. document.querySelectorAll(".spp-accordion-content").forEach(ele => {
  1756. if (ele.classList.contains("spp-accordion-is-open")) ele.classList.remove("spp-accordion-is-open")
  1757. ele.style.maxHeight = null;
  1758. });
  1759. });
  1760. document.querySelectorAll("button.spp-accordion").forEach(ele => {
  1761. ele.addEventListener("click", evt => {
  1762. evt.stopPropagation();
  1763. let btn = evt.target;
  1764. let content = btn.nextElementSibling;
  1765. btn.classList.toggle("spp-accordion-is-open");
  1766. content.classList.toggle("spp-accordion-is-open");
  1767. content.style.maxHeight = content.classList.contains("spp-accordion-is-open") ? content.scrollHeight + 'px' : null;
  1768. });
  1769.  
  1770. });
  1771. }());
  1772. }
  1773.  
  1774. function replaceAllDomainToTheSame() {
  1775.  
  1776.  
  1777. const arr = [...document.querySelectorAll("a")];
  1778. const domains = [
  1779. "spring-plus.net",
  1780. "summer-plus.net",
  1781. "soul-plus.net",
  1782. "south-plus.net",
  1783. "north-plus.net",
  1784. "snow-plus.net",
  1785. "level-plus.net",
  1786. "white-plus.net",
  1787. "imoutolove.me",
  1788. "south-plus.org",
  1789. ];
  1790. const checker = value => domains.some(element => value.href.includes(element));
  1791.  
  1792. arr.filter(checker).filter(ele => !ele.href.includes(window.location.hostname)).forEach(ele => {
  1793. console.log("替换链接:", ele);
  1794. let newURL = new URL(ele.href);
  1795. newURL.hostname = window.location.hostname;
  1796. ele.href = newURL.href;
  1797. });
  1798.  
  1799.  
  1800. }
  1801.  
  1802. function MutationObserverProcess() {
  1803. function callback(mutationList, observer) {
  1804. mutationList.forEach((mutation) => {
  1805. mutation.addedNodes.forEach(ele => {
  1806.  
  1807. if (ele.tagName === "IMG") {
  1808. if (ele.classList.contains("spp-mutation-processed")) return
  1809.  
  1810. function hide(confirmSelector, handler) {
  1811. const postContainer = ele.closest(confirmSelector);
  1812. if (!postContainer) return
  1813. handler(ele);
  1814. ele.classList.add("spp-mutation-processed");
  1815. }
  1816.  
  1817. if (document.location.href.includes("/read.php")) {
  1818. ele.setAttribute("loading", "lazy");
  1819. if (GMK.getValue("hideUserAvatar")) hide(".user-pic", hideAvatar)
  1820. if (GMK.getValue("hidePostImage")) hide(".t5.t2 .r_one", hideImg)
  1821. }
  1822. }
  1823. if (document.location.href.includes("/read.php") && GMK.getValue("linkToReplyAndQuote")) {
  1824. if (ele.title === "复制此楼地址") {
  1825. ele.insertAdjacentHTML("beforebegin", `<a name="SPP-${ele.innerText}" id="SPP-${ele.innerText}"></a>`);
  1826. }
  1827. }
  1828. });
  1829. });
  1830.  
  1831.  
  1832. }
  1833.  
  1834. let observerOptions = {
  1835. childList: true, // 观察目标子节点的变化,是否有添加或者删除
  1836. attributes: true, // 观察属性变动
  1837. subtree: true // 观察后代节点,默认为 false
  1838. }
  1839.  
  1840. let observer = new MutationObserver(callback);
  1841. observer.observe(document.documentElement, observerOptions);
  1842. }
  1843.  
  1844. async function mppTask() {
  1845. let taskInterval = 5000;
  1846. await (async function () {
  1847.  
  1848. // 先查询页数没到最后一页的
  1849. let markedList = Object.entries(MppManager.getMarkList()).filter(e => {
  1850. return e[1]['maxPage'] - e[1]['page'] > 0
  1851. });
  1852. // 如果都到最后一页了,就按上次查询时间距今最远的来
  1853. if (!markedList.length) {
  1854. markedList = Object.entries(MppManager.getMarkList())
  1855. .sort((f, s) => {
  1856. return f[1]['lastFetchTime'] - s[1]['lastFetchTime']
  1857. });
  1858. }
  1859. console.log(markedList);
  1860. if (!markedList.length) return;
  1861. if (MppManager.isAllChecked()) taskInterval = 10 * 1000;
  1862. let [_tid, threadStatus] = markedList[0];
  1863. let maxPage = threadStatus['maxPage'] || 1;
  1864. let currentPage = threadStatus["page"] || 1;
  1865. const dummy = await getPage(`/read.php?tid=${_tid}&page=${currentPage}`, true);
  1866. const m = dummy.innerHTML.match(/var totalpage = parseInt\('(\d+)'\)/);
  1867. if (m) maxPage = parseInt(m[1]);
  1868. const allPosts = [...dummy.querySelectorAll(".t5.t2")];
  1869.  
  1870. function getOfferState(dummy) {
  1871. const ele = dummy.querySelector(".tips .s3");
  1872. if (!ele) {
  1873. console.log("没找到悬赏状态", ele, dummy);
  1874. return;
  1875. }
  1876. const state = ele.textContent;
  1877. if (state.includes("剩余时间:已结束")) {
  1878. return "悬赏超时";
  1879. } else if (state.includes("悬赏结束")) {
  1880. return `<a class="mpp-sell" href='/read.php?tid=${_tid}' target="_blank" style="color: #73a5ff">有答案了</a>`;
  1881. } else if (state.includes("悬赏中")) {
  1882. return `剩余${state.match(/(\d+)小时/)[1]}小时`;
  1883. }
  1884. }
  1885.  
  1886. if (currentPage === 1) {
  1887. threadStatus["offerState"] = getOfferState(dummy);
  1888. allPosts.shift();
  1889. }
  1890. // 在第一页获取一下帖子的状态,看看到底有没有结贴
  1891. if (threadStatus["allPagesChecked"]) {
  1892. await sleep(5000); // 否则会遇到提示刷新小于1秒;
  1893. const page1dummy = await getPage(`/read.php?tid=${_tid}&page=1`, true);
  1894. threadStatus["offerState"] = getOfferState(page1dummy);
  1895. }
  1896. threadStatus["sell"] = threadStatus["sell"] || [];
  1897. threadStatus["hyperlink"] = threadStatus["hyperlink"] || [];
  1898. threadStatus["magnetOrMiaochuan"] = threadStatus["magnetOrMiaochuan"] || [];
  1899. threadStatus['title'] = dummy.querySelector('.crumbs-item.current strong>a').textContent;
  1900. threadStatus['lastFetchTime'] = getTimeStamp();
  1901. threadStatus['maxPage'] = maxPage;
  1902. threadStatus["page"] += currentPage < maxPage ? 1 : 0;
  1903. const getPid = (post) => {
  1904. return post.previousSibling.getAttribute("name");
  1905. }
  1906. allPosts.forEach(post => {
  1907.  
  1908. let u = `/read.php?tid=${_tid}&page=${currentPage}#${getPid(post)}`;
  1909. if (post.querySelector(".quote.jumbotron")) {
  1910. if (!threadStatus["sell"].includes(u)) threadStatus["sell"].push(u)
  1911. } else if (post.querySelector(".tpc_content a")) {
  1912. if (!threadStatus["hyperlink"].includes(u)) threadStatus["hyperlink"].push(u);
  1913. } else if (post.querySelector(".tpc_content").textContent.match(/^[0-9a-fA-F]{20,}/mg)) {
  1914. if (!threadStatus["magnetOrMiaochuan"].includes(u)) threadStatus["magnetOrMiaochuan"].push(u);
  1915. }
  1916. });
  1917.  
  1918.  
  1919. threadStatus["allPagesChecked"] = currentPage === maxPage;
  1920. if (MppManager.isThreadExist(_tid)) MppManager.setThreadStatus(_tid, threadStatus);
  1921. console.log(threadStatus);
  1922.  
  1923. })();
  1924. if (sessionStorage.getItem("Soul++:MppTaskID") === 'start') setTimeout(mppTask, taskInterval);
  1925.  
  1926. }
  1927.  
  1928. function linkToReplyAndQuote(target = document) {
  1929. if (window.location.hash.includes("#SPP-")) {
  1930. const ele = document.querySelector(window.location.hash);
  1931. if (ele) ele.scrollIntoView();
  1932. }
  1933.  
  1934. let allPosts = [...target.querySelectorAll(".t5.t2")];
  1935. allPosts.forEach(ele => {
  1936. const quote = ele.querySelector("h6.quote2+div");
  1937. if (quote) {
  1938. const floor = parseInt(quote.firstChild.textContent.match(/引用第(\d+)楼/)[1]);
  1939. const page = Math.ceil(floor / 30);
  1940. const text = quote.firstChild.textContent.replace(/引用(第\d+楼)(.+)/, `引用<a style="color: dodgerblue" href="/read.php?tid=${tid}&page=${page}#SPP-B${floor}F">$1</a>$2`);
  1941. quote.removeChild(quote.firstChild);
  1942. quote.insertAdjacentHTML("afterbegin", text);
  1943.  
  1944. }
  1945. const reply = ele.querySelector(".h1.fl>.fl");
  1946. if (reply) {
  1947. const m = reply.firstChild.textContent.match(/回 (\d+)楼/);
  1948. if(!m) return
  1949. const floor = parseInt(m[1]);
  1950. const page = Math.ceil(floor / 30);
  1951. const text = reply.innerText.replace(/回 (\d+)楼(.+)/, `回 <a style="color: dodgerblue" href="/read.php?tid=${tid}&page=${page}#SPP-B${floor}F">$1楼</a>$2`);
  1952. reply.innerText = "";
  1953. reply.insertAdjacentHTML("afterbegin", text);
  1954.  
  1955. }
  1956. });
  1957. }
  1958.  
  1959. function hoistingResourcePost(target = document) {
  1960. if (document.location.href.includes("#")) return;
  1961. let allPosts = [...target.querySelectorAll(".t5.t2")];
  1962.  
  1963. // 是否拿出顶楼
  1964. const insertPosition = page === 1 ? allPosts.shift() : target.querySelector("input[name=tid]");
  1965. const resourcePosts = allPosts.filter(post =>
  1966. post.querySelector(".quote.jumbotron")
  1967. || post.querySelector(".tpc_content a")
  1968. || post.querySelector(".tpc_content").textContent.match(/^[0-9a-fA-F]{20,}/mg)
  1969. ).reverse();
  1970. resourcePosts.forEach(ele => insertPosition.after(ele));
  1971.  
  1972. }
  1973.  
  1974. function AddIntersectionObserver(callback, element) {
  1975. const obs = new window.IntersectionObserver(callback, {
  1976. root: null,
  1977. threshold: 0.5,
  1978. })
  1979. obs.observe(element);
  1980. }
  1981.  
  1982. //##############################################################
  1983. // 执行入口
  1984. //##############################################################
  1985. (function () {
  1986.  
  1987. GMK.addStyle(`<style>
  1988. .spp-last-viewed-thread{
  1989. background: #deeeff;
  1990. }
  1991. .spp-viewed-thread{
  1992. color: #bbbbbb;
  1993. }
  1994. .spp-hide{
  1995. display: none;
  1996. }
  1997. .spp-menu{
  1998. position: fixed;
  1999. left: 50%;
  2000. top: 50%;
  2001. transform: translate(-50%, -50%);
  2002. background: white;
  2003. z-index: 2000000;
  2004. }
  2005. .spp-menu-mask{
  2006. position: fixed;
  2007. left: 0;
  2008. top: 0;
  2009. width: 100%;
  2010. height: 100%;
  2011. background: black;
  2012. opacity: 0.5;
  2013. user-select: none;
  2014. z-index: 1000000;
  2015. }
  2016. .spp-menu-container{
  2017. background: #eeeeee;
  2018. display:grid;
  2019. grid-template-areas:
  2020. "title"
  2021. "main"
  2022. "op";
  2023. grid-template-rows: 28px auto 40px;
  2024. grid-gap: 10px;
  2025. min-height: 80vh;
  2026. max-height: 80vh;
  2027. width: 600px;
  2028. }
  2029. .spp-menu-title{
  2030. grid-area: title;
  2031. background: url(images/colorImagination/bg_topnav.gif) repeat-x #000000;
  2032. border-left: black;
  2033. border-right: black;
  2034. text-align: center;
  2035. font-weight: bold;
  2036. height: 100%;
  2037. color: white;
  2038. padding-top: 5px;
  2039. margin: 0;
  2040. }
  2041. .spp-menu-main{
  2042. grid-area: main;
  2043. height: 70vh;
  2044. overflow-y: scroll;
  2045. }
  2046. /*.spp-menu-op-zone{*/
  2047. /* grid-area: op;*/
  2048. /* display: grid;*/
  2049. /* grid-template-columns: 50% 50%;*/
  2050. /*}*/
  2051. .spp-hide{
  2052. display: none;
  2053. }
  2054. button.spp-accordion{
  2055. width: 100%;
  2056. border: none;
  2057. outline: none;
  2058. background-color: whitesmoke;
  2059. text-align: left;
  2060. padding: 10px 10px;
  2061. font-size: 14px;
  2062. font-weight: bold;
  2063. color: #444;
  2064. cursor: pointer;
  2065. transition: background-color 0.2s linear;
  2066. }
  2067. button.spp-accordion:after{
  2068. content:'+';
  2069. font-size: 14px;
  2070. float: right;
  2071. }
  2072. button.spp-accordion.spp-accordion-is-open:after{
  2073. content: '-';
  2074. }
  2075. button.spp-accordion:hover, button.spp-accordion.spp-accordion-is-open{
  2076. background-color: #ddd;
  2077. }
  2078. .spp-accordion-content{
  2079. background: #eeeeee;
  2080. border-left: 1px solid whitesmoke;
  2081. border-right: 1px solid whitesmoke;
  2082. padding: 0 20px;
  2083. margin-bottom: 3px;
  2084. max-height: 0;
  2085. overflow: hidden;
  2086. /*transition: all 0.2s ease-in-out;*/
  2087. font-size: 14px;
  2088. }
  2089. .spp-accordion-content.spp-accordion-is-open{
  2090. max-height: fit-content;
  2091. /*transition: all 0.2s ease-in-out;*/
  2092. }
  2093. .spp-sticky{
  2094. position: sticky;
  2095. top: 0;
  2096. }
  2097. .spp-menu-accordion-op{
  2098. display: grid;
  2099. grid-template-columns: repeat(5,1fr);
  2100. justify-content: end;
  2101. align-content: center;
  2102. }
  2103. .spp-menu-accordion-op a{
  2104. padding: 5px;
  2105. margin-left: 20px;
  2106. font-size: 12px;
  2107. cursor: pointer;
  2108. }
  2109. .spp-menu-checkbox{
  2110. margin: 10px;
  2111. }
  2112. .spp-menu-checkbox input[type="checkbox"]{
  2113. margin-bottom: 5px;
  2114. }
  2115. /*.spp-menu-op-zone{*/
  2116. /* display: flex;*/
  2117. /* justify-content: space-around;*/
  2118. /* align-content: center;*/
  2119. /*}*/
  2120. .spp-menu-op-zone button{
  2121. display: block;
  2122. width: 100%;
  2123. height: 100%;
  2124. background: black;
  2125. color: white;
  2126. border: 1px solid black;
  2127. font-size: 16px;
  2128. font-family: 宋体,"sans-serif";
  2129. font-weight: bold;
  2130. cursor: pointer;
  2131. }
  2132. #spp-menu-close{
  2133. background:linear-gradient(to bottom, #3a3a3a,#000000);
  2134. }
  2135. .spp-menu-description{
  2136. padding-left: 20px;
  2137. font-size: 12px;
  2138. }
  2139. .spp-danger-content{
  2140. padding: 0;
  2141. }
  2142. .spp-btn-danger{
  2143. display: block;
  2144. background: crimson;
  2145. color: whitesmoke;
  2146. width: 100%;
  2147. height: 40px;
  2148. border: none;
  2149. outline: none;
  2150. cursor: pointer;
  2151. }
  2152. .spp-menu-sub-item{
  2153. padding-left: 20px;
  2154. }
  2155. .spp-loading-animation{
  2156. width:150px;
  2157. margin:50px auto;
  2158. text-align: center;
  2159. }
  2160. .spp-loading-animation >div{
  2161. width: 18px;
  2162. height: 18px;
  2163. border-radius: 100%;
  2164. display:inline-block;
  2165. background-color: #af0909;
  2166. -webkit-animation: dot 1.4s infinite ease-in-out;
  2167. animation: dot 1.4s infinite ease-in-out;
  2168. -webkit-animation-fill-mode: both;
  2169. animation-fill-mode: both;
  2170. }
  2171. .spp-loading-animation .dot1{
  2172. -webkit-animation-delay: -0.30s;
  2173. animation-delay: -0.30s;
  2174. }
  2175. .spp-loading-animation .dot2{
  2176. -webkit-animation-delay: -0.15s;
  2177. animation-delay: -0.15s;
  2178. }
  2179. @-webkit-keyframes dot {
  2180. 0%, 80%, 100% {-webkit-transform: scale(0.0) }
  2181. 40% { -webkit-transform: scale(1.0) }
  2182. }
  2183. @keyframes dot {
  2184. 0%, 80%, 100% {-webkit-transform: scale(0.0) }
  2185. 40% { -webkit-transform: scale(1.0) }
  2186. }
  2187. </style>`.replace(/<\/?style>/gm, ""));
  2188. GMK.addStyle(GMK.getResourceText("TOASTIFY_CSS"));
  2189.  
  2190. console.log(`=======================================
  2191. Soul++ 已经启动
  2192. =======================================`);
  2193. // 给所有图片增加懒加载,以及处理图片隐藏
  2194. MutationObserverProcess();
  2195.  
  2196. // toast("Soul++ 已启动,可以在论坛导航栏进行设置", ToastType.WARNING, 5000);
  2197. // toast("Soul++ 已启动,可以在论坛导航栏进行设置", ToastType.INFO, 5000);
  2198. // toast("Soul++ 已启动,可以在论坛导航栏进行设置", ToastType.SUCCESS, 5000);
  2199. // toast("Soul++ 已启动,可以在论坛导航栏进行设置", ToastType.DANGER, 5000);
  2200.  
  2201. document.addEventListener("readystatechange", evt => {
  2202. if (!(document.readyState === "interactive")) return;
  2203. createSettingMenu();
  2204. backToTop();
  2205.  
  2206. if (document.location.href.includes("/read.php")) {
  2207. postAddAnchorAttribute(document, page, tid);
  2208. if (GMK.getValue("hoistingResourcePost")) {
  2209. hoistingResourcePost();
  2210. }
  2211. if (GMK.getValue("linkToReplyAndQuote")) {
  2212. linkToReplyAndQuote();
  2213. }
  2214. }
  2215. if (document.location.href.includes("/thread.php")) {
  2216. threadAddAnchorAttribute(document, page, fid);
  2217. if (GMK.getValue("hideForumRules")) {
  2218. document.cookie = "deploy=%09thread%09%0A;"
  2219. document.querySelector("#cate_thread").style.display = "none";
  2220. } else {
  2221. document.cookie = "deploy=;"
  2222. document.querySelector("#cate_thread").style.display = null;
  2223. }
  2224. }
  2225.  
  2226. if (GMK.getValue("buyRefresh_free") && document.location.href.includes("/read.php")) {
  2227. buyRefresh_free();
  2228. }
  2229. if (GMK.getValue("dynamicLoadingThreads") && (document.location.href.includes("/thread.php"))) {
  2230. dynamicLoadingNextPage(PageType.THREADS_PAGE);
  2231. }
  2232. if (GMK.getValue("dynamicLoadingPicWall") && document.location.href.includes("/thread_new.php")) {
  2233. dynamicLoadingNextPage(PageType.PIC_WALL_PAGE);
  2234. }
  2235. if (GMK.getValue("dynamicLoadingPosts") && document.location.href.includes("/read.php")) {
  2236. dynamicLoadingNextPage(PageType.POSTS_PAGE);
  2237. }
  2238. if (GMK.getValue("dynamicLoadingSearchResult") && document.location.href.includes("/search.php")) {
  2239. dynamicLoadingNextPage(PageType.SEARCH_RESULT);
  2240. }
  2241. if (GMK.getValue("blockAdforumSearchResult") && document.location.href.includes("/search.php")) {
  2242. blockAdforumSearchResult();
  2243. }
  2244. if (GMK.getValue("automaticTaskCollection")) {
  2245. automaticTaskCollection();
  2246. }
  2247. if (GMK.getValue("highlightViewedThread")) {
  2248. highlightViewedThread();
  2249. }
  2250. if (GMK.getValue("replaceAllDomainToTheSame")) {
  2251. replaceAllDomainToTheSame();
  2252. }
  2253. if (GMK.getValue("markPlusPlus")) {
  2254. mark();
  2255. }
  2256.  
  2257. });
  2258.  
  2259.  
  2260. })();
  2261.  
  2262.  
  2263.