NodeSeek+

load post detail information is automatically loaded when the button is clicked

当前为 2023-11-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NodeSeek+
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3.4
  5. // @description load post detail information is automatically loaded when the button is clicked
  6. // @author tsd
  7. // @match https://www.nodeseek.com/*
  8. // @match https://www.nodeseek.com/*
  9. // @icon https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png
  10. // @license GPLv3
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // @grant unsafeWindow
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_deleteValue
  17. // @grant GM_removeValueChangeListener
  18. // @grant GM_addValueChangeListener
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_unregisterMenuCommand
  21.  
  22. // ==/UserScript==
  23.  
  24. (function () {
  25. "use strict";
  26. console.log("script");
  27.  
  28. //GM_setValue
  29. //allCollectionData 收藏的post的id列表
  30. //checkInTime 签到的时间
  31. //isRandom 是否摸奖签到
  32. //isAuto 是否自动签到
  33. //contentTime 记录点击展开回复内容div的时间
  34. //replyNum 当前回复数
  35.  
  36. //默认抽奖
  37. if (GM_getValue("isRandom") === undefined) {
  38. GM_setValue("isRandom", true);
  39. }
  40. //默认手动
  41. if (GM_getValue("isAuto") === undefined) {
  42. GM_setValue("isAuto", false);
  43. }
  44.  
  45. //注册菜单
  46. let switchCheckType;
  47. let switchAutoType;
  48. let listenerswitchCheckType;
  49. let listenerswitchAutoType;
  50. function createMenu() {
  51. switchCheckType = GM_registerMenuCommand(
  52. GM_getValue("isRandom") === true
  53. ? "切换签到模式,当前为随机"
  54. : "切换签到模式,当前为固定",
  55. menuRandomClick
  56. );
  57. switchAutoType = GM_registerMenuCommand(
  58. GM_getValue("isAuto") === true
  59. ? "切换自动模式,当前为自动"
  60. : "切换自动模式,当前为手动",
  61. menuAutoClick
  62. );
  63. //是否有变动
  64. listenerswitchCheckType = GM_addValueChangeListener(
  65. "isRandom",
  66. function (name, old_value, new_value, remote) {
  67. if (old_value !== new_value) {
  68. alert(new_value === true ? "已切换为随机" : "已切换为固定");
  69. }
  70. }
  71. );
  72. listenerswitchAutoType = GM_addValueChangeListener(
  73. "isAuto",
  74. function (name, old_value, new_value, remote) {
  75. if (old_value !== new_value) {
  76. alert(new_value === true ? "已切换为自动" : "已切换为手动");
  77. }
  78. }
  79. );
  80. }
  81. //菜单点击刷新签到信息
  82. function menuRandomClick() {
  83. GM_unregisterMenuCommand(switchCheckType);
  84. GM_getValue("isRandom") === true
  85. ? GM_setValue("isRandom", false)
  86. : GM_setValue("isRandom", true);
  87. //重新注册
  88. GM_removeValueChangeListener(listenerswitchCheckType);
  89. GM_removeValueChangeListener(listenerswitchAutoType);
  90. createMenu();
  91. checkIn();
  92. }
  93. //菜单点击刷新自动信息
  94. function menuAutoClick() {
  95. GM_unregisterMenuCommand(switchAutoType);
  96. GM_getValue("isAuto") === true
  97. ? GM_setValue("isAuto", false)
  98. : GM_setValue("isAuto", true);
  99. //重新注册
  100. GM_removeValueChangeListener(listenerswitchCheckType);
  101. GM_removeValueChangeListener(listenerswitchAutoType);
  102. createMenu();
  103. checkIn();
  104. }
  105.  
  106. // 检查是否登陆
  107. let loginStatus = false;
  108. // 查看手机情况
  109. let mobileStatus = false;
  110. if (document.querySelector(".user-head")) {
  111. loginStatus = true;
  112. }
  113. if(!document.querySelector("#nsk-right-panel-container>.user-card")){
  114. mobileStatus = true;
  115. }
  116.  
  117. if (loginStatus) {
  118. //注册油猴菜单
  119. createMenu();
  120. //处理登录;
  121. checkIn();
  122. //维护一个全部的收藏id列表
  123. if (GM_getValue("allCollectionData") === undefined) {
  124. loadUntilEmpty();
  125. }
  126. } else {
  127. //清除收藏的post的id列表
  128. //GM_deleteValue("allCollectionData");
  129. //清除签到的时间
  130. //GM_deleteValue("checkInTime");
  131. //清除签到方式
  132. //GM_deleteValue("isRandom");
  133. //清除自动方式
  134. //GM_deleteValue("isAuto");
  135. }
  136.  
  137. //签到判断
  138. function checkIn() {
  139. let timeNow =
  140. new Date().getFullYear() +
  141. "/" +
  142. (new Date().getMonth() + 1) +
  143. "/" +
  144. new Date().getDate();
  145. let oldTime = GM_getValue("checkInTime");
  146. if (!oldTime || oldTime !== timeNow) {
  147. //允许执行签到
  148. if (GM_getValue("isAuto") === true) {
  149. //执行自动签到
  150. getChicken(GM_getValue("isRandom")).then((responseData) => {
  151. if (responseData.success === true) {
  152. //签到成功之后存下时间
  153. GM_setValue("checkInTime", timeNow);
  154. console.log(`[NodeSeek] 签到:`, responseData.message);
  155. } else if (responseData.message === "今天已完成签到,请勿重复操作") {
  156. //存下时间
  157. GM_setValue("checkInTime", timeNow);
  158. console.log(`[NodeSeek] 签到:`, responseData.message);
  159. } else {
  160. console.error("Error in checkIn:", error);
  161. }
  162. });
  163. } else {
  164. //执行手动签到
  165. //处理按钮
  166. let right_panel = document.querySelector("#nsk-right-panel-container");
  167. let new_check = document.createElement("div");
  168. let publish_btn = document.querySelector(".btn.new-discussion");
  169. let publish_btn_parent = publish_btn.parentNode;
  170. new_check.innerHTML = '<a class="btn new-discussion">签到</a>';
  171. //展示签到按钮
  172. right_panel.insertBefore(new_check, publish_btn_parent);
  173. setupCursorStyle(new_check);
  174. new_check.onclick = function () {
  175. getChicken(GM_getValue("isRandom")).then((responseData) => {
  176. console.log(responseData.message);
  177. if (responseData.success === true) {
  178. //签到成功之后存下时间
  179. GM_setValue("checkInTime", timeNow);
  180. //弹窗示意多少鸡腿
  181. alert(responseData.message);
  182. //隐藏
  183. new_check.style.display = "none";
  184. } else if (
  185. responseData.message === "今天已完成签到,请勿重复操作"
  186. ) {
  187. //存下时间
  188. GM_setValue("checkInTime", timeNow);
  189. alert(responseData.message);
  190. //隐藏
  191. new_check.style.display = "none";
  192. } else {
  193. console.error("Error in checkIn:", error);
  194. }
  195. });
  196. };
  197. }
  198. } else {
  199. //返回已经签到按钮或隐藏
  200. }
  201. }
  202.  
  203. // 签到
  204. async function getChicken(random) {
  205. const url = "https://www.nodeseek.com/api/attendance";
  206. const data = {
  207. random: random,
  208. };
  209. try {
  210. const responseData = await postData(url, data);
  211. return responseData;
  212. } catch (error) {
  213. console.error("Error in getChicken:", error);
  214. return null;
  215. }
  216. }
  217.  
  218. //查看帖子中的回复消息
  219. initializePage();
  220.  
  221. // 定义一个函数来发送GET请求
  222. async function loadData(page) {
  223. const url = `https://www.nodeseek.com/api/statistics/list-collection?page=${page}`;
  224. try {
  225. const response = await fetch(url);
  226. if (!response.ok) {
  227. throw new Error(`HTTP error! status: ${response.status}`);
  228. }
  229. const data = await response.json();
  230. return data;
  231. } catch (error) {
  232. console.error("Error:", error);
  233. return null;
  234. }
  235. }
  236. async function loadUntilEmpty(page = 1) {
  237. //收藏列表数组
  238. let allCollectionData = [];
  239. while (true) {
  240. const data = await loadData(page);
  241. data.collections.forEach((item) => {
  242. // 将获取到的数据加到数组中
  243. allCollectionData.push(item.post_id);
  244. });
  245. // 如果没有获取到数据或获取到的数据为空,停止加载
  246. if (!data || data.collections.length === 0) {
  247. //丢进去方便存取
  248. GM_setValue("allCollectionData", allCollectionData);
  249. break;
  250. }
  251. page++;
  252. }
  253. }
  254.  
  255. function initializePage() {
  256. let lists = document.querySelectorAll(".post-list");
  257. lists.forEach((list) => {
  258. let items = list.childNodes;
  259. items.forEach((element) => {
  260. setupPostItem(element);
  261. });
  262. });
  263. }
  264.  
  265. function setupPostItem(element) {
  266. let post_item = element.querySelector(".post-title>a");
  267. let new_div = document.createElement("span");
  268. if(mobileStatus){
  269. new_div.className = "info-triganle-mobile";
  270. }else{
  271. new_div.className = "info-triganle";
  272. }
  273. new_div.innerHTML = '<span class="triangle">▼</span>';
  274. element.querySelector(".post-info").append(new_div);
  275. setupCursorStyle(new_div);
  276. new_div.onclick = function () {
  277. if(GM_getValue("contentTime") + 500 >= Date.now()){
  278. console.warn("请勿重复点击");
  279. }else{
  280. GM_setValue("contentTime",Date.now());
  281. GM_setValue("replyNum",element.querySelector(".info-item.info-comments-count > span").innerText);
  282. togglePostContent(post_item, element, new_div);
  283. }
  284. };
  285. }
  286.  
  287. function togglePostContent(post_item, element, new_div) {
  288. let id = post_item.href.replace("https://www.nodeseek.com", "");
  289. let content = document.getElementById(id);
  290. if (content) {
  291. toggleDisplay(content, new_div);
  292. } else {
  293. new_div.firstElementChild.innerText = "○";
  294. document.body.style.cursor = "wait";
  295. new_div.firstElementChild.className = "content-loaded";
  296. fetchContent(post_item.href, element, (contents, targetEle) => {
  297. insertContentAfter(contents, targetEle);
  298. loadNextPage(contents, targetEle, 1);
  299. new_div.firstElementChild.innerText = "▲";
  300. document.body.style.cursor = "auto";
  301. });
  302. }
  303. }
  304. //显隐箭头
  305. function toggleDisplay(content, new_div) {
  306. if (content.style.display === "none") {
  307. content.style.display = "block";
  308. new_div.firstElementChild.innerText = "▲";
  309. } else {
  310. content.style.display = "none";
  311. new_div.firstElementChild.innerText = "▼";
  312. }
  313. }
  314. //获取div框中的内容
  315. //TODO:重构一下这坨狗屎
  316. function fetchContent(url, targetEle, callback) {
  317. const xhr = new XMLHttpRequest();
  318. xhr.open("GET", url, true);
  319. xhr.onload = function () {
  320. if (xhr.status === 200) {
  321. const tempContainer = document.createElement("div");
  322. tempContainer.innerHTML = xhr.responseText;
  323. let contents = document.createElement("div");
  324. contents.id = url.replace("https://www.nodeseek.com", "");
  325. contents.className = "content-div";
  326.  
  327. //content
  328. let post_contents = tempContainer.querySelectorAll(".post-content");
  329. contents.innerHTML += '<div class="post-content-box"></div>';
  330. //menu
  331. let colloct = contents.firstChild;
  332. colloct.innerHTML +=
  333. //TODO:免费鸡腿和like处理
  334. '<div data-v-372de460="" class="comment-menu">' +
  335. // +'<div data-v-372de460="" title="加鸡腿" class="menu-item"><svg data-v-372de460="" class="iconpark-icon"><use data-v-372de460="" href="#chicken-leg"></use></svg><span data-v-372de460="">0</span></div>'
  336. // +'<div data-v-372de460="" title="反对" class="menu-item"><svg data-v-372de460="" class="iconpark-icon"><use data-v-372de460="" href="#bad-one"></use></svg><span data-v-372de460="">0</span></div> '
  337. '<div data-v-372de460="" title="收藏" class="menu-item"><svg data-v-372de460="" class="iconpark-icon"><use data-v-372de460="" href="#star-6negdgdk"></use></svg></div>' +
  338. // +'<div data-v-372de460="" class="menu-item"><svg data-v-372de460="" class="iconpark-icon"><use data-v-372de460="" href="#quote"></use></svg><span data-v-372de460="">引用</span></div> <!----> '
  339. // +'<div data-v-372de460="" class="menu-item"><svg data-v-372de460="" class="iconpark-icon"><use data-v-372de460="" href="#back"></use></svg><span data-v-372de460="">回复</span></div> <!---->
  340. "</div>";
  341. //找收藏的id
  342. let regex = /\/post-(\d+)-1/;
  343. let match = contents.id.match(regex);
  344. //先将就着用
  345. let post_id;
  346. let is_collected;
  347. let icon;
  348. icon = colloct.firstElementChild.querySelector(".menu-item");
  349. if (match != null) {
  350. //当前展开的post的id
  351. post_id = match[1];
  352. post_id = parseInt(post_id);
  353. is_collected = GM_getValue("allCollectionData").some(
  354. (item) => item === post_id
  355. );
  356. //判断是否已收藏
  357. if (is_collected) {
  358. icon.style.color = "red";
  359. }
  360. }
  361. setupCursorStyle(icon);
  362. icon.onclick = function () {
  363. colloctContent(post_id, colloct);
  364. };
  365. post_contents.forEach((e) => {
  366. contents.firstChild.appendChild(e.parentElement);
  367. });
  368.  
  369. if (callback && typeof callback === "function") {
  370. callback(contents, targetEle);
  371. }
  372. } else {
  373. return;
  374. }
  375. };
  376. xhr.onerror = function () {
  377. console.error("Network error occurred while loading content.");
  378. };
  379. xhr.send();
  380. }
  381. //帖子的收藏处理
  382. function colloctContent(post_id, colloct) {
  383. let icon = colloct.firstElementChild.querySelector(".menu-item");
  384. if (icon.style.color === "red") {
  385. //取消收藏处理
  386. let result = confirm("您确定要取消收藏吗?");
  387. if (result) {
  388. collection_del("remove", post_id).then((success) => {
  389. if (success === true) {
  390. icon.style.color = "";
  391. //维护一个全部的收藏id列表
  392. loadUntilEmpty();
  393. }
  394. });
  395. }
  396. } else {
  397. //收藏帖子
  398. collection_add("add", post_id).then((success) => {
  399. if (success === true) {
  400. icon.style.color = "red";
  401. //维护一个全部的收藏id列表
  402. loadUntilEmpty();
  403. }
  404. });
  405. }
  406. }
  407. //收藏方法
  408. async function collection_add(action_type, post_id) {
  409. const url = "https://www.nodeseek.com/api/statistics/collection";
  410. const data = {
  411. action: action_type,
  412. postId: post_id,
  413. };
  414. try {
  415. const responseData = await postData(url, data);
  416. if (responseData && responseData.success === true) {
  417. alert("收藏成功!");
  418. }
  419. // else if (responseData && responseData.success === false) {
  420. // alert("你已经收藏过了!");
  421. // }
  422. return responseData ? responseData.success : null;
  423. } catch (error) {
  424. console.error("Error in collection_add:", error);
  425. return null;
  426. }
  427. }
  428.  
  429. //取消收藏方法
  430. async function collection_del(action_type, post_id) {
  431. const url = "https://www.nodeseek.com/api/statistics/collection";
  432. const data = {
  433. action: action_type,
  434. postId: post_id,
  435. };
  436. try {
  437. const responseData = await postData(url, data);
  438. if (responseData && responseData.success === true) {
  439. alert("取消收藏成功!");
  440. }
  441. return responseData ? responseData.success : null;
  442. } catch (error) {
  443. console.error("Error in collection_del:", error);
  444. return null;
  445. }
  446. }
  447.  
  448. //POST请求
  449. async function postData(url = "", data = {}) {
  450. try {
  451. const response = await fetch(url, {
  452. method: "POST",
  453. headers: {
  454. "Content-Type": "application/json",
  455. },
  456. body: JSON.stringify(data),
  457. });
  458. const responseData = await response.json();
  459. return responseData;
  460. } catch (error) {
  461. console.error("Error in postData:", error);
  462. }
  463. }
  464.  
  465. function insertContentAfter(content, targetEle) {
  466. let ul = targetEle.parentNode;
  467. ul.insertBefore(content, targetEle.nextSibling);
  468. }
  469.  
  470. function loadNextPage(contentDiv, targetEle, currentPage) {
  471. if(GM_getValue("replyNum") / 10 <= currentPage){
  472. return;
  473. }
  474. let nextPage = currentPage + 1;
  475. let nextPageUrl = targetEle
  476. .querySelector(".post-title>a")
  477. .href.replace(/(\d+)$/, nextPage);
  478. fetchContent(nextPageUrl, targetEle, (nextContents, targetEle) => {
  479. let postContentBox = contentDiv.querySelector(".post-content-box");
  480. if (nextContents.querySelector(".post-content")) {
  481. let nextPostContents = nextContents.querySelectorAll(".post-content");
  482. nextPostContents.forEach((e) => {
  483. postContentBox.appendChild(e.parentElement);
  484. });
  485. // 递归调用以加载后续页面,延迟1秒
  486. setTimeout(() => {
  487. loadNextPage(contentDiv, targetEle, nextPage);
  488. }, 1000);
  489. }
  490. });
  491. }
  492.  
  493. function setupCursorStyle(element) {
  494. element.addEventListener("mouseover", function () {
  495. document.body.style.cursor = "pointer";
  496. });
  497. element.addEventListener("mouseout", function () {
  498. document.body.style.cursor = "auto";
  499. });
  500. }
  501.  
  502. let css = `
  503. .content-div {
  504. height: 600px;
  505. padding: 20px;
  506. margin: 10px auto;
  507. border: 1px solid gray;
  508. border-radius: 10px;
  509. overflow: scroll;
  510. }
  511.  
  512. .post-content-box {
  513. border-bottom: 2px dashed gray;
  514. padding-bottom: 10px;
  515. margin-bottom: 10px;
  516. }
  517.  
  518. .triangle {
  519. font-size: medium;
  520. color: gray;
  521. }
  522. .info-triganle{
  523. position: absolute;
  524. right: 54px;
  525. }
  526. .info-triganle-mobile{
  527. position: absolute;
  528. }
  529. .content-loaded {
  530. font-size: medium;
  531. color: red;
  532. }
  533. ::-webkit-scrollbar {
  534. width: 6px;
  535. height: 6px;
  536. }
  537. ::-webkit-scrollbar-track {
  538. border-radius: 3px;
  539. background: rgba(0,0,0,0.06);
  540. -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.08);
  541. }
  542. ::-webkit-scrollbar-thumb {
  543. border-radius: 3px;
  544. background: rgba(0,0,0,0.12);
  545. -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
  546. }
  547.  
  548. `;
  549. GM_addStyle(css);
  550. })();