NodeSeek X

【原NodeSeek增强】自动签到、无缝翻页帖子评论、快捷回复、代码高亮、屏蔽用户、屏蔽帖子、楼主低等级提醒

目前为 2024-05-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NodeSeek X
  3. // @namespace http://www.nodeseek.com/
  4. // @version 0.3-beta.8
  5. // @description 【原NodeSeek增强】自动签到、无缝翻页帖子评论、快捷回复、代码高亮、屏蔽用户、屏蔽帖子、楼主低等级提醒
  6. // @author dabao
  7. // @match *://www.nodeseek.com/*
  8. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACz0lEQVR4Ae3B32tVdQAA8M85u7aVHObmzJVD0+ssiphstLEM62CBlCBEIAYhUoGGD/kiRUo+9CIEElFZgZJFSApBVhCUX2WFrVQKf5Qy26SgdK4pN7eZu+cbtyfJ/gLx83HD9SAhlEyXupiPhUSTeonRfNw1ws2aRJeN5jHcolFhJJ9M8Zj99piDTnv12SjzfzIb9dmrC7Pttt8ykjDVLsu8ZZ1GH1oqeDofJLtJh4fMEw3Y72jlCuEO2+W+sNJFr3vOZ1YIi8NIGA29hDWhGgZDJ2Rt2ZvZSBazmMUsZsPZ1qwVQmcYDNWwhtAbRsNIWJx6WLPDfgxNVkm9nR8hm+XduLba7F9RtcXztmUzyY/YJrUqNPvBYc0eSS3CwXxMl4WG7CarsyEuvU2HOkRNujSw3PosxR6DFurKxx3E/akFohPo0aDfEO61os5LdrtLVWG1TzxokifdiSH9GnTjuGhBqsWE39GOo3kVi8wsmeVW00SJ200zA9r0kFcdQzv+MKElVW/S+L5EE86pmUth3BV/SzCOCUjMVXMWzfsSYybVl1SlSlESkagpuOI1nzshFX1gyAF1UKhJEKOkJFVNXVBv+pJoBK1qBkh86z1/SaR+9o5zEgoDaloxsiSart6F1Bkl83ESHWEKvvEbqZJETaokgSH9hCk6cBLtSs6kDqEb/cZ0K+MnO0X/VdhRGUBZjzH9uA+HUl+a0BvmO+J7bVZSKWz1kehqhfe9oWalNoccDmW9JnyV+toxsy3PK3aY9Gx4gMp567ziV4WawpCXra+MEhZ5xqTtecVycxzXlxA22OK4ZYbt9LjvrM5PkNUp6zVPdNpBv1QKwt126Paxp8zwqXu8kG8pYZdHlT2Rvxo2aVG2ObyYn65UnXLKVULZZrP02ZRfCms1OmAXCSHRYqrLzuZFaDFV6s/8omuERs0Kl/LzITVTvTHDeXTD9eAftAsSYhXYOWUAAAAASUVORK5CYII=
  9. // @require https://cdn.staticfile.net/layui/2.9.8/layui.min.js
  10. // @resource highlightStyle https://cdn.staticfile.net/highlight.js/11.9.0/styles/atom-one-light.min.css
  11. // @resource highlightStyle_dark https://cdn.staticfile.net/highlight.js/11.9.0/styles/atom-one-dark.min.css
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_deleteValue
  16. // @grant GM_notification
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_unregisterMenuCommand
  19. // @grant GM_getResourceURL
  20. // @grant GM_addElement
  21. // @grant GM_addStyle
  22. // @grant GM_openInTab
  23. // @grant unsafeWindow
  24. // @run-at document-end
  25. // @license GPL-3.0 License
  26. // @supportURL https://www.nodeseek.com/post-36263-1
  27. // @homepageURL https://www.nodeseek.com/post-36263-1
  28. // ==/UserScript==
  29.  
  30. (function () {
  31. 'use strict';
  32.  
  33. const { version, author, name, icon } = GM_info.script;
  34.  
  35. const BASE_URL = "https://www.nodeseek.com";
  36.  
  37. const util = {
  38. clog(c) {
  39. console.group(`%c %c [${name}]-v${version} by ${author}`, `background:url(${icon}) center center no-repeat;background-size:12px;padding:3px`, "");
  40. console.log(c);
  41. console.groupEnd();
  42. },
  43. getValue(name) {
  44. return GM_getValue(name);
  45. },
  46. setValue(name, value) {
  47. GM_setValue(name, value);
  48. },
  49. sleep(time) {
  50. return new Promise((resolve) => setTimeout(resolve, time));
  51. },
  52. addStyle(id, tag, css) {
  53. tag = tag || 'style';
  54. let doc = document, styleDom = doc.head.querySelector(`#${id}`);
  55. if (styleDom) return;
  56. let style = doc.createElement(tag);
  57. style.rel = 'stylesheet';
  58. style.id = id;
  59. tag === 'style' ? style.innerHTML = css : style.href = css;
  60. doc.head.appendChild(style);
  61. },
  62. removeStyle(id, tag) {
  63. tag = tag || 'style';
  64. let doc = document, styleDom = doc.head.querySelector(`#${id}`);
  65. if (styleDom) { doc.head.removeChild(styleDom) };
  66. },
  67. getAttrsByPrefix(element, prefix) {
  68. const attributes = element.attributes;
  69. let matchingAttributes = {};
  70. for (let attribute of attributes) {
  71. const attributeName = attribute.name;
  72. const attributeValue = attribute.value;
  73.  
  74. if (attributeName.startsWith(prefix)) {
  75. matchingAttributes[attributeName] = attributeValue;
  76. }
  77. }
  78. return matchingAttributes;
  79. },
  80. data(element, key, value) {
  81. if (arguments.length < 2) {
  82. return undefined;
  83. }
  84. if (value != undefined) {
  85. element.dataset[key] = value;
  86. }
  87. return element.dataset[key];
  88. },
  89. async post(url, data, headers, responseType = 'json') {
  90. url = !url.startsWith("http") ? BASE_URL + url : url;
  91. return this.fetchData(url, 'POST', data, headers, responseType);
  92. },
  93. async get(url, headers, responseType = 'json') {
  94. url = !url.startsWith("http") ? BASE_URL + url : url;
  95. return this.fetchData(url, 'GET', null, headers, responseType);
  96. },
  97. async fetchData(url, method, data, headers, responseType) {
  98. const options = {
  99. method: method,
  100. headers: headers
  101. };
  102. if (data) {
  103. if (typeof data === 'object') {
  104. data = JSON.stringify(data);
  105. }
  106. options.body = data;
  107. }
  108. const response = await fetch(url, options);
  109. return handleResponse(response, responseType);
  110. async function handleResponse(response, responseType) {
  111. const responseHandlers = {
  112. 'json': () => response.json(),
  113. 'text': () => response.text(),
  114. 'stream': () => response.body,
  115. 'formData': () => response.formData(),
  116. 'arrayBuffer': () => response.arrayBuffer()
  117. };
  118. const handler = responseHandlers[responseType];
  119. if (!handler) {
  120. throw new Error('不支持的响应类型');
  121. }
  122. return await handler();
  123. }
  124. },
  125. getCurrentDate() {
  126. const localTimezoneOffset = (new Date()).getTimezoneOffset();
  127. const beijingOffset = 8 * 60;
  128. const beijingTime = new Date(Date.now() + (localTimezoneOffset + beijingOffset) * 60 * 1000);
  129. const timeNow = `${beijingTime.getFullYear()}/${(beijingTime.getMonth() + 1)}/${beijingTime.getDate()}`;
  130. return timeNow;
  131. },
  132. createElement(tagName, options = {}, childrens = [], doc = document, namespace = null) {
  133. if (Array.isArray(options)) {
  134. if (childrens.length !== 0) {
  135. throw new Error("If options is an array, childrens should not be provided.");
  136. }
  137. childrens = options;
  138. options = {};
  139. }
  140.  
  141. const { staticClass = '', dynamicClass = '', attrs = {}, on = {} } = options;
  142.  
  143. const ele = namespace ? doc.createElementNS(namespace, tagName) : doc.createElement(tagName);
  144.  
  145. if (staticClass) {
  146. staticClass.split(' ').forEach(cls => ele.classList.add(cls.trim()));
  147. }
  148. if (dynamicClass) {
  149. dynamicClass.split(' ').forEach(cls => ele.classList.add(cls.trim()));
  150. }
  151.  
  152. Object.entries(attrs).forEach(([key, value]) => {
  153. if (key === 'style' && typeof value === 'object') {
  154. Object.entries(value).forEach(([styleKey, styleValue]) => {
  155. ele.style[styleKey] = styleValue;
  156. });
  157. } else {
  158. if (value !== undefined) ele.setAttribute(key, value);
  159. }
  160. });
  161.  
  162. Object.entries(on).forEach(([event, handler]) => {
  163. ele.addEventListener(event, handler);
  164. });
  165.  
  166. childrens.forEach(child => {
  167. if (typeof child === 'string') {
  168. child = doc.createTextNode(child);
  169. }
  170. ele.appendChild(child);
  171. });
  172.  
  173. return ele;
  174. }
  175.  
  176. };
  177.  
  178. const opts = {
  179. post: {
  180. pathPattern: /^\/(categories\/|page|award|search|$)/,
  181. scrollThreshold: 200,
  182. nextPagerSelector: '.nsk-pager a.pager-next',
  183. postListSelector: 'ul.post-list',
  184. topPagerSelector: 'div.nsk-pager.pager-top',
  185. bottomPagerSelector: 'div.nsk-pager.pager-bottom',
  186. },
  187. comment: {
  188. pathPattern: /^\/post-/,
  189. scrollThreshold: 690,
  190. nextPagerSelector: '.nsk-pager a.pager-next',
  191. postListSelector: 'ul.comments',
  192. topPagerSelector: 'div.nsk-pager.post-top-pager',
  193. bottomPagerSelector: 'div.nsk-pager.post-bottom-pager',
  194. },
  195. setting: {
  196. SETTING_SIGN_IN_STATUS: 'setting_sign_in_status',
  197. SETTING_SIGN_IN_LAST_DATE: 'setting_sign_in_last_date',
  198. SETTING_SIGN_IN_IGNORE_DATE: 'setting_sign_in_ignore_date',
  199. SETTING_AUTO_LOADING_STATUS: 'setting_auto_loading_status'
  200. }
  201. };
  202. layui.use(function () {
  203. let layer = layui.layer,
  204. $ = layui.jquery;
  205. const message = {
  206. info: (text) => message.__msg(text, { "background-color": "#4D82D6" }),
  207. success: (text) => message.__msg(text, { "background-color": "#57BF57" }),
  208. warning: (text) => message.__msg(text, { "background-color": "#D6A14D" }),
  209. error: (text) => message.__msg(text, { "background-color": "#E1715B" }),
  210. __msg: (text, style) => { let index = layer.msg(text, { offset: 't', area: ['100%', 'auto'], anim: 'slideDown' }); layer.style(index, Object.assign({ opacity: 0.9 }, style)); }
  211. };
  212.  
  213. const main = {
  214. // 初始化配置数据
  215. initValue() {
  216. const value = [
  217. { name: opts.setting.SETTING_SIGN_IN_STATUS, defaultValue: 0 },
  218. { name: opts.setting.SETTING_SIGN_IN_LAST_DATE, defaultValue: '1753/1/1' },
  219. { name: opts.setting.SETTING_SIGN_IN_IGNORE_DATE, defaultValue: '1753/1/1' },
  220. { name: opts.setting.SETTING_AUTO_LOADING_STATUS, defaultValue: 1 }
  221. ];
  222. this.upgradeConfig();
  223. value.forEach((v) => util.getValue(v.name) === undefined && util.setValue(v.name, v.defaultValue));
  224. },
  225. // 升级配置项
  226. upgradeConfig() {
  227. const upgradeConfItem = (oldConfKey, newConfKey) => {
  228. if (util.getValue(oldConfKey) && util.getValue(newConfKey) === undefined) {
  229. util.clog(`升级配置项 ${oldConfKey} ${newConfKey}`);
  230. util.setValue(newConfKey, util.getValue(oldConfKey));
  231. GM_deleteValue(oldConfKey);
  232. }
  233. };
  234. upgradeConfItem('menu_signInTime', opts.setting.SETTING_SIGN_IN_LAST_DATE);
  235. },
  236. loginStatus: false,
  237. //检查是否登陆
  238. checkLogin() {
  239. if (unsafeWindow.meCard && unsafeWindow.meCard.logined) {
  240. this.loginStatus = true;
  241. util.clog(`当前登录用户 ${unsafeWindow.meCard.user.member_name} (ID ${unsafeWindow.meCard.user.member_id})`);
  242. }
  243. },
  244. // 自动签到
  245. autoSignIn(rand) {
  246. if (!this.loginStatus) return
  247. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 0) return;
  248.  
  249. rand = rand || (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 1);
  250.  
  251. let timeNow = util.getCurrentDate(),
  252. timeOld = util.getValue(opts.setting.SETTING_SIGN_IN_LAST_DATE);
  253. if (!timeOld || timeOld != timeNow) { // 是新的一天
  254. util.setValue(opts.setting.SETTING_SIGN_IN_LAST_DATE, timeNow); // 写入签到时间以供后续比较
  255. this.signInRequest(rand);
  256. }
  257. },
  258. // 重新签到
  259. reSignIn() {
  260. if (!this.loginStatus) return;
  261. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) === 0) {
  262. unsafeWindow.mscAlert('提示', this.getMenuStateText(this._menus[0], 0) + ' 状态时不支持重新签到!');
  263. return;
  264. }
  265.  
  266. util.setValue(opts.setting.SETTING_SIGN_IN_LAST_DATE, '1753/1/1');
  267. location.reload();
  268. },
  269. addSignTips() {
  270. if (!this.loginStatus) return
  271. if (util.getValue(opts.setting.SETTING_SIGN_IN_STATUS) !== 0) return;
  272.  
  273. const timeNow = util.getCurrentDate();
  274. const { SETTING_SIGN_IN_IGNORE_DATE, SETTING_SIGN_IN_LAST_DATE } = opts.setting;
  275. const timeIgnore = util.getValue(SETTING_SIGN_IN_IGNORE_DATE);
  276. const timeOld = util.getValue(SETTING_SIGN_IN_LAST_DATE);
  277.  
  278. if (timeNow === timeIgnore || timeNow === timeOld) return;
  279.  
  280. const _this = this;
  281. let tip = util.createElement("div", { staticClass: 'nsplus-tip' });
  282. let tip_p = util.createElement('p');
  283. tip_p.innerHTML = '今天你还没有签到哦!&emsp;【<a class="sign_in_btn" data-rand="true" href="javascript:;">随机抽个鸡腿</a>】&emsp;【<a class="sign_in_btn" data-rand="false" href="javascript:;">只要5个鸡腿</a>】&emsp;【<a id="sign_in_ignore" href="javascript:;">今天不再提示</a>】';
  284. tip.appendChild(tip_p);
  285. tip.querySelectorAll('.sign_in_btn').forEach(function (item) {
  286. item.addEventListener("click", function (e) {
  287. const rand = util.data(this, 'rand');
  288. _this.signInRequest(rand);
  289. tip.remove();
  290. util.setValue(SETTING_SIGN_IN_LAST_DATE, timeNow); // 写入签到时间以供后续比较
  291. })
  292. });
  293. tip.querySelector('#sign_in_ignore').addEventListener("click", function (e) {
  294. tip.remove();
  295. util.setValue(SETTING_SIGN_IN_IGNORE_DATE, timeNow);
  296. });
  297.  
  298. document.querySelector('#nsk-frame').before(tip);
  299. },
  300. signInRequest(rand) {
  301. util.post('/api/attendance?random=' + (rand || false), {}, { "Content-Type": "application/json" }, 'json').then(function (json) {
  302. if (json.success) {
  303. message.success('签到成功!今天午饭+' + json.gain + '个鸡腿; 积攒了' + json.current + '个鸡腿了');
  304. }
  305. else {
  306. message.info(json.message);
  307. }
  308. }).catch(function (err) {
  309. util.clog(err)
  310. });
  311. util.clog(`[${name}] 签到完成`);
  312. },
  313. is_show_quick_comment: false,
  314. quickComment() {
  315. if (!this.loginStatus || !opts.comment.pathPattern.test(location.pathname)) return;
  316. if (util.getValue(opts.setting.SETTING_AUTO_LOADING_STATUS) === 0) return;
  317.  
  318. const _this = this;
  319.  
  320. const onClick = (e) => {
  321. if (_this.is_show_quick_comment) {
  322. return;
  323. }
  324. e.preventDefault();
  325.  
  326. const mdEditor = document.querySelector('.md-editor');
  327. const clientHeight = document.documentElement.clientHeight, clientWidth = document.documentElement.clientWidth;
  328. const mdHeight = mdEditor.clientHeight, mdWidth = mdEditor.clientWidth;
  329. const top = (clientHeight / 2) - (mdHeight / 2), left = (clientWidth / 2) - (mdWidth / 2);
  330. mdEditor.style.cssText = `position: fixed; top: ${top}px; left: ${left}px; margin: 30px 0px; width: 100%; max-width: ${mdWidth}px; z-index: 999;`;
  331. const moveEl = mdEditor.querySelector('.tab-select.window_header');
  332. moveEl.style.cursor = "move";
  333. moveEl.addEventListener('mousedown', startDrag);
  334. addEditorCloseButton();
  335. _this.is_show_quick_comment = true;
  336. };
  337. const commentDiv = document.querySelector('#fast-nav-button-group #back-to-parent').cloneNode(true);
  338. commentDiv.id = 'back-to-comment';
  339. commentDiv.innerHTML = '<svg class="iconpark-icon" style="width: 24px; height: 24px;"><use href="#comments"></use></svg>';
  340. commentDiv.addEventListener("click", onClick);
  341. document.querySelector('#back-to-parent').before(commentDiv);
  342. document.querySelectorAll('div.comment-menu > div:nth-last-child(1),div.comment-menu > div:nth-last-child(2) ').forEach(function (item) { item.addEventListener("click", onClick, true); });
  343.  
  344. function addEditorCloseButton() {
  345. const fullScreenToolbar = document.querySelector('#editor-body .window_header > :last-child');
  346. const cloneToolbar = fullScreenToolbar.cloneNode(true);
  347. cloneToolbar.setAttribute('title', '关闭');
  348. cloneToolbar.querySelector('span').classList.replace('i-icon-full-screen-one', 'i-icon-close');
  349. cloneToolbar.querySelector('span').innerHTML = '<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M8 8L40 40" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 40L40 8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
  350. cloneToolbar.addEventListener("click", function (e) {
  351. const mdEditor = document.querySelector('.md-editor');
  352. mdEditor.style = "";
  353. const moveEl = mdEditor.querySelector('.tab-select.window_header');
  354. moveEl.style.cursor = "";
  355. moveEl.removeEventListener('mousedown', startDrag);
  356.  
  357. this.remove();
  358. _this.is_show_quick_comment = false;
  359. });
  360. fullScreenToolbar.after(cloneToolbar);
  361. }
  362. function startDrag(event) {
  363. if (event.button !== 0) return;
  364.  
  365. const draggableElement = document.querySelector('.md-editor');
  366. const parentMarginTop = parseInt(window.getComputedStyle(draggableElement).marginTop);
  367. const initialX = event.clientX - draggableElement.offsetLeft;
  368. const initialY = event.clientY - draggableElement.offsetTop + parentMarginTop;
  369. document.onmousemove = function (event) {
  370. const newX = event.clientX - initialX;
  371. const newY = event.clientY - initialY;
  372. draggableElement.style.left = newX + 'px';
  373. draggableElement.style.top = newY + 'px';
  374. };
  375. document.onmouseup = function () {
  376. document.onmousemove = null;
  377. document.onmouseup = null;
  378. };
  379. }
  380. },
  381.  
  382. //新窗口打开帖子
  383. openPostInNewTab() {
  384. if (!opts.post.pathPattern.test(location.pathname)) return;
  385. if (document.querySelector('a[href^="/post-"]')) {
  386. document.querySelectorAll('a[href^="/post-"]').forEach(function (item) {
  387. if (item.classList.contains("pager-prev") || item.classList.contains("pager-pos") || item.classList.contains("pager-next")) {
  388. return;
  389. }
  390. item.target || (item.target = "_blank");
  391. });
  392. }
  393. },
  394. //自动点击跳转页链接
  395. autoJump() {
  396. if (!/^\/jump/.test(location.pathname)) return;
  397. document.querySelector('.btn').click();
  398. },
  399. blockPost(ele) {
  400. ele = ele || document;
  401. ele.querySelectorAll('.post-title>a[href]').forEach(function (item) {
  402. if (item.textContent.toLowerCase().includes("__keys__")) {
  403. item.closest(".post-list-item").classList.add('blocked-post')
  404. }
  405. });
  406. },
  407. //屏蔽用户
  408. blockMemberDOMInsert() {
  409. if (!this.loginStatus) return;
  410.  
  411. const _this = this;
  412. Array.from(document.querySelectorAll(".post-list .post-list-item,.content-item")).forEach((function (t, n) {
  413. var r = t.querySelector('.avatar-normal');
  414. r.addEventListener("click", (function (n) {
  415. n.preventDefault();
  416. let intervalId = setInterval(async () => {
  417. const userCard = document.querySelector('div.user-card.hover-user-card');
  418. const pmButton = document.querySelector('div.user-card.hover-user-card a.btn');
  419. if (userCard && pmButton) {
  420. clearInterval(intervalId);
  421. const dataVAttrs = util.getAttrsByPrefix(userCard, 'data-v');
  422. const userName = userCard.querySelector('a.Username').textContent;
  423. dataVAttrs.style = "float:left; background-color:rgba(0,0,0,.3)";
  424. const blockBtn = util.createElement("a", {
  425. staticClass: "btn", attrs: dataVAttrs, on: {
  426. click: function (e) {
  427. e.preventDefault();
  428. unsafeWindow.mscConfirm(`确定要屏蔽“${userName}”吗?`, '你可以在本站的 设置=>屏蔽用户 中解除屏蔽', function () { blockMember(userName); })
  429. }
  430. }
  431. }, ["屏蔽"]);
  432. pmButton.after(blockBtn);
  433. }
  434. }, 50);
  435. }))
  436. }))
  437. function blockMember(userName) {
  438. util.post("/api/block-list/add", { "block_member_name": userName }, { "Content-Type": "application/json" }, '').then(function (data) {
  439. if (data.success) {
  440. let msg = '屏蔽用户【' + userName + '】成功!';
  441. unsafeWindow.mscAlert(msg);
  442. util.clog(msg);
  443. } else {
  444. let msg = '屏蔽用户【' + userName + '】失败!' + data.message;
  445. unsafeWindow.mscAlert(msg);
  446. util.clog(msg);
  447. }
  448. }).catch(function (err) {
  449. util.clog(err);
  450. });
  451. }
  452. },
  453. addImageSlide() {
  454. if (!opts.comment.pathPattern.test(location.pathname)) return;
  455.  
  456. const posts = document.querySelectorAll('article.post-content');
  457. posts.forEach(function (post, i) {
  458. const images = post.querySelectorAll('img:not(.sticker)');
  459. if (images.length === 0) return;
  460.  
  461. images.forEach(function (image, i) {
  462. const newImg = image.cloneNode(true);
  463. image.parentNode.replaceChild(newImg, image);
  464. newImg.addEventListener('click', function (e) {
  465. e.preventDefault();
  466. const imgArr = Array.from(post.querySelectorAll('img:not(.sticker)'));
  467. const clickedIndex = imgArr.indexOf(this);
  468. const photoData = imgArr.map((img, i) => ({ alt: img.alt, pid: i + 1, src: img.src }));
  469. layer.photos({ photos: { "title": "图片预览", "start": clickedIndex, "data": photoData } });
  470. }, true);
  471. });
  472. });
  473. },
  474. addLevelTag() {//添加等级标签
  475. if (!this.loginStatus) return;
  476. if (!opts.comment.pathPattern.test(location.pathname)) return;
  477.  
  478. this.getUserInfo(unsafeWindow.__config__.postData.op.uid).then((user) => {
  479. let warningInfo = '';
  480. const daysDiff = Math.floor((new Date() - new Date(user.created_at)) / (1000 * 60 * 60 * 24));
  481. if (daysDiff < 30) {
  482. warningInfo = `⚠️`;
  483. }
  484. console.log(user);
  485. const span = util.createElement("span", { staticClass: `nsk-badge role-tag user-level user-lv${user.rank}`, on: { mouseenter: function (e) { layer.tips(`注册 <span class="layui-badge">${daysDiff}</span> 天;帖子 ${user.nPost};评论 ${user.nComment}`, this, { tips: 3, time: 0 }); }, mouseleave: function (e) { layer.closeAll(); } } }, [util.createElement("span", [`${warningInfo}Lv ${user.rank}`])]);
  486.  
  487. const authorLink = document.querySelector('#nsk-body .nsk-post .nsk-content-meta-info .author-info>a');
  488. if (authorLink != null) {
  489. authorLink.after(span);
  490. }
  491. });
  492. },
  493. getUserInfo(uid) {
  494. return new Promise((resolve, reject) => {
  495. util.get(`/api/account/getInfo/${uid}`, {}, 'json').then((data) => {
  496. if (!data.success) {
  497. util.clog(data);
  498. return;
  499. }
  500. resolve(data.detail);
  501. }).catch((err) => reject(err));
  502. })
  503. },
  504. userCardEx() {
  505. if (!this.loginStatus) return;
  506.  
  507. const updateNotificationElement = (element, href, iconHref, text, count) => {
  508. element.querySelector("a").setAttribute("href", `${href}`);
  509. element.querySelector("a > svg > use").setAttribute("href", `${iconHref}`)
  510. element.querySelector("a > :nth-child(2)").textContent = `${text} `;
  511. element.querySelector("a > :last-child").textContent = count;
  512. if (count > 0) {
  513. element.querySelector("a > :last-child").classList.add("notify-count");
  514. }
  515. return element;
  516. };
  517.  
  518. const userCard = document.querySelector(".user-card .user-stat");
  519. const lastElement = userCard.querySelector(".stat-block:first-child > :last-child");
  520. const unViewedCount = unsafeWindow.__config__.user.unViewedCount;
  521.  
  522. if (lastElement.querySelector("a > .notify-count:last-child")) {
  523. lastElement.querySelector("a > .notify-count:last-child").classList.remove("notify-count");
  524. }
  525.  
  526. const atMeElement = lastElement.cloneNode(true);
  527. updateNotificationElement(atMeElement, "/notification#/atMe", "#at-sign", "我", unViewedCount.atMe);
  528. lastElement.after(atMeElement);
  529.  
  530. const msgElement = lastElement.cloneNode(true);
  531. updateNotificationElement(msgElement, "/notification#/message?mode=list", "#envelope-one", "私信", unViewedCount.message);
  532. userCard.querySelector(".stat-block:last-child").append(msgElement);
  533.  
  534. updateNotificationElement(lastElement, "/notification#/reply", "#remind-6nce9p47", "回复", unViewedCount.reply);
  535. },
  536. // 自动翻页
  537. autoLoading() {
  538. if (util.getValue(opts.setting.SETTING_AUTO_LOADING_STATUS) === 0) return;
  539.  
  540. let opt = {};
  541. if (opts.post.pathPattern.test(location.pathname)) { opt = opts.post; }
  542. else if (opts.comment.pathPattern.test(location.pathname)) { opt = opts.comment; }
  543. else { return; }
  544. let is_requesting = false;
  545. let _this = this;
  546. this.windowScroll(function (direction, e) {
  547. if (direction === 'down') { // 下滑才准备翻页
  548. let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
  549. if (document.documentElement.scrollHeight <= document.documentElement.clientHeight + scrollTop + opt.scrollThreshold && !is_requesting) {
  550. if (!document.querySelector(opt.nextPagerSelector)) return;
  551. let nextUrl = document.querySelector(opt.nextPagerSelector).attributes.href.value;
  552. is_requesting = true;
  553. util.get(nextUrl, {}, 'text').then(function (data) {
  554. let doc = new DOMParser().parseFromString(data, "text/html");
  555. _this.blockPost(doc);//过滤帖子
  556. document.querySelector(opt.postListSelector).append(...doc.querySelector(opt.postListSelector).childNodes);
  557. document.querySelector(opt.topPagerSelector).innerHTML = doc.querySelector(opt.topPagerSelector).innerHTML;
  558. document.querySelector(opt.bottomPagerSelector).innerHTML = doc.querySelector(opt.bottomPagerSelector).innerHTML;
  559. history.pushState(null, null, nextUrl);
  560. is_requesting = false;
  561. }).catch(function (err) {
  562. is_requesting = false;
  563. util.clog(err);
  564. });
  565. }
  566. }
  567. });
  568. },
  569. // 滚动条事件
  570. windowScroll(fn1) {
  571. let beforeScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop,
  572. fn = fn1 || function () { };
  573. setTimeout(function () { // 延时执行,避免刚载入到页面就触发翻页事件
  574. window.addEventListener('scroll', function (e) {
  575. const afterScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop,
  576. delta = afterScrollTop - beforeScrollTop;
  577. if (delta == 0) return false;
  578. fn(delta > 0 ? 'down' : 'up', e);
  579. beforeScrollTop = afterScrollTop;
  580. }, false);
  581. }, 1000)
  582. },
  583. switchMultiState(stateName, states) {//多态顺序切换
  584. let currState = util.getValue(stateName);
  585. currState = (currState + 1) % states.length;
  586. util.setValue(stateName, currState);
  587. this.registerMenus();
  588. },
  589. getMenuStateText(menu, stateVal) {
  590. return `${menu.states[stateVal].s1} ${menu.text}(${menu.states[stateVal].s2})`;
  591. },
  592. _menus: [
  593. { name: opts.setting.SETTING_SIGN_IN_STATUS, callback: (name, states) => main.switchMultiState(name, states), accessKey: '', text: '自动签到', states: [{ s1: '❌', s2: '关闭' }, { s1: '🎲', s2: '随机🍗' }, { s1: '📌', s2: '5个🍗' }], autoClose: false },
  594. { name: 're_sign_in', callback: (name, states) => main.reSignIn(), accessKey: '', text: '🔂 重新签到', states: [] },
  595. { name: opts.setting.SETTING_AUTO_LOADING_STATUS, callback: (name, states) => main.switchMultiState(name, states), accessKey: '', text: '无缝加载', states: [{ s1: '❌', s2: '关闭' }, { s1: '✅', s2: '开启' }] },
  596. { name: 'advanced_settings', callback: (name, states) => main.advancedSettings(), accessKey: '', text: '⚙️ 高级设置', states: [] },
  597. { name: 'feedback', callback: (name, states) => GM_openInTab('https://greasyfork.org/zh-CN/scripts/479426/feedback', { active: true, insert: true, setParent: true }), accessKey: '', text: '💬 反馈 & 建议', states: [] }
  598. ],
  599. _menuIds: [],
  600. registerMenus() {
  601. this._menuIds.forEach(function (id) {
  602. GM_unregisterMenuCommand(id);
  603. });
  604. this._menuIds = [];
  605.  
  606. const _this = this;
  607. this._menus.forEach(function (menu) {
  608. let k = menu.text;
  609. if (menu.states.length > 0) {
  610. k = _this.getMenuStateText(menu, util.getValue(menu.name));
  611. }
  612. let autoClose = menu.hasOwnProperty('autoClose') ? menu.autoClose : true;
  613. let menuId = GM_registerMenuCommand(k, function () { menu.callback(menu.name, menu.states) }, { autoClose: autoClose });
  614. menuId = menuId || k;
  615. _this._menuIds.push(menuId);
  616. });
  617. },
  618. advancedSettings() {
  619. let layerWidth = layui.device().mobile ? '100%' : '620px';
  620. layer.open({
  621. type: 1,
  622. offset: 'r',
  623. anim: 'slideLeft', // 从右往左
  624. area: [layerWidth, '100%'],
  625. scrollbar: false,
  626. shade: 0.1,
  627. shadeClose: false,
  628. btn: ["保存设置"],
  629. btnAlign: 'l',
  630. title: 'NodeSeek X 设置',
  631. id: 'setting-layer-direction-r',
  632. content: `<div class="layui-row" style="display:flex;height:100%">
  633. <div class="layui-panel layui-col-xs3 layui-col-sm3 layui-col-md3" id="demo-menu">
  634. <ul class="layui-menu" lay-filter="demo"></ul>
  635. </div>
  636. <div class="layui-col-xs9 layui-col-sm9 layui-col-md9" style="overflow-y: auto; padding-left: 10px" id="demo-content">
  637. <fieldset id="group1" class="layui-elem-field layui-field-title">
  638. <legend>基本设置</legend>
  639. </fieldset>
  640. <div style="height: 500px;">Content for Group 1</div>
  641. <fieldset id="group2" class="layui-elem-field layui-field-title">
  642. <legend>扩展设置</legend>
  643. </fieldset>
  644. <div style="height: 500px;">Content for Group 2</div>
  645. <fieldset id="group3" class="layui-elem-field layui-field-title">
  646. <legend>实验设置</legend>
  647. </fieldset>
  648. <div style="height: 500px;">Content for Group 3</div>
  649. </div>
  650. </div>
  651. <script>
  652. document.querySelectorAll('#demo-content > fieldset').forEach(function (el, i) {
  653. let li = document.createElement('li');
  654. if (i === 0) li.classList = 'layui-menu-item-checked';
  655. let div = document.createElement('div');
  656. div.classList = 'layui-menu-body-title';
  657. let a = document.createElement('a');
  658. a.href = '#' + el.id;
  659. a.textContent = el.textContent;
  660. a.addEventListener('click', aClick);
  661. li.append(div);
  662. div.append(a);
  663. document.querySelector('#demo-menu>ul').append(li);
  664. });
  665. const docContent = document.querySelector('#demo-content');
  666. docContent.addEventListener('scroll', function (e) {
  667. var scrollPos = docContent.scrollTop;
  668. console.log(scrollPos);
  669. docContent.querySelectorAll('fieldset').forEach(function (el) {
  670. var topPos = el.offsetTop - 10;
  671. if (scrollPos >= topPos) {
  672. var id = el.getAttribute('id');
  673. document.querySelectorAll('.layui-menu > li.layui-menu-item-checked').forEach(function (navItem) {
  674. navItem.classList.remove('layui-menu-item-checked');
  675. });
  676. var navItem = document.querySelector('.layui-menu > li a[href="#' + id + '"]').closest('li');
  677. navItem.classList.add('layui-menu-item-checked');
  678. }
  679. });
  680. });
  681. function aClick(e) {
  682. e.preventDefault();
  683. var id = this.getAttribute('href');
  684. var target = document.querySelector(id);
  685. docContent.scrollTo({
  686. top: target.offsetTop - 10,
  687. // behavior: 'smooth'
  688. });
  689. }
  690. <\/script>`,
  691. yes: function (index, layero, that) {
  692. layer.msg('111');
  693. layer.close(index); // 关闭弹层
  694. }
  695. });
  696. },
  697. addCodeHighlight() {
  698. const codes = document.querySelectorAll(".post-content pre code");
  699. if (codes) {
  700. codes.forEach(function (code) {
  701. const copyBtn = util.createElement("span", { staticClass: "copy-code", attrs: { title: "复制代码" }, on: { click: copyCode } }, [util.createElement("svg", { staticClass: 'iconpark-icon' }, [util.createElement("use", { attrs: { href: "#copy" } }, [], document, "http://www.w3.org/2000/svg")], document, "http://www.w3.org/2000/svg")]);
  702. code.after(copyBtn);
  703. });
  704. }
  705. function copyCode(e) {
  706. const pre = this.closest('pre');
  707. const selection = window.getSelection();
  708. const range = document.createRange();
  709. range.selectNodeContents(pre.querySelector("code"));
  710. selection.removeAllRanges();
  711. selection.addRange(range);
  712. document.execCommand('copy');
  713. selection.removeAllRanges();
  714. updateCopyButton(this);
  715. layer.tips(`复制成功`, this, { tips: 4, time: 1000 })
  716. }
  717. function updateCopyButton(ele) {
  718. ele.querySelector("use").setAttribute("href", "#check");
  719. util.sleep(1000).then(() => ele.querySelector("use").setAttribute("href", "#copy"));
  720. }
  721. },
  722. addPluginStyle() {
  723. let style = `
  724. .nsplus-tip { background-color: rgba(255, 217, 0, 0.8); border: 0px solid black; padding: 10px; text-align: center;animation: blink 5s cubic-bezier(.68,.05,.46,.96) infinite;}
  725. /* @keyframes blink{ 0%{background-color: red;} 25%{background-color: yellow;} 50%{background-color: blue;} 75%{background-color: green;} 100%{background-color: red;} } */
  726. .nsplus-tip p,.nsplus-tip p a { color: #f00 }
  727. .nsplus-tip p a:hover {color: #0ff}
  728. #back-to-comment{display:flex;}
  729. #fast-nav-button-group .nav-item-btn:nth-last-child(4){bottom:120px;}
  730. body.light-layout .post-list .post-title a:visited{color:#681da8}
  731. body.dark-layout .post-list .post-title a:visited {color:#999}
  732. .role-tag.user-level.user-lv0 {background-color: rgb(199 194 194); border: 1px solid rgb(199 194 194); color: #fafafa;}
  733. .role-tag.user-level.user-lv1 {background-color: #ff9400; border: 1px solid #ff9400; color: #fafafa;}
  734. .role-tag.user-level.user-lv2 {background-color: #ff9400; border: 1px solid #ff9400; color: #fafafa;}
  735. .role-tag.user-level.user-lv3 {background-color: #ff3a55; border: 1px solid #ff3a55; color: #fafafa;}
  736. .role-tag.user-level.user-lv4 {background-color: #ff3a55; border: 1px solid #ff3a55; color: #fafafa;}
  737. .role-tag.user-level.user-lv5 {background-color: #de00ff; border: 1px solid #de00ff; color: #fafafa;}
  738. .role-tag.user-level.user-lv6 {background-color: #de00ff; border: 1px solid #de00ff; color: #fafafa;}
  739. .role-tag.user-level.user-lv7 {background-color: #ff0000; border: 1px solid #ff0000; color: #fafafa;}
  740. .role-tag.user-level.user-lv8 {background-color: #3478f7; border: 1px solid #3478f7; color: #fafafa;}
  741.  
  742. .post-content pre { position: relative; }
  743. .post-content pre span.copy-code { position: absolute; right: .5em; top: .5em; cursor: pointer;color: #c1c7cd; }
  744. .post-content pre .iconpark-icon {width:16px;height:16px;margin:3px;}
  745. .post-content pre .iconpark-icon:hover {color:var(--link-hover-color)}
  746. .dark-layout .post-content pre code.hljs { padding: 1em !important; }
  747. `;
  748. if (document.head) {
  749. util.addStyle('nsplus-style', 'style', style);
  750. util.addStyle('layui-style', 'link', 'https://img0.520912.xyz/images/layui/css/layui.css');
  751. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle"));
  752. }
  753. },
  754. addPluginScript() {
  755. GM_addElement(document.body, 'script', {
  756. src: 'https://cdn.staticfile.net/highlight.js/11.9.0/highlight.min.js'
  757. });
  758. GM_addElement(document.body, 'script', {
  759. textContent: 'window.onload = function(){hljs.highlightAll();}'
  760. });
  761. GM_addElement(document.body, "script", { textContent: `!function(e){var t,n,d,o,i,a,r='<svg><symbol id="envelope-one" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M36 16V8H4v24h8" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M12 40h32V16H12v24Z" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="m12 16 16 12 16-12" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M32 16H12v15" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M44 31V16H24" data-follow-stroke="currentColor"/></symbol><symbol id="at-sign" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M44 24c0-11.046-8.954-20-20-20S4 12.954 4 24s8.954 20 20 20v0c4.989 0 9.55-1.827 13.054-4.847" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M24 32a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M32 24a6 6 0 0 0 6 6v0a6 6 0 0 0 6-6m-12 1v-9" data-follow-stroke="currentColor"/></symbol><symbol id="copy" viewBox="0 0 48 48" fill="none"><path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="currentColor" d="M13 12.432v-4.62A2.813 2.813 0 0 1 15.813 5h24.374A2.813 2.813 0 0 1 43 7.813v24.375A2.813 2.813 0 0 1 40.187 35h-4.67" data-follow-stroke="currentColor"/><path stroke-linejoin="round" stroke-width="4" stroke="currentColor" d="M32.188 13H7.811A2.813 2.813 0 0 0 5 15.813v24.374A2.813 2.813 0 0 0 7.813 43h24.375A2.813 2.813 0 0 0 35 40.187V15.814A2.813 2.813 0 0 0 32.187 13Z" data-follow-stroke="currentColor"/></symbol></svg>';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window);` });
  762. },
  763. darkMode() {
  764. // 选择要监视的目标元素(body元素)
  765. const targetNode = document.querySelector('body');
  766. // 进入页面时判断是否是深色模式
  767. if (targetNode.classList.contains('dark-layout')) {
  768. util.addStyle('layuicss-theme-dark', 'link', 'https://sight-wcg.github.io/layui-theme-dark/dist/layui-theme-dark.css');
  769. util.removeStyle('hightlight-style');
  770. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark"));
  771. }
  772.  
  773. // 配置MutationObserver的选项
  774. const observerConfig = {
  775. attributes: true, // 监视属性变化
  776. attributeFilter: ['class'], // 只监视类属性
  777. };
  778.  
  779. // 创建一个新的MutationObserver,并指定触发变化时的回调函数
  780. const observer = new MutationObserver((mutationsList, observer) => {
  781. for (let mutation of mutationsList) {
  782. if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
  783. if (targetNode.classList.contains('dark-layout')) {
  784. util.addStyle('layuicss-theme-dark', 'link', 'https://sight-wcg.github.io/layui-theme-dark/dist/layui-theme-dark.css');
  785. util.removeStyle('hightlight-style');
  786. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark"));
  787. } else {
  788. util.removeStyle('layuicss-theme-dark');
  789. util.removeStyle('hightlight-style');
  790. util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle"));
  791. }
  792. }
  793. }
  794. });
  795.  
  796. // 使用给定的配置选项开始观察目标节点
  797. observer.observe(targetNode, observerConfig);
  798. },
  799. init() {
  800. this.initValue();
  801. this.addPluginStyle();
  802. this.checkLogin();
  803. this.autoSignIn();//自动签到
  804. this.addSignTips();//签到提示
  805. this.autoJump();//自动点击跳转页
  806. this.autoLoading();//无缝加载帖子和评论
  807. this.openPostInNewTab();//在新标签页打开帖子
  808. this.blockMemberDOMInsert();//屏蔽用户
  809. this.blockPost();//屏蔽帖子
  810. this.quickComment();//快捷评论
  811. this.addLevelTag();//添加等级标签
  812. this.userCardEx();//用户卡片扩展
  813. this.registerMenus();
  814. this.addPluginScript();
  815. this.addCodeHighlight();
  816. this.addImageSlide();
  817. this.darkMode();
  818. }
  819. }
  820. main.init();
  821. });
  822. })();