NGA Filter

troll must die

当前为 2021-10-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 1.4.0
  5. // @author snyssss
  6. // @description troll must die
  7.  
  8. // @match *://bbs.nga.cn/*
  9. // @match *://ngabbs.com/*
  10. // @match *://nga.178.com/*
  11.  
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15.  
  16. // @noframes
  17. // ==/UserScript==
  18.  
  19. ((n, self) => {
  20. if (n === undefined) return;
  21.  
  22. const key = "NGAFilter";
  23.  
  24. // 过滤提示
  25. const FILTER_TIPS =
  26. "过滤顺序:用户 &gt; 标记 &gt; 关键字<br/>过滤级别:隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承 &gt; 显示<br/>多个标记或者关键字按最高级别过滤";
  27.  
  28. // 过滤方式
  29. const FILTER_MODE = ["继承", "标记", "遮罩", "隐藏", "显示"];
  30.  
  31. // 切换过滤方式
  32. const switchFilterMode = (value) => {
  33. const next = FILTER_MODE.indexOf(value) + 1;
  34.  
  35. if (next >= FILTER_MODE.length) {
  36. return FILTER_MODE[0];
  37. }
  38.  
  39. return FILTER_MODE[next];
  40. };
  41.  
  42. // 数据
  43. const data = (() => {
  44. const d = {
  45. tags: {},
  46. users: {},
  47. keywords: {},
  48. options: {
  49. filterRegdateLimit: 0,
  50. filterMode: "隐藏",
  51. },
  52. };
  53.  
  54. const v = GM_getValue(key);
  55.  
  56. if (typeof v !== "object") {
  57. return d;
  58. }
  59.  
  60. return Object.assign(d, v);
  61. })();
  62.  
  63. // 保存数据
  64. const saveData = () => {
  65. GM_setValue(key, data);
  66. };
  67.  
  68. // 增加标记
  69. const addTag = (name) => {
  70. const tag = Object.values(data.tags).find((item) => item.name === name);
  71.  
  72. if (tag) return tag.id;
  73.  
  74. const id =
  75. Math.max(...Object.values(data.tags).map((item) => item.id), 0) + 1;
  76.  
  77. const hash = (() => {
  78. let h = 5381;
  79. for (var i = 0; i < name.length; i++) {
  80. h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
  81. }
  82. return h;
  83. })();
  84.  
  85. const hex = Math.abs(hash).toString(16) + "000000";
  86.  
  87. const hsv = [
  88. `0x${hex.substring(2, 4)}` / 255,
  89. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  90. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  91. ];
  92.  
  93. const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  94.  
  95. const color = ["#", ...rgb].reduce((a, b) => {
  96. return a + ("0" + b.toString(16)).slice(-2);
  97. });
  98.  
  99. data.tags[id] = {
  100. id,
  101. name,
  102. color,
  103. filterMode: FILTER_MODE[0],
  104. };
  105.  
  106. saveData();
  107.  
  108. return id;
  109. };
  110.  
  111. // 增加用户
  112. const addUser = (id, name = null, tags = [], filterMode = FILTER_MODE[0]) => {
  113. if (data.users[id]) return data.users[id];
  114.  
  115. data.users[id] = {
  116. id,
  117. name,
  118. tags,
  119. filterMode,
  120. };
  121.  
  122. saveData();
  123.  
  124. return data.users[id];
  125. };
  126.  
  127. // 增加关键字
  128. const addKeyword = (
  129. keyword,
  130. filterMode = FILTER_MODE[0],
  131. filterLevel = 0
  132. ) => {
  133. const id =
  134. Math.max(...Object.values(data.keywords).map((item) => item.id), 0) + 1;
  135.  
  136. data.keywords[id] = {
  137. id,
  138. keyword,
  139. filterMode,
  140. filterLevel,
  141. };
  142.  
  143. saveData();
  144.  
  145. return id;
  146. };
  147.  
  148. // 旧版本数据迁移
  149. {
  150. const dataKey = "troll_data";
  151. const modeKey = "troll_mode";
  152. const keywordKey = "troll_keyword";
  153.  
  154. if (localStorage.getItem(dataKey)) {
  155. let trollMap = (function () {
  156. try {
  157. return JSON.parse(localStorage.getItem(dataKey)) || {};
  158. } catch (e) {}
  159.  
  160. return {};
  161. })();
  162.  
  163. let filterMode = ~~localStorage.getItem(modeKey);
  164.  
  165. let filterKeyword = localStorage.getItem(keywordKey) || "";
  166.  
  167. // 整理标签
  168. [...new Set(Object.values(trollMap).flat())].forEach((item) =>
  169. addTag(item)
  170. );
  171.  
  172. // 整理用户
  173. Object.keys(trollMap).forEach((item) => {
  174. addUser(
  175. item,
  176. null,
  177. (typeof trollMap[item] === "object" ? trollMap[item] : []).map(
  178. (tag) => addTag(tag)
  179. )
  180. );
  181. });
  182.  
  183. data.options.filterMode = filterMode ? "隐藏" : "标记";
  184. data.options.keyword = filterKeyword;
  185.  
  186. localStorage.removeItem(dataKey);
  187. localStorage.removeItem(modeKey);
  188. localStorage.removeItem(keywordKey);
  189.  
  190. saveData();
  191. }
  192.  
  193. // v1.1.0 -> v1.1.1
  194. {
  195. Object.values(data.users).forEach(({ id, name, tags, enabled }) => {
  196. if (enabled !== undefined) {
  197. data.users[id] = {
  198. id,
  199. name,
  200. tags,
  201. filterMode: enabled ? "继承" : "显示",
  202. };
  203. }
  204. });
  205.  
  206. Object.values(data.tags).forEach(({ id, name, color, enabled }) => {
  207. if (enabled !== undefined) {
  208. data.tags[id] = {
  209. id,
  210. name,
  211. color,
  212. filterMode: enabled ? "继承" : "显示",
  213. };
  214. }
  215. });
  216.  
  217. if (data.options.filterMode === 0) {
  218. data.options.filterMode = "隐藏";
  219. } else if (data.options.filterMode === 1) {
  220. data.options.filterMode = "标记";
  221. }
  222.  
  223. saveData();
  224. }
  225.  
  226. // v1.2.x -> v1.3.0
  227. {
  228. if (data.options.keyword) {
  229. addKeyword(data.options.keyword);
  230.  
  231. delete data.options.keyword;
  232.  
  233. saveData();
  234. }
  235. }
  236. }
  237.  
  238. // 编辑用户标记
  239. const editUser = (() => {
  240. let window;
  241. return (uid, name, callback) => {
  242. if (window === undefined) {
  243. window = n.createCommmonWindow();
  244. }
  245.  
  246. const user = data.users[uid];
  247.  
  248. const content = document.createElement("div");
  249.  
  250. const size = Math.floor((screen.width * 0.8) / 200);
  251.  
  252. const items = Object.values(data.tags).map(
  253. (tag, index) => `
  254. <td class="c1">
  255. <label for="s-tag-${index}" style="display: block; cursor: pointer;">
  256. <b class="block_txt nobr" style="background:${
  257. tag.color
  258. }; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>
  259. </label>
  260. </td>
  261. <td class="c2" width="1">
  262. <input id="s-tag-${index}" type="checkbox" value="${tag.id}" ${
  263. user && user.tags.find((item) => item === tag.id) && "checked"
  264. }/>
  265. </td>
  266. `
  267. );
  268.  
  269. const rows = [...new Array(Math.ceil(items.length / size))].map(
  270. (item, index) =>
  271. `
  272. <tr class="row${(index % 2) + 1}">
  273. ${items.slice(size * index, size * (index + 1)).join("")}
  274. </tr>
  275. `
  276. );
  277.  
  278. content.className = "w100";
  279. content.innerHTML = `
  280. <div class="filter-table-wrapper" style="width: 80vw;">
  281. <table class="filter-table forumbox">
  282. <tbody>
  283. ${rows.join("")}
  284. </tbody>
  285. </table>
  286. </div>
  287. <div style="margin: 10px 0;">
  288. <input placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
  289. </div>
  290. <div style="margin: 10px 0;">
  291. <span>过滤方式:</span>
  292. <button>${(user && user.filterMode) || FILTER_MODE[0]}</button>
  293. <div class="right_">
  294. <button>删除</button>
  295. <button>保存</button>
  296. </div>
  297. </div>
  298. <div class="silver" style="margin-top: 5px;">${FILTER_TIPS}</div>
  299. `;
  300.  
  301. const actions = content.getElementsByTagName("button");
  302.  
  303. actions[0].onclick = () => {
  304. actions[0].innerText = switchFilterMode(
  305. actions[0].innerText || FILTER_MODE[0]
  306. );
  307. };
  308.  
  309. actions[1].onclick = () => {
  310. if (confirm("是否确认?")) {
  311. delete data.users[uid];
  312.  
  313. saveData();
  314.  
  315. callback && callback();
  316.  
  317. window._.hide();
  318. }
  319. };
  320.  
  321. actions[2].onclick = () => {
  322. if (confirm("是否确认?")) {
  323. const values = [...content.getElementsByTagName("input")];
  324. const newTags = values[values.length - 1].value
  325. .split("|")
  326. .filter((item) => item.length)
  327. .map((item) => addTag(item));
  328. const tags = [
  329. ...new Set(
  330. values
  331. .filter((item) => item.type === "checkbox" && item.checked)
  332. .map((item) => ~~item.value)
  333. .concat(newTags)
  334. ),
  335. ].sort();
  336.  
  337. if (user) {
  338. user.tags = tags;
  339. user.filterMode = actions[0].innerText;
  340. } else {
  341. addUser(uid, name, tags, actions[0].innerText);
  342. }
  343.  
  344. saveData();
  345.  
  346. callback && callback();
  347.  
  348. window._.hide();
  349. }
  350. };
  351.  
  352. if (user === undefined) {
  353. actions[1].style = "display: none;";
  354. }
  355.  
  356. window._.addContent(null);
  357. window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
  358. window._.addContent(content);
  359. window._.show();
  360. };
  361. })();
  362.  
  363. // 判断过滤方式
  364. const getFilterMode = async (uid, content, level) => {
  365. let result = -1;
  366.  
  367. const filterRegdateLimit = data.options.filterRegdateLimit || 0;
  368.  
  369. const user = data.users[uid];
  370.  
  371. const tags = user ? user.tags.map((tag) => data.tags[tag]) : [];
  372.  
  373. const keywords = Object.values(data.keywords);
  374.  
  375. if (filterRegdateLimit > 0) {
  376. if (n.userInfo.users[uid] === undefined) {
  377. n.userInfo.users[uid] = await new Promise((resolve) => {
  378. fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`)
  379. .then((res) => res.blob())
  380. .then((blob) => {
  381. const reader = new FileReader();
  382.  
  383. reader.onload = () => {
  384. const text = reader.result;
  385. const result = JSON.parse(
  386. text.replace("window.script_muti_get_var_store=", "")
  387. );
  388.  
  389. resolve(result.data[0]);
  390. };
  391.  
  392. reader.readAsText(blob, "GBK");
  393. })
  394. .catch(() => {
  395. resolve();
  396. });
  397. });
  398. }
  399.  
  400. const userInfo = n.userInfo.users[uid];
  401.  
  402. if (
  403. userInfo &&
  404. userInfo.regdate * 1000 > new Date() - filterRegdateLimit
  405. ) {
  406. return FILTER_MODE.indexOf("隐藏");
  407. }
  408. }
  409.  
  410. if (user) {
  411. const filterMode = FILTER_MODE.indexOf(user.filterMode);
  412.  
  413. if (filterMode > 0) {
  414. return filterMode;
  415. }
  416.  
  417. result = filterMode;
  418. }
  419.  
  420. if (tags.length) {
  421. const filterMode = (() => {
  422. if (tags.some((tag) => tag.filterMode !== "显示")) {
  423. return tags
  424. .filter((tag) => tag.filterMode !== "显示")
  425. .map((tag) => FILTER_MODE.indexOf(tag.filterMode) || 0)
  426. .sort((a, b) => b - a)[0];
  427. }
  428.  
  429. return FILTER_MODE.indexOf("显示");
  430. })();
  431.  
  432. if (filterMode > 0) {
  433. return filterMode;
  434. }
  435.  
  436. result = filterMode;
  437. }
  438.  
  439. if (keywords.length) {
  440. const filterMode = (() => {
  441. const r = keywords
  442. .filter((item) => item.keyword && item.filterMode !== "显示")
  443. .filter((item) => (item.filterLevel || 0) >= level)
  444. .sort(
  445. (a, b) =>
  446. FILTER_MODE.indexOf(b.filterMode) -
  447. FILTER_MODE.indexOf(a.filterMode)
  448. )
  449. .find((item) => content.search(item.keyword) >= 0);
  450.  
  451. if (r) {
  452. return FILTER_MODE.indexOf(r.filterMode);
  453. }
  454.  
  455. return result;
  456. })();
  457.  
  458. if (filterMode > 0) {
  459. return filterMode;
  460. }
  461.  
  462. result = filterMode;
  463. }
  464.  
  465. return result;
  466. };
  467.  
  468. // 处理引用
  469. const handleQuote = async (content) => {
  470. const quotes = content.querySelectorAll(".quote");
  471.  
  472. await Promise.all(
  473. [...quotes].map(async (quote) => {
  474. const uid = (() => {
  475. const ele = quote.querySelector("a[href^='/nuke.php']");
  476.  
  477. if (ele) {
  478. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  479.  
  480. if (res) {
  481. return res[1];
  482. }
  483. }
  484.  
  485. return 0;
  486. })();
  487.  
  488. if (uid) {
  489. const filterMode = await new Promise(async (resolve) => {
  490. const mode = await getFilterMode(uid, quote.innerText, 1);
  491.  
  492. if (mode === 0) {
  493. resolve(data.options.filterMode);
  494. }
  495.  
  496. if (mode > 0) {
  497. resolve(FILTER_MODE[mode]);
  498. }
  499.  
  500. resolve("");
  501. });
  502.  
  503. if (filterMode === "标记") {
  504. quote.innerHTML = `
  505. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  506. <span class="crimson">Troll must die.</span>
  507. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  508. <div style="display: none;" name="troll_${uid}">
  509. ${quote.innerHTML}
  510. </div>
  511. </div>`;
  512. } else if (filterMode === "遮罩") {
  513. const source = document.createElement("DIV");
  514.  
  515. source.innerHTML = quote.innerHTML;
  516. source.style.display = "none";
  517.  
  518. const caption = document.createElement("CAPTION");
  519.  
  520. caption.className = "filter-mask filter-mask-block";
  521.  
  522. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  523. caption.onclick = () => {
  524. quote.removeChild(caption);
  525.  
  526. source.style.display = "";
  527. };
  528.  
  529. quote.innerHTML = "";
  530. quote.appendChild(source);
  531. quote.appendChild(caption);
  532. } else if (filterMode === "隐藏") {
  533. quote.innerHTML = "";
  534. }
  535. }
  536. })
  537. );
  538. };
  539.  
  540. // 过滤
  541. const reFilter = (() => {
  542. let hasNext = false;
  543. let isRunning = false;
  544.  
  545. const func = async () => {
  546. const tPage = location.pathname === "/thread.php";
  547. const pPage = location.pathname === "/read.php";
  548. const uPage = location.pathname === "/nuke.php";
  549.  
  550. if (tPage && new RegExp(`authorid=${self}`).test(location.search)) {
  551. return;
  552. }
  553.  
  554. if (tPage) {
  555. const tData = n.topicArg.data;
  556.  
  557. await Promise.all(
  558. Object.values(tData).map(async (item) => {
  559. if (item.containerC) return;
  560.  
  561. const uid =
  562. item[2].search.match(/uid=(\S+)/) &&
  563. item[2].search.match(/uid=(\S+)/)[1];
  564.  
  565. const filterMode = await new Promise(async (resolve) => {
  566. const mode = await getFilterMode(uid, item[1].innerText, 0);
  567.  
  568. if (mode === 0) {
  569. resolve(data.options.filterMode);
  570. }
  571.  
  572. if (mode > 0) {
  573. resolve(FILTER_MODE[mode]);
  574. }
  575.  
  576. resolve("");
  577. });
  578.  
  579. item.contentC = item[1];
  580.  
  581. item.contentB = item.contentB || item.contentC.innerHTML;
  582.  
  583. item.containerC =
  584. item.containerC || item.contentC.parentNode.parentNode;
  585.  
  586. item.containerC.style = "";
  587. item.contentC.style = "";
  588. item[1].className = item[1].className.replace(" filter-mask", "");
  589. item[2].className = item[2].className.replace(" filter-mask", "");
  590.  
  591. if (filterMode === "标记") {
  592. item.contentC.style = "text-decoration: line-through;";
  593. } else if (filterMode === "遮罩") {
  594. item[1].className += " filter-mask";
  595. item[2].className += " filter-mask";
  596. } else if (filterMode === "隐藏") {
  597. item.containerC.style = "display: none;";
  598. }
  599. })
  600. );
  601. } else if (pPage) {
  602. const pData = n.postArg.data;
  603.  
  604. await Promise.all(
  605. Object.values(pData).map(async (item) => {
  606. if (~~item.pAid === self) return;
  607. if (item.containerC) return;
  608.  
  609. if (typeof item.i === "number") {
  610. item.actionC =
  611. item.actionC ||
  612. (() => {
  613. const ele = item.uInfoC.querySelector('[name="uid"]');
  614.  
  615. ele.onclick = null;
  616.  
  617. return ele;
  618. })();
  619.  
  620. item.tagC =
  621. item.tagC ||
  622. (() => {
  623. const tc = document.createElement("div");
  624.  
  625. tc.className = "filter-tags";
  626.  
  627. item.uInfoC.appendChild(tc);
  628.  
  629. return tc;
  630. })();
  631. }
  632.  
  633. item.pName =
  634. item.pName ||
  635. item.uInfoC.getElementsByClassName("author")[0].innerText;
  636.  
  637. item.reFilter =
  638. item.reFilter ||
  639. (async () => {
  640. const uid = item.pAid;
  641.  
  642. const filterMode = await new Promise(async (resolve) => {
  643. const mode = await getFilterMode(
  644. uid,
  645. item.contentC.innerText,
  646. 1
  647. );
  648.  
  649. if (mode === 0) {
  650. resolve(data.options.filterMode);
  651. }
  652.  
  653. if (mode > 0) {
  654. resolve(FILTER_MODE[mode]);
  655. }
  656.  
  657. resolve("");
  658. });
  659.  
  660. item.avatarC =
  661. item.avatarC ||
  662. (() => {
  663. const tc = document.createElement("div");
  664.  
  665. const avatar = document.getElementById(
  666. `posteravatar${item.i}`
  667. );
  668.  
  669. if (avatar) {
  670. avatar.parentNode.insertBefore(tc, avatar.nextSibling);
  671.  
  672. tc.appendChild(avatar);
  673. }
  674.  
  675. return tc;
  676. })();
  677.  
  678. item.contentB = item.contentB || item.contentC.innerHTML;
  679.  
  680. item.containerC =
  681. item.containerC ||
  682. (() => {
  683. let temp = item.contentC;
  684.  
  685. if (item.i >= 0) {
  686. while (temp.nodeName !== "TBODY") {
  687. temp = temp.parentNode;
  688. }
  689. } else {
  690. while (temp.nodeName !== "DIV") {
  691. temp = temp.parentNode;
  692. }
  693. }
  694.  
  695. return temp;
  696. })();
  697.  
  698. item.avatarC.style.display = "";
  699. item.containerC.style.display = "";
  700. item.contentC.innerHTML = item.contentB;
  701.  
  702. if (item.actionC) {
  703. item.actionC.style = "background: #aaa;";
  704. }
  705.  
  706. if (filterMode === "标记") {
  707. item.avatarC.style.display = "none";
  708. item.contentC.innerHTML = `
  709. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  710. <span class="crimson">Troll must die.</span>
  711. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  712. <div style="display: none;" name="troll_${uid}">
  713. ${item.contentB}
  714. </div>
  715. </div>`;
  716.  
  717. if (item.actionC && data.users[uid]) {
  718. item.actionC.style = "background: #cb4042;";
  719. }
  720. } else if (filterMode === "遮罩") {
  721. const caption = document.createElement("CAPTION");
  722.  
  723. if (item.i >= 0) {
  724. caption.className = "filter-mask filter-mask-block";
  725. } else {
  726. caption.className = "filter-mask filter-mask-block left";
  727. caption.style = "width: 47%;";
  728. }
  729.  
  730. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  731. caption.onclick = () => {
  732. item.containerC.parentNode.removeChild(caption);
  733. item.containerC.style.display = "";
  734. };
  735.  
  736. item.containerC.parentNode.insertBefore(
  737. caption,
  738. item.containerC
  739. );
  740. item.containerC.style.display = "none";
  741.  
  742. if (item.actionC && data.users[uid]) {
  743. item.actionC.style = "background: #cb4042;";
  744. }
  745. } else if (filterMode === "隐藏") {
  746. item.containerC.style.display = "none";
  747. } else {
  748. await handleQuote(item.contentC);
  749. }
  750.  
  751. if (item.tagC) {
  752. const tags = data.users[uid]
  753. ? data.users[uid].tags.map((tag) => data.tags[tag]) || []
  754. : [];
  755.  
  756. item.tagC.style.display = tags.length ? "" : "none";
  757. item.tagC.innerHTML = tags
  758. .map(
  759. (tag) =>
  760. `<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
  761. )
  762. .join("");
  763. }
  764. });
  765.  
  766. if (item.actionC) {
  767. item.actionC.onclick =
  768. item.actionC.onclick ||
  769. ((e) => {
  770. if (item.pAid < 0) return;
  771.  
  772. const user = data.users[item.pAid];
  773.  
  774. if (e.ctrlKey === false) {
  775. editUser(item.pAid, item.pName, item.reFilter);
  776. } else {
  777. if (user) {
  778. delete data.users[user.id];
  779. } else {
  780. addUser(item.pAid, item.pName);
  781. }
  782.  
  783. saveData();
  784. item.reFilter();
  785. }
  786. });
  787. }
  788.  
  789. await item.reFilter();
  790. })
  791. );
  792. } else if (uPage) {
  793. const container = document.getElementById("ucp_block");
  794.  
  795. if (container.firstChild) {
  796. const uid = container.innerText.match(/用户ID\s*:\s*(\S+)/)[1];
  797.  
  798. const name = container.innerText.match(/用户名\s*:\s*(\S+)/)[1];
  799.  
  800. container.tagC =
  801. container.tagC ||
  802. (() => {
  803. const c = document.createElement("span");
  804.  
  805. c.innerHTML = `
  806. <h2 class="catetitle">:: ${name} 的标记 ::</h2>
  807. <div class="cateblock" style="text-align: left; line-height: 1.8em;">
  808. <div class="contentBlock" style="padding: 5px 10px;">
  809. <span>
  810. <ul class="actions" style="padding: 0px; margin: 0px;">
  811. <li style="padding-right: 5px;">
  812. <span>
  813. <a href="javascript: void(0);">[编辑 ${name} 的标记]</a>
  814. </span>
  815. </li>
  816. <div class="clear"></div>
  817. </ul>
  818. </span>
  819. <div class="filter-tags"></div>
  820. <div class="clear"></div>
  821. </div>
  822. </div>
  823. `;
  824.  
  825. c.getElementsByTagName("a")[0].onclick = () => {
  826. editUser(uid, name, container.refresh);
  827. };
  828.  
  829. container.firstChild.insertBefore(
  830. c,
  831. container.firstChild.childNodes[1]
  832. );
  833.  
  834. return c.getElementsByClassName("filter-tags")[0];
  835. })();
  836.  
  837. container.refresh = () => {
  838. container.tagC.innerHTML = data.users[uid].tags
  839. .map(
  840. (tag) =>
  841. `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`
  842. )
  843. .join("");
  844. };
  845.  
  846. container.refresh();
  847. }
  848. }
  849. };
  850.  
  851. const execute = () =>
  852. func().finally(() => {
  853. if (hasNext) {
  854. hasNext = false;
  855.  
  856. execute();
  857. } else {
  858. isRunning = false;
  859. }
  860. });
  861.  
  862. return async () => {
  863. if (isRunning) {
  864. hasNext = true;
  865. } else {
  866. isRunning = true;
  867.  
  868. await execute();
  869. }
  870. };
  871. })();
  872.  
  873. // STYLE
  874. GM_addStyle(`
  875. .filter-table-wrapper {
  876. max-height: 80vh;
  877. overflow-y: auto;
  878. }
  879. .filter-table {
  880. margin: 0;
  881. }
  882. .filter-table th,
  883. .filter-table td {
  884. position: relative;
  885. white-space: nowrap;
  886. }
  887. .filter-table th {
  888. position: sticky;
  889. top: 2px;
  890. z-index: 1;
  891. }
  892. .filter-table input:not([type]), .filter-table input[type="text"] {
  893. margin: 0;
  894. box-sizing: border-box;
  895. height: 100%;
  896. width: 100%;
  897. }
  898. .filter-input-wrapper {
  899. position: absolute;
  900. top: 6px;
  901. right: 6px;
  902. bottom: 6px;
  903. left: 6px;
  904. }
  905. .filter-text-ellipsis {
  906. display: flex;
  907. }
  908. .filter-text-ellipsis > * {
  909. flex: 1;
  910. width: 1px;
  911. overflow: hidden;
  912. text-overflow: ellipsis;
  913. }
  914. .filter-button-group {
  915. margin: -.1em -.2em;
  916. }
  917. .filter-tags {
  918. margin: 2px -0.2em 0;
  919. text-align: left;
  920. }
  921. .filter-mask {
  922. margin: 1px;
  923. color: #81C7D4;
  924. background: #81C7D4;
  925. }
  926. .filter-mask-block {
  927. display: block;
  928. border: 1px solid #66BAB7;
  929. text-align: center !important;
  930. }
  931. .filter-input-wrapper {
  932. position: absolute;
  933. top: 6px;
  934. right: 6px;
  935. bottom: 6px;
  936. left: 6px;
  937. }
  938. `);
  939.  
  940. // UI
  941. const u = (() => {
  942. const modules = {};
  943.  
  944. const tabContainer = (() => {
  945. const c = document.createElement("div");
  946.  
  947. c.className = "w100";
  948. c.innerHTML = `
  949. <div class="right_" style="margin-bottom: 5px;">
  950. <table class="stdbtn" cellspacing="0">
  951. <tbody>
  952. <tr></tr>
  953. </tbody>
  954. </table>
  955. </div>
  956. <div class="clear"></div>
  957. `;
  958.  
  959. return c;
  960. })();
  961.  
  962. const tabPanelContainer = (() => {
  963. const c = document.createElement("div");
  964.  
  965. c.style = "width: 80vw;";
  966.  
  967. return c;
  968. })();
  969.  
  970. const content = (() => {
  971. const c = document.createElement("div");
  972.  
  973. c.append(tabContainer);
  974. c.append(tabPanelContainer);
  975.  
  976. return c;
  977. })();
  978.  
  979. const addModule = (() => {
  980. const tc = tabContainer.getElementsByTagName("tr")[0];
  981. const cc = tabPanelContainer;
  982.  
  983. return (module) => {
  984. const tabBox = document.createElement("td");
  985.  
  986. tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
  987.  
  988. const tab = tabBox.childNodes[0];
  989.  
  990. const toggle = () => {
  991. Object.values(modules).forEach((item) => {
  992. if (item.tab === tab) {
  993. item.tab.className = "nobr";
  994. item.content.style = "display: block";
  995. item.refresh();
  996. } else {
  997. item.tab.className = "nobr silver";
  998. item.content.style = "display: none";
  999. }
  1000. });
  1001. };
  1002.  
  1003. tc.append(tabBox);
  1004. cc.append(module.content);
  1005.  
  1006. tab.onclick = toggle;
  1007.  
  1008. modules[module.name] = {
  1009. ...module,
  1010. tab,
  1011. toggle,
  1012. };
  1013.  
  1014. return modules[module.name];
  1015. };
  1016. })();
  1017.  
  1018. return {
  1019. content,
  1020. modules,
  1021. addModule,
  1022. };
  1023. })();
  1024.  
  1025. // 用户
  1026. const userModule = (() => {
  1027. const content = (() => {
  1028. const c = document.createElement("div");
  1029.  
  1030. c.style = "display: none";
  1031. c.innerHTML = `
  1032. <div class="filter-table-wrapper">
  1033. <table class="filter-table forumbox">
  1034. <thead>
  1035. <tr class="block_txt_c0">
  1036. <th class="c1" width="1">昵称</th>
  1037. <th class="c2">标记</th>
  1038. <th class="c3" width="1">过滤方式</th>
  1039. <th class="c4" width="1">操作</th>
  1040. </tr>
  1041. </thead>
  1042. <tbody></tbody>
  1043. </table>
  1044. </div>
  1045. `;
  1046.  
  1047. return c;
  1048. })();
  1049.  
  1050. const refresh = (() => {
  1051. const container = content.getElementsByTagName("tbody")[0];
  1052.  
  1053. const func = () => {
  1054. container.innerHTML = "";
  1055.  
  1056. Object.values(data.users).forEach((item) => {
  1057. const tc = document.createElement("tr");
  1058.  
  1059. tc.className = `row${
  1060. (container.querySelectorAll("TR").length % 2) + 1
  1061. }`;
  1062.  
  1063. tc.refresh = () => {
  1064. if (data.users[item.id]) {
  1065. tc.innerHTML = `
  1066. <td class="c1">
  1067. <a href="/nuke.php?func=ucp&uid=${
  1068. item.id
  1069. }" class="b nobr">[${
  1070. item.name ? "@" + item.name : "#" + item.id
  1071. }]</a>
  1072. </td>
  1073. <td class="c2">
  1074. ${item.tags
  1075. .map((tag) => {
  1076. if (data.tags[tag]) {
  1077. return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
  1078. }
  1079. })
  1080. .join("")}
  1081. </td>
  1082. <td class="c3">
  1083. <div class="filter-table-button-group">
  1084. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1085. </div>
  1086. </td>
  1087. <td class="c4">
  1088. <div class="filter-table-button-group">
  1089. <button>编辑</button>
  1090. <button>删除</button>
  1091. </div>
  1092. </td>
  1093. `;
  1094.  
  1095. const actions = tc.getElementsByTagName("button");
  1096.  
  1097. actions[0].onclick = () => {
  1098. data.users[item.id].filterMode = switchFilterMode(
  1099. data.users[item.id].filterMode || FILTER_MODE[0]
  1100. );
  1101.  
  1102. actions[0].innerHTML = data.users[item.id].filterMode;
  1103.  
  1104. saveData();
  1105. reFilter();
  1106. };
  1107.  
  1108. actions[1].onclick = () => {
  1109. editUser(item.id, item.name, tc.refresh);
  1110. };
  1111.  
  1112. actions[2].onclick = () => {
  1113. if (confirm("是否确认?")) {
  1114. delete data.users[item.id];
  1115. container.removeChild(tc);
  1116.  
  1117. saveData();
  1118. reFilter();
  1119. }
  1120. };
  1121. } else {
  1122. tc.remove();
  1123. }
  1124. };
  1125.  
  1126. tc.refresh();
  1127.  
  1128. container.appendChild(tc);
  1129. });
  1130. };
  1131.  
  1132. return func;
  1133. })();
  1134.  
  1135. return {
  1136. name: "用户",
  1137. content,
  1138. refresh,
  1139. };
  1140. })();
  1141.  
  1142. // 标记
  1143. const tagModule = (() => {
  1144. const content = (() => {
  1145. const c = document.createElement("div");
  1146.  
  1147. c.style = "display: none";
  1148. c.innerHTML = `
  1149. <div class="filter-table-wrapper">
  1150. <table class="filter-table forumbox">
  1151. <thead>
  1152. <tr class="block_txt_c0">
  1153. <th class="c1" width="1">标记</th>
  1154. <th class="c2">列表</th>
  1155. <th class="c3" width="1">过滤方式</th>
  1156. <th class="c4" width="1">操作</th>
  1157. </tr>
  1158. </thead>
  1159. <tbody></tbody>
  1160. </table>
  1161. </div>
  1162. `;
  1163.  
  1164. return c;
  1165. })();
  1166.  
  1167. const refresh = (() => {
  1168. const container = content.getElementsByTagName("tbody")[0];
  1169.  
  1170. const func = () => {
  1171. container.innerHTML = "";
  1172.  
  1173. Object.values(data.tags).forEach((item) => {
  1174. const tc = document.createElement("tr");
  1175.  
  1176. tc.className = `row${
  1177. (container.querySelectorAll("TR").length % 2) + 1
  1178. }`;
  1179.  
  1180. tc.innerHTML = `
  1181. <td class="c1">
  1182. <b class="block_txt nobr" style="background:${
  1183. item.color
  1184. }; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
  1185. </td>
  1186. <td class="c2">
  1187. <button>${
  1188. Object.values(data.users).filter((user) =>
  1189. user.tags.find((tag) => tag === item.id)
  1190. ).length
  1191. }
  1192. </button>
  1193. <div style="white-space: normal; display: none;">
  1194. ${Object.values(data.users)
  1195. .filter((user) =>
  1196. user.tags.find((tag) => tag === item.id)
  1197. )
  1198. .map(
  1199. (user) =>
  1200. `<a href="/nuke.php?func=ucp&uid=${
  1201. user.id
  1202. }" class="b nobr">[${
  1203. user.name ? "@" + user.name : "#" + user.id
  1204. }]</a>`
  1205. )
  1206. .join("")}
  1207. </div>
  1208. </td>
  1209. <td class="c3">
  1210. <div class="filter-table-button-group">
  1211. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1212. </div>
  1213. </td>
  1214. <td class="c4">
  1215. <div class="filter-table-button-group">
  1216. <button>删除</button>
  1217. </div>
  1218. </td>
  1219. `;
  1220.  
  1221. const actions = tc.getElementsByTagName("button");
  1222.  
  1223. actions[0].onclick = (() => {
  1224. let hide = true;
  1225. return () => {
  1226. hide = !hide;
  1227. actions[0].nextElementSibling.style.display = hide
  1228. ? "none"
  1229. : "block";
  1230. };
  1231. })();
  1232.  
  1233. actions[1].onclick = () => {
  1234. data.tags[item.id].filterMode = switchFilterMode(
  1235. data.tags[item.id].filterMode || FILTER_MODE[0]
  1236. );
  1237.  
  1238. actions[1].innerHTML = data.tags[item.id].filterMode;
  1239.  
  1240. saveData();
  1241. reFilter();
  1242. };
  1243.  
  1244. actions[2].onclick = () => {
  1245. if (confirm("是否确认?")) {
  1246. delete data.tags[item.id];
  1247.  
  1248. Object.values(data.users).forEach((user) => {
  1249. const index = user.tags.findIndex((tag) => tag === item.id);
  1250. if (index >= 0) {
  1251. user.tags.splice(index, 1);
  1252. }
  1253. });
  1254.  
  1255. container.removeChild(tc);
  1256.  
  1257. saveData();
  1258. reFilter();
  1259. }
  1260. };
  1261.  
  1262. container.appendChild(tc);
  1263. });
  1264. };
  1265.  
  1266. return func;
  1267. })();
  1268.  
  1269. return {
  1270. name: "标记",
  1271. content,
  1272. refresh,
  1273. };
  1274. })();
  1275.  
  1276. // 关键字
  1277. const keywordModule = (() => {
  1278. const content = (() => {
  1279. const c = document.createElement("div");
  1280.  
  1281. c.style = "display: none";
  1282. c.innerHTML = `
  1283. <div class="filter-table-wrapper">
  1284. <table class="filter-table forumbox">
  1285. <thead>
  1286. <tr class="block_txt_c0">
  1287. <th class="c1">列表</th>
  1288. <th class="c2" width="1">过滤方式</th>
  1289. <th class="c3" width="1">包括内容</th>
  1290. <th class="c4" width="1">操作</th>
  1291. </tr>
  1292. </thead>
  1293. <tbody></tbody>
  1294. </table>
  1295. </div>
  1296. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
  1297. `;
  1298.  
  1299. return c;
  1300. })();
  1301.  
  1302. const refresh = (() => {
  1303. const container = content.getElementsByTagName("tbody")[0];
  1304.  
  1305. const func = () => {
  1306. container.innerHTML = "";
  1307.  
  1308. Object.values(data.keywords).forEach((item) => {
  1309. const tc = document.createElement("tr");
  1310.  
  1311. tc.className = `row${
  1312. (container.querySelectorAll("TR").length % 2) + 1
  1313. }`;
  1314.  
  1315. tc.innerHTML = `
  1316. <td class="c1">
  1317. <div class="filter-input-wrapper">
  1318. <input value="${item.keyword || ""}" />
  1319. </div>
  1320. </td>
  1321. <td class="c2">
  1322. <div class="filter-table-button-group">
  1323. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1324. </div>
  1325. </td>
  1326. <td class="c3">
  1327. <div style="text-align: center;">
  1328. <input type="checkbox" ${
  1329. item.filterLevel ? `checked="checked"` : ""
  1330. } />
  1331. </div>
  1332. </td>
  1333. <td class="c4">
  1334. <div class="filter-table-button-group">
  1335. <button>保存</button>
  1336. <button>删除</button>
  1337. </div>
  1338. </td>
  1339. `;
  1340.  
  1341. const inputElement = tc.querySelector("INPUT");
  1342. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  1343. const actions = tc.getElementsByTagName("button");
  1344.  
  1345. actions[0].onclick = () => {
  1346. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1347. };
  1348.  
  1349. actions[1].onclick = () => {
  1350. if (inputElement.value) {
  1351. data.keywords[item.id] = {
  1352. id: item.id,
  1353. keyword: inputElement.value,
  1354. filterMode: actions[0].innerHTML,
  1355. filterLevel: levelElement.checked ? 1 : 0,
  1356. };
  1357.  
  1358. saveData();
  1359. refresh();
  1360. }
  1361. };
  1362.  
  1363. actions[2].onclick = () => {
  1364. if (confirm("是否确认?")) {
  1365. delete data.keywords[item.id];
  1366.  
  1367. saveData();
  1368. refresh();
  1369. }
  1370. };
  1371.  
  1372. container.appendChild(tc);
  1373. });
  1374.  
  1375. {
  1376. const tc = document.createElement("tr");
  1377.  
  1378. tc.className = `row${
  1379. (container.querySelectorAll("TR").length % 2) + 1
  1380. }`;
  1381.  
  1382. tc.innerHTML = `
  1383. <td class="c1">
  1384. <div class="filter-input-wrapper">
  1385. <input value="" />
  1386. </div>
  1387. </td>
  1388. <td class="c2">
  1389. <div class="filter-table-button-group">
  1390. <button>${FILTER_MODE[0]}</button>
  1391. </div>
  1392. </td>
  1393. <td class="c3">
  1394. <div style="text-align: center;">
  1395. <input type="checkbox" />
  1396. </div>
  1397. </td>
  1398. <td class="c4">
  1399. <div class="filter-table-button-group">
  1400. <button>添加</button>
  1401. </div>
  1402. </td>
  1403. `;
  1404.  
  1405. const inputElement = tc.querySelector("INPUT");
  1406. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  1407. const actions = tc.getElementsByTagName("button");
  1408.  
  1409. actions[0].onclick = () => {
  1410. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1411. };
  1412.  
  1413. actions[1].onclick = () => {
  1414. if (inputElement.value) {
  1415. addKeyword(
  1416. inputElement.value,
  1417. actions[0].innerHTML,
  1418. levelElement.checked ? 1 : 0
  1419. );
  1420.  
  1421. saveData();
  1422. refresh();
  1423. }
  1424. };
  1425.  
  1426. container.appendChild(tc);
  1427. }
  1428. };
  1429.  
  1430. return func;
  1431. })();
  1432.  
  1433. return {
  1434. name: "关键字",
  1435. content,
  1436. refresh,
  1437. };
  1438. })();
  1439.  
  1440. // 通用设置
  1441. const commonModule = (() => {
  1442. const content = (() => {
  1443. const c = document.createElement("div");
  1444.  
  1445. c.style = "display: none";
  1446.  
  1447. return c;
  1448. })();
  1449.  
  1450. const refresh = (() => {
  1451. const container = content;
  1452.  
  1453. const func = () => {
  1454. container.innerHTML = "";
  1455.  
  1456. // 默认过滤方式
  1457. {
  1458. const tc = document.createElement("div");
  1459.  
  1460. tc.innerHTML += `
  1461. <div>默认过滤方式</div>
  1462. <div></div>
  1463. <div class="silver" style="margin-top: 10px;">${FILTER_TIPS}</div>
  1464. `;
  1465.  
  1466. ["标记", "遮罩", "隐藏"].forEach((item, index) => {
  1467. const ele = document.createElement("SPAN");
  1468.  
  1469. ele.innerHTML += `
  1470. <input id="s-fm-${index}" type="radio" name="filterType" ${
  1471. data.options.filterMode === item && "checked"
  1472. }>
  1473. <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
  1474. `;
  1475.  
  1476. const inp = ele.querySelector("input");
  1477.  
  1478. inp.onchange = () => {
  1479. if (inp.checked) {
  1480. data.options.filterMode = item;
  1481. saveData();
  1482. reFilter();
  1483. }
  1484. };
  1485.  
  1486. tc.querySelectorAll("div")[1].append(ele);
  1487. });
  1488.  
  1489. container.appendChild(tc);
  1490. }
  1491.  
  1492. // 小号过滤
  1493. {
  1494. const tc = document.createElement("div");
  1495.  
  1496. tc.innerHTML += `
  1497. <br/>
  1498. <div>
  1499. 隐藏注册时间小于<input value="${
  1500. (data.options.filterRegdateLimit || 0) / 86400000
  1501. }" maxLength="4" style="width: 48px;" />天的用户
  1502. <button>确认</button>
  1503. </div>
  1504. `;
  1505.  
  1506. const actions = tc.getElementsByTagName("button");
  1507.  
  1508. actions[0].onclick = () => {
  1509. const v = actions[0].previousElementSibling.value;
  1510.  
  1511. const n = Number(v) || 0;
  1512.  
  1513. data.options.filterRegdateLimit = n < 0 ? 0 : n * 86400000;
  1514.  
  1515. saveData();
  1516. reFilter();
  1517. };
  1518.  
  1519. container.appendChild(tc);
  1520. }
  1521.  
  1522. // 删除没有标记的用户
  1523. {
  1524. const tc = document.createElement("div");
  1525.  
  1526. tc.innerHTML += `
  1527. <br/>
  1528. <div>
  1529. <button>删除没有标记的用户</button>
  1530. </div>
  1531. `;
  1532.  
  1533. const actions = tc.getElementsByTagName("button");
  1534.  
  1535. actions[0].onclick = () => {
  1536. if (confirm("是否确认?")) {
  1537. Object.values(data.users).forEach((item) => {
  1538. if (item.tags.length === 0) {
  1539. delete data.users[item.id];
  1540. }
  1541. });
  1542.  
  1543. saveData();
  1544. reFilter();
  1545. }
  1546. };
  1547.  
  1548. container.appendChild(tc);
  1549. }
  1550.  
  1551. // 删除没有用户的标记
  1552. {
  1553. const tc = document.createElement("div");
  1554.  
  1555. tc.innerHTML += `
  1556. <br/>
  1557. <div>
  1558. <button>删除没有用户的标记</button>
  1559. </div>
  1560. `;
  1561.  
  1562. const actions = tc.getElementsByTagName("button");
  1563.  
  1564. actions[0].onclick = () => {
  1565. if (confirm("是否确认?")) {
  1566. Object.values(data.tags).forEach((item) => {
  1567. if (
  1568. Object.values(data.users).filter((user) =>
  1569. user.tags.find((tag) => tag === item.id)
  1570. ).length === 0
  1571. ) {
  1572. delete data.tags[item.id];
  1573. }
  1574. });
  1575.  
  1576. saveData();
  1577. reFilter();
  1578. }
  1579. };
  1580.  
  1581. container.appendChild(tc);
  1582. }
  1583. };
  1584.  
  1585. return func;
  1586. })();
  1587.  
  1588. return {
  1589. name: "通用设置",
  1590. content,
  1591. refresh,
  1592. };
  1593. })();
  1594.  
  1595. u.addModule(userModule).toggle();
  1596. u.addModule(tagModule);
  1597. u.addModule(keywordModule);
  1598. u.addModule(commonModule);
  1599.  
  1600. // 增加菜单项
  1601. (() => {
  1602. const title = "过滤设置";
  1603.  
  1604. let window;
  1605.  
  1606. n.mainMenu.addItemOnTheFly(title, null, () => {
  1607. if (window === undefined) {
  1608. window = n.createCommmonWindow();
  1609. }
  1610.  
  1611. window._.addContent(null);
  1612. window._.addTitle(title);
  1613. window._.addContent(u.content);
  1614. window._.show();
  1615. });
  1616. })();
  1617.  
  1618. // 执行过滤
  1619. (() => {
  1620. const hookFunction = (object, functionName, callback) => {
  1621. ((originalFunction) => {
  1622. object[functionName] = function () {
  1623. const returnValue = originalFunction.apply(this, arguments);
  1624.  
  1625. callback.apply(this, [returnValue, originalFunction, arguments]);
  1626.  
  1627. return returnValue;
  1628. };
  1629. })(object[functionName]);
  1630. };
  1631.  
  1632. const initialized = {
  1633. topicArg: false,
  1634. postArg: false,
  1635. };
  1636.  
  1637. hookFunction(n, "eval", () => {
  1638. if (Object.values(initialized).findIndex((item) => item === false) < 0) {
  1639. return;
  1640. }
  1641.  
  1642. if (n.topicArg && initialized.topicArg === false) {
  1643. hookFunction(n.topicArg, "add", reFilter);
  1644.  
  1645. initialized.topicArg = true;
  1646. }
  1647.  
  1648. if (n.postArg && initialized.postArg === false) {
  1649. hookFunction(n.postArg, "proc", reFilter);
  1650.  
  1651. initialized.postArg = true;
  1652. }
  1653. });
  1654.  
  1655. if (n.ucp) {
  1656. hookFunction(n.ucp, "_echo", reFilter);
  1657. }
  1658.  
  1659. reFilter();
  1660. })();
  1661. })(commonui, __CURRENT_UID);